API 레이트 리미팅 · 개념 가이드

# 토큰 버킷 알고리즘
동작 원리

토큰 버킷(Token Bucket)은 API 요청 속도를 제어하는 가장 널리 쓰이는 알고리즘입니다. 짧은 트래픽 버스트를 허용하면서도 장기 평균 처리량을 일정하게 유지합니다.

레이트 리미팅 토큰 버킷 Spring Boot 초급-중급

레이트 리미팅 없이 API를 공개하면 단일 클라이언트의 폭발적인 요청이 전체 서비스를 마비시킬 수 있습니다. 토큰 버킷은 각 클라이언트에게 가상의 버킷을 할당하고, 버킷에 토큰이 있을 때만 요청을 허가합니다.

핵심 비유

지하철 교통카드를 생각하세요. 잔액이 있으면 탑승(요청)이 가능하고, 잔액이 없으면 게이트(서버)가 막힙니다. 잔액은 일정 속도로 자동 충전됩니다.

## 동작 원리

1 버킷 채우기 (Token Refill) 수동 없음

서버는 각 클라이언트(또는 IP, API 키)마다 버킷 하나를 관리합니다. 버킷에는 최대 용량(capacity)이 있고, 고정 속도(refill_rate 토큰/초)로 토큰이 채워집니다.

예) `capacity = 100`, `refill_rate = 10 req/s` — 버킷이 빈 상태에서 10초 후 완전 충전.

    bucket.tokens = min(capacity, bucket.tokens + refill_rate * elapsed)

2 토큰 소비 (Token Consumption) 요청마다

요청이 들어올 때마다 버킷에서 토큰 1개(또는 가중치)를 차감합니다. 버킷에 토큰이 충분하면 요청이 통과됩니다.

    if bucket.tokens >= cost:
        bucket.tokens -= cost
        return ALLOW
    else:
        return DENY

가중치(cost)를 통해 무거운 엔드포인트에 더 많은 토큰을 요구할 수 있습니다. 예) 대용량 파일 업로드는 `cost = 5`.

3 초과 거절 & 응답 헤더 429 반환

토큰이 부족하면 서버는 HTTP 429 Too Many Requests를 반환합니다. 클라이언트가 재시도 타이밍을 알 수 있도록 표준 헤더를 포함해야 합니다.

    HTTP/1.1 429 Too Many Requests
    X-RateLimit-Limit: 100
    X-RateLimit-Remaining: 0
    X-RateLimit-Reset: 1746787200
    Retry-After: 8

`Retry-After`는 다음 토큰이 채워질 때까지 남은 초(second)를 나타냅니다. 클라이언트는 이 값을 보고 지수 백오프 대신 정확한 대기 시간을 계산할 수 있습니다.

## 코드 예시

    // Bucket4j + Spring Boot 예시
    @RateLimiter(name = "commentApi", fallbackMethod = "rateLimitFallback")
    @PostMapping("/comments")
    public ResponseEntity<Comment> createComment(@RequestBody CommentRequest req) {
        return ResponseEntity.ok(commentService.create(req));
    }

    public ResponseEntity<?> rateLimitFallback(CommentRequest req, Throwable t) {
        return ResponseEntity.status(429)
            .header("Retry-After", "8")
            .body(Map.of("error", "too_many_requests"));
    }

    # redis-py + slowapi 예시
    from slowapi import Limiter
    from slowapi.util import get_remote_address

    limiter = Limiter(key_func=get_remote_address)

    @app.post("/comments")
    @limiter.limit("10/second")
    async def create_comment(request: Request, body: CommentBody):
        return await comment_service.create(body)

    # nginx.conf
    http {
        limit_req_zone $binary_remote_addr
            zone=comment_api:10m rate=10r/s;

        server {
            location /api/v1/comments {
                limit_req zone=comment_api burst=20 nodelay;
                limit_req_status 429;
                proxy_pass http://backend;
            }
        }
    }

## 자주 묻는 질문

슬라이딩 윈도우와 토큰 버킷의 차이가 무엇인가요?

슬라이딩 윈도우(Sliding Window)는 지정된 기간(예: 최근 1분) 안의 요청 수를 카운트합니다. 토큰 버킷은 버스트(burst)를 허용한다는 점이 다릅니다 — 버킷이 가득 찬 상태에서는 짧은 시간에 `capacity`만큼의 요청을 순간적으로 처리할 수 있습니다.

API 응답 일관성이 중요하면 슬라이딩 윈도우를, 합리적 버스트 허용이 필요하면 토큰 버킷을 선택하세요.

Redis 없이 토큰 버킷을 구현할 수 있나요?

단일 인스턴스 환경이라면 인메모리(ConcurrentHashMap 등)로도 충분합니다. 하지만 다중 인스턴스(로드 밸런서 하단) 환경에서는 Redis 또는 Hazelcast 같은 분산 캐시가 필수입니다. 인스턴스별로 버킷을 따로 관리하면 실질 한도가 인스턴스 수만큼 늘어납니다.

429 응답을 받은 클라이언트는 어떻게 처리해야 하나요?

`Retry-After` 헤더가 있으면 해당 초(second)만큼 대기 후 재시도합니다. 헤더가 없으면 지수 백오프(exponential backoff)를 사용하세요 — 1s → 2s → 4s → … 순으로 대기 시간을 늘립니다.

SDKs(예: Axios, fetch wrapper) 레벨에서 `429` 인터셉터를 글로벌로 설정해 두면 개별 API 호출 코드에서 처리를 반복할 필요가 없습니다.

사용자별 / API 키별로 다른 한도를 설정하려면?

버킷 키를 `userId` 또는 `apiKey`로 분리하고, 플랜(Tier)에 따라 `capacity`와 `refill_rate`를 달리 설정합니다.

예) Free 플랜: `10 req/s, capacity 50` · Pro 플랜: `100 req/s, capacity 500`. Redis Hash에 `tier → config` 맵을 두면 동적으로 변경 가능합니다.