"선착순 이벤트 때 매크로가 1초에 1,000번씩 새로고침을 눌러서 서버가 다운되었습니다."
"특정 사용자가 API를 너무 많이 호출해서 다른 사용자가 이용을 못 합니다."
이런 상황을 막기 위해 **단위 시간당 요청 횟수를 제한(Throttling)**하는 것이 처리율 제한 패턴입니다. API 게이트웨이나 미들웨어에서 주로 사용되는데, 알고리즘 선택에 따라 **"경계 시간의 트래픽 두 배 문제"**를 해결할 수도, 못 할 수도 있습니다.
1. 방식 1: 고정 윈도우 카운터 (Fixed Window Counter) - "단순 무식"
시간을 고정된 단위(예: 1분)로 나누고, 각 단위마다 카운터를 둡니다. 구현이 가장 쉽고 메모리를 적게 먹습니다.
특징
- 동작 원리: "12:00:00 ~ 12:00:59" 구간에 카운터 0 -> 100 도달 시 차단. "12:01:00"이 되면 카운터 초기화.
- Redis 활용: INCR 명령어와 EXPIRE 명령어로 아주 쉽게 구현 가능합니다.
코드 예시 (Redis 개념)
Java
public boolean isAllowed(String userId) {
// 현재 분(Minute)을 키로 사용 (예: rate:userA:1205)
long currentMinute = System.currentTimeMillis() / 60000;
String key = "rate:" + userId + ":" + currentMinute;
// 카운트 증가
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
// 첫 요청이면 1분 뒤 만료 설정
redisTemplate.expire(key, 1, TimeUnit.MINUTES);
}
// 100회 넘으면 차단
return count <= 100;
}
치명적 문제점 (경계 문제)
"시간의 경계(Edge)에서 트래픽이 2배가 될 수 있습니다."
- 12:00:59초에 100개 요청 (허용됨)
- 12:01:01초에 100개 요청 (허용됨)
- 결과적으로 2초 만에 200개 요청이 폭주했지만, 알고리즘상으로는 문제가 없다고 판단하여 서버가 위험해질 수 있습니다.
2. 방식 2: 토큰 버킷 (Token Bucket) - "아마존/구글의 표준"
일정한 속도로 토큰을 채워 넣고, 요청이 올 때마다 토큰을 하나씩 꺼내 쓰는 방식입니다. 토큰이 없으면 요청을 거부합니다. 버스트(Burst, 일시적 폭주) 트래픽을 유연하게 처리하면서도 평균 속도를 일정하게 유지합니다.
특징
- 버킷: 토큰을 담는 그릇 (최대 용량 존재).
- 리필(Refill): 초당 N개씩 토큰이 자동으로 충전됨.
- 소비: 요청 시 토큰 1개 차감.
코드 예시 (Bucket4j 라이브러리 사용)
Java
// 1. 버킷 생성 (용량 100개, 1초마다 10개씩 충전)
Bucket bucket = Bucket4j.builder()
.addLimit(Bandwidth.classic(100, Refill.greedy(10, Duration.ofSeconds(1))))
.build();
// 2. 요청 처리
public void handleRequest(Request req) {
// 토큰 1개를 소비할 수 있는지 확인
if (bucket.tryConsume(1)) {
process(req); // 성공: 처리
} else {
throw new RateLimitExceededException(); // 실패: 429 Too Many Requests
}
}
장단점
- 장점:
- 경계 문제 해결: 시간이 흐름에 따라 토큰이 차기 때문에, 고정 윈도우 방식처럼 경계에서 트래픽이 2배로 튀는 문제가 없습니다.
- 유연성: 잠깐의 트래픽 폭주(Burst)는 버킷에 쌓여있던 토큰만큼 허용해 주고, 그 이후에는 정해진 속도(Refill Rate)로 제어합니다.
- 단점: 구현 로직이 고정 윈도우보다 복잡합니다. (보통은 Bucket4j, Guava RateLimiter 같은 라이브러리나 Redis-Cell을 사용합니다.)
3. 실무 비교: 언제 무엇을 쓰는가?
| 구분 | Fixed Window (단순 카운터) | Token Bucket / Sliding Window (정교함) |
| 구현 난이도 | 하 (Redis INCR로 끝) | 중/상 (알고리즘 또는 라이브러리 필요) |
| 정확도 | 낮음 (경계 시간 트래픽 2배 허용) | 높음 (평균 처리율 보장) |
| 메모리 사용 | 적음 | 중간 (상태값 관리 필요) |
| 트래픽 패턴 | 일정한 트래픽에 적합 | 돌발적인 폭주(Burst)가 잦은 서비스에 적합 |
| 추천 상황 | 간단한 내부 API 제한, 일일 쿼터 제한 | 대고객용 공용 API, 결제/로그인 등 민감한 API |
결론
"하루에 1,000번 호출 가능" 처럼 긴 시간 단위의 단순한 제한이라면 고정 윈도우(Fixed Window) 방식이 구현하기 쉽고 싸게 먹힙니다.
하지만 "초당 100회 제한(TPS)" 처럼 짧은 시간의 정교한 트래픽 제어가 필요하다면, 반드시 **토큰 버킷(Token Bucket)**이나 슬라이딩 윈도우 로그(Sliding Window Log) 방식을 사용해야 디도스(DDoS)성 트래픽이나 매크로로부터 서버를 안전하게 보호할 수 있습니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 재시도 패턴(Retry Pattern) 완벽 정리 (0) | 2025.12.07 |
|---|---|
| 헬스 체크 패턴(Health Check Pattern) 완벽 정리 (0) | 2025.12.07 |
| 트랜잭셔널 아웃박스 패턴(Transactional Outbox Pattern) 완벽 정리 (0) | 2025.12.07 |
| CQRS 패턴(CQRS Pattern) 완벽 정리 (0) | 2025.12.07 |
| API 게이트웨이 패턴(API Gateway) 완벽 정리 (0) | 2025.12.07 |