Rate Limiting: API가 요청에 빠지는 것을 방지하는 방법

발행: (2026년 3월 11일 AM 03:34 GMT+9)
10 분 소요
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line and all formatting exactly as you requested.

Introduction

안녕하세요! 저는 Jairo입니다
당신이 가장 좋아하는 dev.to 작가.
농담이에요 — 제가 그렇다는 걸 알고 있어요. 분위기를 깨는 중 😄

지난 주에 저는 Alex Xu가 쓴 System Design Interview라는 훌륭한 책을 읽고 있었습니다. 백엔드 시스템을 다루고 있다면 아직 읽지 않았다면, 읽어보는 것이 좋을 것입니다.

그 책의 한 개념이 소프트웨어 엔지니어에 대한 흥미로운 점을 떠올리게 했습니다: 우리는 모두 레이트 리밋이 존재한다는 것을 알지만, 실제로 언제 사용해야 하는지, 내부적으로 어떻게 동작하는지, 어떤 전략을 선택해야 하는지 이해하는 엔지니어는 거의 없습니다.

그래서 오늘은 여러분의 API가 가질 수 있는 가장 중요한 보호 중 하나인 레이트 리밋에 대해 이야기해보겠습니다.

🌧️ 간단한 비유

Your application is a person walking in the rain, and every raindrop represents an HTTP request hitting your server.

  1. 처음에는 모든 것이 괜찮다.
  2. 그러다 비가 점점 더 세져 → 빗방울이 더 많아지고, 요청도 더 많아진다.
  3. 결국 애플리케이션이 완전히 젖는다 — CPU 사용량이 급증하고, 데이터베이스가 버거워지며, 서버가 수프가 된다.

이상적이지 않다.

비가 올 때 우리는 무엇을 할까?
우리는 우산을 잡는다. (아니, 레지던트 이블의 악당 기업이 아니라 실제 우산이다.)

그 우산은 rate limiter를 의미한다. 비를 완전히 막지는 않지만, 얼마나 많은 비가 닿는지를 조절한다. 같은 방식으로, rate limiter는 일부 요청은 통과시키고 과도한 요청은 차단하여 시스템이 과부하되는 것을 방지한다.

🚫 왜 속도 제한이 중요한가

속도 제한이 없으면 클라이언트가 다음과 같이 요청을 보낼 수 있습니다:

  • 초당 10 요청
  • 초당 100 요청
  • 초당 1 000 요청

애플리케이션은 들어오는 모든 요청을 처리하려고 시도하게 되고, 결국 다음과 같은 문제가 발생합니다:

  • CPU 과부하
  • 데이터베이스 경쟁
  • 연쇄적인 장애
  • 전체 시스템 충돌

속도 제한을 적용하면 서버는 간단히 다음과 같이 응답할 수 있습니다:

HTTP 429 – Too Many Requests

즉, 서버가 말하는 것은:

“천천히 해, 친구.”

Source:

📊 일반적인 Rate‑Limiting 전략

1. 토큰 버킷

  • 아이디어: 버킷에 일정 수의 토큰이 들어 있다. 들어오는 요청마다 토큰 하나를 소모한다.
  • 리필: 토큰은 고정된 비율로 다시 추가된다.

예시 설정

매개변수
버킷 용량10 토큰
리필 속도초당 1 토큰
  • 버킷 용량만큼 짧은 버스트를 허용한다.
  • 버킷이 비면 새로운 토큰이 채워질 때까지 요청은 대기해야 한다.

왜 인기가 있을까? 버스트를 허용하면서도 전체 트래픽을 제어할 수 있다.

2. 리키 버킷

  • 아이디어: 요청이 버킷에 들어가면 구멍을 통해 일정한 속도로 새어나간다(물 흐름처럼).

  • 버킷이 가득 차면 새로운 요청은 거부된다.

  • 효과: 일정하고 예측 가능한 요청 속도를 강제하여 트래픽 급증을 완화한다.

  • 단점: 토큰 버킷만큼 버스트를 잘 처리하지 못한다.

3. 슬라이딩 윈도우 로그

  • 아이디어: 모든 요청의 타임스탬프를 저장한다.

  • 분당 5 요청 제한의 경우, 시스템은 최근 60초 이내의 모든 타임스탬프를 확인한다.

  • 장점: 매우 정확하며 실제 시간 윈도우를 항상 사용한다.

  • 단점: 많은 타임스탬프를 저장해야 하므로 대규모에서는 비용이 많이 든다.

4. 슬라이딩 윈도우 카운터 (최적화된 슬라이딩 윈도우)

  • 아이디어: 두 개의 카운터만 유지한다:

    1. 현재 윈도우 내 요청 수
    2. 이전 윈도우 내 요청 수
  • 두 카운터 사이의 가중 평균을 계산해 실제 요청 비율을 추정한다.

  • 장점: 메모리 사용량을 크게 줄이면서도 충분히 정확한 결과를 제공한다.

  • 주요 사용처: 대규모 분산 시스템.

🛠️ Simple Java 구현

다음 예제들은 핵심 아이디어를 설명하기 위해 단순화되었습니다.

1. Fixed‑Window (클라이언트당 초당 1 요청)

import java.util.concurrent.ConcurrentHashMap;

public class SimpleRateLimiter {

    private final ConcurrentHashMap<String, Long> lastRequest = new ConcurrentHashMap<>();

    /** Returns true if the request is allowed */
    public boolean allowRequest(String clientId) {
        long now = System.currentTimeMillis();
        Long last = lastRequest.get(clientId);

        if (last == null || now - last > 1000) { // > 1 second
            lastRequest.put(clientId, now);
            return true;
        }
        return false;
    }
}

2. Token Bucket (용량 = 10)

import java.util.concurrent.atomic.AtomicInteger;

public class TokenBucket {

    private final int capacity = 10;
    private final AtomicInteger tokens = new AtomicInteger(capacity);

    /** Returns true if a token is available */
    public boolean allowRequest() {
        if (tokens.get() > 0) {
            tokens.decrementAndGet();
            return true;
        }
        return false;
    }

    /** Refill one token – typically called by a scheduled task */
    public void refill() {
        // refill logic here
    }
}

스케줄러 작업이 주기적으로 refill()을 호출할 수 있습니다(예: 매초).

3. 라이브러리 사용 – Resilience4j

import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.vavr.control.Try;

import java.time.Duration;
import java.util.function.Supplier;

RateLimiterConfig config = RateLimiterConfig.custom()
        .limitForPeriod(5)                     // max 5 calls
        .limitRefreshPeriod(Duration.ofSeconds(1))
        .timeoutDuration(Duration.ofMillis(0))
        .build();

RateLimiter rateLimiter = RateLimiter.of("apiLimiter", config);

Supplier<String> decoratedSupplier =
        RateLimiter.decorateSupplier(rateLimiter, () -> "Hello API");

Try.ofSupplier(decoratedSupplier)
   .onFailure(e -> System.out.println("Rate limit exceeded"));
  • 장점: Spring Boot, Micrometer 등 생태계 도구와 잘 통합됩니다.
  • 단점: 외부 의존성이 추가되며, 설정 옵션을 이해해야 합니다.

📚 요약

  1. Rate limiting은 서비스가 과부하, 악용 및 연쇄 실패로부터 보호합니다.
  2. 트래픽 패턴에 맞는 전략을 선택하세요:
    • Token Bucket → 버스트 트래픽, 유연함.
    • Leaky Bucket → 부드럽고 일정한 흐름.
    • Sliding Window Log → 정밀한 제한, 메모리 사용량 증가.
    • Sliding Window Counter → 분산 시스템에 적합한 균형.
  3. 프로덕션 환경에서는 직접 구현하기보다 검증된 라이브러리(예: Resilience4j, Bucket4j, Spring Cloud Gateway)를 사용하는 것이 좋습니다.

즐거운 코딩 되시고, API가 건조하게 유지되길 바랍니다! 🌂

레이트 제한 개요

1. API 게이트웨이

NGINX, Kong, Cloudflare 또는 AWS API Gateway와 같은 도구는 일반적으로 트래픽이 애플리케이션에 도달하기 에 제한을 적용합니다.

2. 애플리케이션 레이어

Resilience4j 또는 Bucket4j와 같은 라이브러리를 사용하면 개발자가 서비스 내부에서 직접 요청 흐름을 제어할 수 있습니다.

3. 분산 시스템

Redis는 종종 여러 인스턴스 간에 레이트 제한 카운터를 공유하는 데 사용됩니다.

레이트 제한은 겉보기에는 간단해 보이지만, 시스템이 실제 트래픽을 처리하기 시작하면 그 중요성이 금방 드러납니다.

잘 설계된 레이트 리미터는 다음을 보호합니다:

  • API
  • Infrastructure
  • Databases
  • Users

그리고 때때로, 안정적인 시스템과 장애 사이의 차이는 놀라울 정도로 단순합니다.

때때로 API는 단지 좋은 우산 하나만 있으면 됩니다 ☔

0 조회
Back to Blog

관련 글

더 보기 »

올바른 Razorpay 결제 흐름 (간단히 설명)

많은 결제 통합 문제는 개발자들이 Razorpay 결제 라이프사이클이 실제로 어떻게 작동하는지를 오해하기 때문에 발생합니다. 올바른 흐름을 따르는 것은 보안을 보장합니다.