express-rate-limit로 API 요청 제한 구현
express-rate-limit을 활용해 Node.js API에서 IP 기반 요청 제한을 적용하는 방법과 설정 예시, 운영 시 고려사항을 단계별로 설명
목차
개요: 왜 Rate Limiting이 필요한가
웹 API는 과도한 요청, 악성 스크립트, 또는 서비스 남용으로부터 보호가 필요하다. Rate Limiting은 단위 시간당 허용 요청 수를 제한해 서비스 가용성을 지키고 비용을 절감한다. 특히 IP 기반 요청 제한은 간단하면서도 즉각적인 방어 수단으로 널리 사용된다.
express-rate-limit 소개
express-rate-limit은 Express 애플리케이션에 쉽게 통합되는 미들웨어다. 기본 메모리 스토어를 제공하며 구성만으로 빠르게 적용할 수 있다. 단, 단일 프로세스 환경이나 개발 단계에서는 적합하지만, 다중 인스턴스 운영 환경에서는 중앙 저장소(Redis 등)를 고려해야 한다.
핵심 개념
- windowMs: 제한을 적용할 시간 윈도우(밀리초)
- max: 윈도우 내 허용 요청 수
- keyGenerator: 요청을 식별할 키(기본: IP)
- handler: 제한 초과 시 호출되는 함수
간단한 설정 예시
아래 예시는 기본적인 express-rate-limit 적용 방법이다. 이 설정은 동일 IP에서 15분(900000ms)에 100회까지만 요청을 허용한다.
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
standardHeaders: true, // RFC 6585 관련 응답 헤더 포함
legacyHeaders: false, // X-RateLimit-* 헤더 비활성화
});
app.use(limiter);
app.get('/', (req, res) => {
res.send('Hello, world!');
});
app.listen(3000);
설정 항목별 설명
windowMs와 max
windowMs는 제한을 적용할 시간 범위다. max는 그 범위 내 허용되는 요청 수다. 짧은 윈도우와 낮은 max는 공격 방어에 유리하지만 정상 사용자 경험을 해칠 수 있다. 서비스 특성에 맞춰 균형을 맞춘다.
핸들러와 응답 포맷
기본 응답은 HTML 텍스트지만 handler 옵션으로 JSON이나 커스텀 메시지를 반환하도록 쉽게 변경할 수 있다. API 서비스라면 JSON 형식을 권장한다.
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10,
handler: (req, res) => {
res.status(429).json({ error: 'Too many requests. Please try again later.' });
}
});
운영 환경 고려사항
기본 메모리 스토어는 단일 프로세스에서만 동작하므로, 여러 인스턴스를 사용하는 경우 각 인스턴스별로 카운트가 유지되어 의도한 제한이 적용되지 않을 수 있다. 이때 Redis 같은 중앙 저장소를 사용한다.
Redis를 이용한 스토어 예시
rate-limit-redis 같은 스토어를 사용하면 다중 인스턴스 환경에서도 일관된 제한이 가능하다. 아래는 Redis 스토어를 연결한 예시 코드다.
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const redisClient = redis.createClient({
url: 'redis://localhost:6379'
});
const limiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args) }),
windowMs: 15 * 60 * 1000,
max: 100
});
app.use(limiter);
세분화된 적용 전략
모든 라우트에 동일한 제한을 적용하는 대신 엔드포인트별, 인증 상태별로 다른 정책을 적용하면 유연한 방어가 가능하다.
- 인증되지 않은 라우트: 더 엄격한 제한
- 로그인한 사용자: 사용자 ID를 키로 사용
- 민감한 엔드포인트(로그인, 결제): 추가적인 보호 레이어 적용
사용자 식별 키 변경
keyGenerator를 활용하면 기본 IP 대신 사용자 ID나 API 키를 사용해 제한할 수 있다. 이렇게 하면 동일한 IP에서 여러 사용자가 접근해도 각 사용자별 제한을 유지할 수 있다.
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 30,
keyGenerator: (req) => {
return req.user ? req.user.id : req.ip;
}
});
테스트와 모니터링
적용 후에는 실제 트래픽을 흉내 내어 테스트한다. 또한 모니터링을 통해 정상 사용자의 차단 여부, 차단 빈도, Redis 키 사용량을 점검한다. 로그와 메트릭을 통해 정책을 조정하면 과도한 차단을 피할 수 있다.
마무리: 실무 적용 체크리스트
- 서비스 특성에 맞는 windowMs와 max 설정
- 다중 인스턴스 환경에서는 Redis 등 중앙 스토어 사용
- 핸들러를 통해 일관된 API 응답 형식 유지
- 엔드포인트별, 사용자 유형별 맞춤 정책 적용
- 테스트 및 운영 모니터링으로 정책 세부 조정
express-rate-limit 설정은 간단하지만, 잘못된 값은 정상 사용자 경험을 해칠 수 있다. 위 내용을 바탕으로 Node.js API rate limit 구현과 IP 기반 요청 제한 Node 환경에서의 적용을 단계적으로 진행하면 서비스 보호에 효과적이다.