API 속도 제한: 알고리즘 및 구현
당신의 API가 공격을 받고 있습니다. 정교한 해커가 아니라, 초당 수천 번씩 엔드포인트를 두드려 리소스를 소모하고 정상 사용자의 서비스를 저하시키는 단순한 스크립트에 의해서입니다. 속도 제한이 없으면 단 하나의 잘못된 클라이언트가 전체 애플리케이션을 다운시킬 수 있습니다. 이 가이드는 핵심 알고리즘을 설명하고 Node.js와 Nginx에서 실용적인 구현을 보여줍니다.
속도 제한이 중요한 이유
속도 제한은 클라이언트가 주어진 시간 창에서 얼마나 많은 요청을 할 수 있는지 제어합니다. 이는 다음과 같은 공격에 대한 첫 번째 방어선입니다:
- 로그인 또는 API 키 엔드포인트에 대한 무차별 대입 공격
- 과도한 요청으로 인한 서비스 거부(DoS)
- 데이터베이스 쿼리나 파일 처리와 같은 비용이 많이 드는 작업으로 인한 리소스 고갈
- 스크래퍼나 봇에 의한 무료 티어 남용
보안 외에도 속도 제한은 공정한 사용을 보장하고 계층형 가격 계획과 같은 비즈니스 규칙을 시행하는 데 도움이 됩니다.
속도 제한 알고리즘
네 가지 알고리즘이 API 속도 제한을 지배합니다. 각각 정확성, 메모리 사용량, 버스트 처리에서 트레이드오프가 있습니다.
1. 고정 윈도우
고정된 시간 간격(예: 분당 100 요청)으로 요청을 계산합니다. 윈도우가 재설정되면 카운터도 재설정됩니다.
- 장점: 간단하고 메모리 사용량이 적음(클라이언트당 하나의 카운터).
- 단점: 윈도우 경계에서 버스트 발생. 클라이언트가 12:00:59에 100개 요청을 보내고 12:01:00에 또 100개를 보내면 2초 동안 200개 요청이 됩니다.
2. 슬라이딩 윈도우
각 요청의 타임스탬프를 추적하고 지난 N초 내에 몇 개가 포함되는지 계산합니다. 이는 버스트를 완화합니다.
- 장점: 정확하고 경계 스파이크가 없음.
- 단점: 메모리 사용량이 높음(타임스탬프 저장) 또는 가중 평균을 사용한 슬라이딩 윈도우 카운터 사용.
3. 토큰 버킷
버킷에 토큰이 들어 있습니다. 토큰은 고정된 속도로 추가됩니다. 각 요청은 토큰 하나를 소비합니다. 버킷이 비어 있으면 요청이 거부됩니다.
- 장점: 버킷 크기까지 버스트를 허용한 후 평균 속도를 적용.
- 단점: 클라이언트당 토큰 수와 마지막 리필 시간을 저장해야 함.
4. 리키 버킷
요청이 큐(버킷)에 들어가고 일정한 속도로 처리됩니다. 큐가 가득 차면 요청이 삭제됩니다.
- 장점: 트래픽을 일정한 속도로 완화하여 다운스트림 서비스 보호에 이상적.
- 단점: 지연 시간이 추가되어 실시간 API에는 부적합.
| 알고리즘 | 버스트 처리 | 메모리 | 사용 사례 |
|---|---|---|---|
| 고정 윈도우 | 나쁨 | 낮음 | 간단한 API |
| 슬라이딩 윈도우 | 좋음 | 중간 | 범용 |
| 토큰 버킷 | 탁월함 | 중간 | 버스트 허용 API |
| 리키 버킷 | 없음 | 중간 | 트래픽 완화 |
Node.js에서 속도 제한 구현
Express와 Redis를 사용하여 분산 상태를 가진 토큰 버킷 속도 제한기를 구현합니다. Redis는 여러 서버 인스턴스가 있을 때 필수적입니다.
1단계: 의존성 설치
npm install express redis
2단계: 속도 제한 미들웨어 생성
const redis = require('redis');
const client = redis.createClient();
async function tokenBucketLimiter(req, res, next) {
const key = `rate_limit:${req.ip}`;
const capacity = 10; // max tokens
const refillRate = 1; // tokens per second
const now = Date.now();
const data = await client.hGetAll(key);
let tokens = data.tokens ? parseFloat(data.tokens) : capacity;
let lastRefill = data.lastRefill ? parseInt(data.lastRefill) : now;
// Refill tokens based on elapsed time
const elapsed = (now - lastRefill) / 1000;
tokens = Math.min(capacity, tokens + elapsed * refillRate);
if (tokens < 1) {
return res.status(429).json({ error: 'Too many requests' });
}
tokens -= 1;
await client.hSet(key, {
tokens: tokens.toString(),
lastRefill: now.toString()
});
await client.expire(key, 60); // auto-cleanup
next();
}
app.use(tokenBucketLimiter);
이 미들웨어는 토큰 수를 원자적으로 확인하고 업데이트합니다. 프로덕션에서는 경쟁 조건을 피하기 위해 Redis 트랜잭션 또는 Lua 스크립트를 사용하세요.
Nginx에서 속도 제한 구현
Nginx는 limit_req 모듈로 내장 속도 제한을 제공합니다. 이는 리키 버킷 알고리즘을 사용합니다.
1단계: 속도 제한 영역 정의
nginx.conf의 http 블록에서:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
이것은 api라는 10MB 영역을 생성하여 IP당 초당 10개 요청을 허용합니다.
2단계: 제한 적용
location 블록에서:
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
burst=20은 최대 20개 요청의 짧은 버스트를 허용합니다. nodelay는 버스트 요청을 대기열에 넣지 않고 즉시 처리합니다.
API 속도 제한 모범 사례
- 적절한 상태 코드 반환:
429 Too Many Requests를 사용하고Retry-After헤더를 포함하세요. - 클라이언트를 올바르게 식별: 가능하면 IP 주소 대신 API 키나 사용자 ID를 사용하세요. IP는 공유(NAT)되거나 스푸핑될 수 있습니다.
- 상태 분산: 다중 인스턴스 배포에는 Redis나 유사한 저장소를 사용하세요.
- 로그 및 모니터링: 속도 제한 적중을 추적하여 공격을 감지하고 임계값을 조정하세요. Nginx Log Analyzer와 같은 도구는 429 응답을 분석하고 악용 IP를 식별하는 데 도움이 됩니다.
- 제한 사항 전달: API 문서에 속도 제한을 문서화하고
X-RateLimit-Limit,X-RateLimit-Remaining과 같은 헤더를 포함하세요.
FAQ
속도 제한과 스로틀링의 차이점은 무엇인가요?
속도 제한은 임계값을 초과하는 요청을 차단하는 반면, 스로틀링은 요청을 느리게 합니다(예: 대기열에 넣거나 지연). 속도 제한은 이진적이고, 스로틀링은 점진적입니다.
어떤 속도 제한 알고리즘을 사용해야 하나요?
대부분의 API에는 토큰 버킷이 좋은 균형을 제공합니다: 버스트를 허용하면서 평균 속도를 적용합니다. 엄격한 완화가 필요하면 리키 버킷을 사용하세요. 단순성을 위해 고정 윈도우는 저트래픽 API에 적합합니다.
인증된 사용자와 익명 사용자의 속도 제한을 어떻게 처리하나요?
익명 사용자(예: IP별)에게는 더 엄격한 제한을, 인증된 사용자(예: API 키별)에게는 더 관대한 제한을 적용하세요. 구독 계획에 따라 계층형 제한을 구현할 수도 있습니다.
API 트래픽을 분석할 준비가 되셨나요? Nginx Log Analyzer를 사용하여 로그를 파싱하고, 속도 제한 위반을 발견하고, 임계값을 최적화하세요.