백엔드 면접 용어 사전
API·REST처리량 제한 · rate limiting · rate limit

Rate Limiting

일정 시간당 요청 수를 제한하는 보호 장치. 토큰 버킷·슬라이딩 윈도우로 구현한다.

일정 시간 안에 처리할 요청 수를 제한하는 장치.

왜 필요한가

  • 특정 사용자가 자원을 독점하는 것을 막는다
  • 악의적 공격(무차별 대입, 스크래핑)을 완화한다
  • 하위 시스템·외부 API의 한도를 지킨다
  • 비용을 통제한다 (호출당 과금되는 서비스)
  • 과부하로부터 시스템을 보호한다

서킷 브레이커가 "나가는 호출"을 보호한다면, rate limit은 "들어오는 요청"을 막는다.

알고리즘 네 가지

① 고정 윈도우 (Fixed Window) 10:00:00~10:00:59 에 100개 허용 가장 단순하지만 경계 문제가 있다.

10:00:59 에 100개  ┐
10:01:00 에 100개  ┘ → 1초 사이에 200개가 통과한다

② 슬라이딩 로그 (Sliding Log) 각 요청의 타임스탬프를 전부 저장하고, "지금부터 1분 전까지"를 센다 정확하지만 메모리를 많이 쓴다.

③ 슬라이딩 윈도우 (Sliding Window Counter) 현재 윈도우 카운트 + 이전 윈도우 카운트 × 겹치는 비율

  • 10:00 윈도우 80개, 10:01 윈도우 30개, 현재 10:01:15 (25% 경과)
  • 30 + 80 × 0.75 = 90 정확도와 메모리의 좋은 절충이라 실무에서 널리 쓰인다.

④ 토큰 버킷 (Token Bucket) 버킷에 초당 N개씩 토큰이 채워진다 (최대 M개) 요청 하나당 토큰 하나를 쓴다. 없으면 거부

  • 평소 안 쓰면 토큰이 쌓여 있다가 순간적으로 몰아 쓸 수 있다 (버스트 허용)

버스트를 허용하는 것이 장점이다. 사용자 경험이 자연스럽다 — 평소 조용하던 사용자가 잠깐 여러 번 클릭하는 것은 정상 행동이기 때문이다. AWS·Stripe 등 많은 서비스가 이 방식을 쓴다.

누출 버킷(Leaky Bucket) 은 반대로 출력 속도를 일정하게 만든다. 버스트를 허용하지 않고 평탄화한다.

분산 환경에서

  • 서버가 5대인데 각자 세면?
  • 각 서버가 100개씩 허용 = 실제로 500개 통과 ✗

Redis에 중앙 집중시켜야 한다.

INCR ratelimit:user:123:1704067200 EXPIRE ratelimit:user:123:1704067200 60

원자성이 중요하다 — INCR과 EXPIRE 사이에 문제가 생기면 키가 영원히 남는다. Lua 스크립트로 묶어 원자적으로 실행한다.

무엇을 기준으로 셀 것인가

기준특징
IP로그인 전에도 가능. 단, NAT·회사망은 여러 명이 한 IP
사용자 ID정확하지만 로그인 후에만
API 키B2B API에 적합
엔드포인트별로그인 5회/분, 조회 1000회/분처럼 차등

여러 층으로 겹치는 것이 실무적이다 — IP 기준 전역 제한 + 사용자 기준 제한 + 민감한 엔드포인트 추가 제한.

응답 — 표준 헤더

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

Retry-After를 반드시 준다. 클라이언트가 언제 다시 시도할지 알 수 있어, 무작정 재시도해 부하를 더하는 것을 막는다.

설계 시 고려할 것

  • ① 제한에 걸리는 것이 정상 사용자인가 공격자인가
    • 정상 사용자가 자주 걸리면 한도가 잘못된 것
  • ② 429가 사용자에게 어떻게 보이는가
    • "잠시 후 다시 시도해 주세요" + 남은 시간 표시
  • ③ 화이트리스트
    • 내부 시스템·모니터링은 제외
  • ④ 점진적 제재
    • 즉시 차단보다 지연(throttling)이 나은 경우도 있다

스프링에서

// Bucket4j + Redis
Bucket bucket = Bucket.builder()
    .addLimit(Bandwidth.classic(100, Refill.intervally(100, ofMinutes(1))))
    .build();

if (!bucket.tryConsume(1)) {
    return ResponseEntity.status(429).header("Retry-After", "60").build();
}

보통 Filter나 Interceptor에 두어 컨트롤러 진입 전에 차단한다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — API·REST 설계 — 멱등성·상태코드·버저닝·GraphQL