🛠️ 처음부터 만드는 Rate Limiter — 요청 횟수를 제한해 서버를 보호하기
문제
API 서버에 특정 사용자가 1초에 수백 번 요청을 보내면 서버가 다운됩니다. 외부 라이브러리 없이, 일정 시간 내 허용 횟수를 초과하면 요청을 거부하는 Rate Limiter를 직접 만들어 봅시다.
핵심 알고리즘: Sliding Window Counter
고정 윈도우(Fixed Window)는 경계 시점에 2배 트래픽이 통과하는 문제가 있습니다. Sliding Window는 현재 시점 기준으로 윈도우를 이동시켜 이 문제를 해결합니다.
코드
```typescript
class RateLimiter {
private windows: Map
constructor(
private maxRequests: number, // 허용 횟수
private windowMs: number // 윈도우 크기 (ms)
) {}
isAllowed(key: string): boolean {
const now = Date.now();
const cutoff = now - this.windowMs;
// 해당 키의 요청 기록 가져오기
let records = this.windows.get(key) ?? [];
// 윈도우 밖의 오래된 기록 제거
records = records.filter(r => r.timestamp > cutoff);
// 현재 윈도우 내 총 요청 수 계산
const totalCount = records.reduce((sum, r) => sum + r.count, 0);
if (totalCount >= this.maxRequests) {
this.windows.set(key, records);
return false; // 🚫 제한 초과
}
// 요청 기록 추가
records.push({ count: 1, timestamp: now });
this.windows.set(key, records);
return true; // ✅ 허용
}
// 남은 요청 횟수 확인
remaining(key: string): number {
const now = Date.now();
const cutoff = now - this.windowMs;
const records = (this.windows.get(key) ?? []).filter(r => r.timestamp > cutoff);
const used = records.reduce((sum, r) => sum + r.count, 0);
return Math.max(0, this.maxRequests - used);
}
// 리셋까지 남은 시간 (ms)
retryAfter(key: string): number {
const records = this.windows.get(key) ?? [];
if (records.length === 0) return 0;
const oldest = records[0].timestamp;
return Math.max(0, oldest + this.windowMs - Date.now());
}
}
```
사용 예시
```typescript
// 10초 동안 최대 5번 요청 허용
const limiter = new RateLimiter(5, 10_000);
function handleRequest(userId: string) {
if (!limiter.isAllowed(userId)) {
const retry = Math.ceil(limiter.retryAfter(userId) / 1000);
console.log(`🚫 ${userId}: 제한 초과! ${retry}초 후 재시도`);
return { status: 429, retryAfter: retry };
}
const left = limiter.remaining(userId);
console.log(`✅ ${userId}: 허용 (남은 횟수: ${left})`);
return { status: 200 };
}
// 테스트: 빠르게 7번 요청
for (let i = 0; i < 7; i++) {
handleRequest('user-123');
}
// ✅ user-123: 허용 (남은 횟수: 4)
// ✅ user-123: 허용 (남은 횟수: 3)
// ✅ user-123: 허용 (남은 횟수: 2)
// ✅ user-123: 허용 (남은 횟수: 1)
// ✅ user-123: 허용 (남은 횟수: 0)
// 🚫 user-123: 제한 초과! 10초 후 재시도
// 🚫 user-123: 제한 초과! 10초 후 재시도
```
Express 미들웨어로 적용
```typescript
import { Request, Response, NextFunction } from 'express';
function rateLimit(max: number, windowMs: number) {
const limiter = new RateLimiter(max, windowMs);
return (req: Request, res: Response, next: NextFunction) => {
const key = req.ip ?? 'unknown';
if (!limiter.isAllowed(key)) {
const retryAfter = Math.ceil(limiter.retryAfter(key) / 1000);
res.set('Retry-After', String(retryAfter));
res.set('X-RateLimit-Remaining', '0');
return res.status(429).json({
error: 'Too Many Requests',
retryAfter,
});
}
res.set('X-RateLimit-Remaining', String(limiter.remaining(key)));
next();
};
}
// 1분에 100번 제한
app.use('/api', rateLimit(100, 60_000));
```
동작 원리
| 단계 | 설명 |
|------|------|
| 1. 키 식별 | IP, 사용자 ID 등으로 요청자를 구분 |
| 2. 윈도우 정리 | 현재 시점 기준, 윈도우 밖 기록 삭제 |
| 3. 횟수 확인 | 윈도우 내 요청 수 합산 → 한도 비교 |
| 4. 판정 | 한도 이내면 기록 추가 + 허용, 초과면 거부 |
프로덕션에서는?
이 구현은 단일 프로세스 메모리 기반입니다. 프로덕션 환경에서는:
Comments (0)
💬
No comments yet.
Be the first to comment!