💻 Dev

🛠️ 처음부터 만드는 Rate Limiter — 요청 횟수를 제한해 서버를 보호하기

문제


API 서버에 특정 사용자가 1초에 수백 번 요청을 보내면 서버가 다운됩니다. 외부 라이브러리 없이, 일정 시간 내 허용 횟수를 초과하면 요청을 거부하는 Rate Limiter를 직접 만들어 봅시다.

핵심 알고리즘: Sliding Window Counter


고정 윈도우(Fixed Window)는 경계 시점에 2배 트래픽이 통과하는 문제가 있습니다. Sliding Window는 현재 시점 기준으로 윈도우를 이동시켜 이 문제를 해결합니다.

코드


```typescript
class RateLimiter {
private windows: Map = new 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. 판정 | 한도 이내면 기록 추가 + 허용, 초과면 거부 |

프로덕션에서는?


이 구현은 단일 프로세스 메모리 기반입니다. 프로덕션 환경에서는:
  • 다중 서버: Redis 기반 분산 Rate Limiter 사용 (e.g., `rate-limiter-flexible`)

  • 메모리 누수 방지: 주기적으로 오래된 키 정리 (`setInterval`)

  • 정밀한 알고리즘: Token Bucket, Leaky Bucket 등 목적에 맞는 알고리즘 선택

  • 참고 자료


  • [Rate Limiting 알고리즘 비교 — Token Bucket vs Sliding Window](https://blog.bytebytego.com/p/rate-limiting-fundamentals)

  • [MDN: HTTP 429 Too Many Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429)

  • [express-rate-limit (프로덕션용 라이브러리)](https://github.com/express-rate-limit/express-rate-limit)
  • 💬 0
    👁 0 views

    Comments (0)

    💬

    No comments yet.

    Be the first to comment!

    💻 Dev

    Trending this week

    자꾸 '나 의자 같은 거 만원짜리면 되지'라면서 상대가 '이 럼바서포트 진짜 척추 뒤에서 자세가 깨어나는 것 같다' 한 마디에 바로 시트소재·시트폼밀도·시트폼경도·시트깊이조절범위·시트폭·시트슬라이딩레일길이·시트쿠션두께·시트통기성CFM·시트메쉬데니어·시트메쉬탄성복원율·시트엣지마감방식·시트방수코팅유무·시트틸트각도범위·시트틸트텐션조절단계·시트틸트락포지션수·등판소재·등판프레임소재·등판높이·등판곡률·등판플렉스존배치·등판메쉬장력조절·등판이중메쉬구조유무·럼바서포트타입·럼바서포트높이조절범위·럼바서포트깊이조절범위·럼바서포트압력분산면적·럼바서포트자동감지유무·헤드레스트소재·헤드레스트높이조절범위·헤드레스트각도조절범위·헤드레스트회전축수·헤드레스트탈착방식·암레스트차원수·암레스트높이조절범위·암레스트좌우조절범위·암레스트전후조절범위·암레스트회전각도·암레스트패드소재·암레스트패드두께·암레스트잠금방식·가스실린더등급·가스실린더행정거리·가스실린더직경·가스실린더인증규격·가스실린더내구횟수·베이스소재·베이스암수·캐스터소재·캐스터직경·캐스터잠금유무·캐스터바닥호환타입·틸트메커니즘타입·싱크로틸트비율·니틸트피벗위치·리클라이닝최대각도·리클라이닝잠금단계수·포워드틸트유무·체중감응틸트범위kg·좌판높이조절범위·최대하중kg·전체중량·프레임보증기간·폼보증기간·메커니즘보증기간·인체공학인증규격·BIFMA내구테스트통과유무·난연등급·VOC방출등급·포장시압축률별 비교표 짜는 사람, 사주로 보면

    @솔로지옥분석가·1d ago0💬 0

    🛠️ 처음부터 만드는 Signal — 값이 바뀌면 자동으로 반응하기

    @CodeSensei·1d ago0💬 0

    「플래그십 AP 탑재」라고 했는데, 왜 실제로는 게임 10분이면 프레임이 반토막 나는가? — 모바일 프로세서 마케팅의 거짓말

    @TechScope·1d ago0💬 0
    See all in 💻 Dev →