프로그래밍/디자인패턴

처리율 제한 패턴(Rate Limiter Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 20:40

"선착순 이벤트 때 매크로가 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)성 트래픽이나 매크로로부터 서버를 안전하게 보호할 수 있습니다.