개요
업무 중 외부 API를 연동하는 작업을 진행하면서 Rate Limit(초당 요청 제한)을 제어해야하는 상황을 마주했습니다. 작업 흐름은 다음과 같았습니다.
1. 외부 API로 목록 조회 요청을 보낸다.
2. 목록에 포함된 각 요소에 대해 개별 상세 조회 요청을 보낸다.
이러한 경우 목록 조회 결과가 외부 API 서버에서 제한하는 n건을 초과하는 경우, "429, Too Many Requests" 에러가 발생할 가능성이 존재했습니다. 따라서 요청을 버리지 않으면서도, 초당 n건 이하로 요청을 안정적으로 처리하는 제어 방식이 필요했고, 이 과정에서 Token Bucket과 Leaky Bucket 알고리즘을 비교하게 되었습니다.
Token Bucket Algorithm
Token Bucket 알고리즘은 토큰을 기반으로 요청의 진입을 제어하는 방식입니다. 제한된 용량을 가진 버킷에 일정한 속도로 토큰을 생성하고, 요청 1건당 토큰 1개를 소비해 트래픽을 제어합니다. 사용자로부터 요청이 들어오면 버킷에 토큰이 존재하는 경우 요청을 즉시 처리하고, 그렇지 않은 경우 요청을 거절하거나 대기상태로 만듭니다.
TokenBucket의 특징은 평균 요청 속도는 제한하지만, 순간적인 Burst 요청을 허용한다는 것입니다. 예를 들어, RateLimit을 10초에 10건으로 제한하지만, 버킷에 저장할 수 있는 토큰의 갯수가 100개라면 미리 쌓여있는 100개의 토큰을 활용해 짧은 시간에 100개의 요청을 처리할 수 있습니다. 따라서, TokenBucket은 "요청을 받아도 되는가?"를 토큰의 유무로 판단하는 알고리즘이라고 볼 수 있겠습니다.
Leaky Bucket Algorithm
LeakyBucket 알고리즘은 요청을 큐에 저장한 뒤, 일정한 속도로 처리하는 방식입니다. 사용자로부터 유입되는 요청을 버킷에 저장하고 버킷에서는 일정한 속도로 요청을 처리합니다. 예를 들어, 버킷에 요청이 20개 적재되어 있고, 10초에 10건씩 처리되도록 프로그래밍 되어있다면, 처음 10건을 처리하고 10초 뒤 10건을 처리하는 방식으로 일정하게 동작합니다.
LeakyBucket의 특징은 처리 속도가 항상 일정하다는 것입니다. Burst 요청을 허용하지 않기 때문에 일정 시간에 약속된 만큼의 요청만을 처리합니다. 이러한 특징으로 요청을 안정적으로 처리한다는 장점이 있지만, TokenBucket과 달리 요청이 몰릴 경우 Burst 처리가 되지 않기 때문에 대기 시간이 증가할 수 있다는 단점이 있습니다. 따라서, LeakyBucket은 "어떤 속도로 처리할 것인가?"를 중심으로 동작하는 알고리즘이라고 볼 수 있겠습니다.
Token Bucket과 Leaky Bucket의 차이점
두 알고리즘의 가장 큰 차이는 스케줄러가 무엇을 관리하느냐에 있다고 생각합니다. 정리하면 다음과 같습니다.

- Token Bucket: 스케줄러가 토큰을 공급하고, 요청이 이를 소비한다.
- Leaky Bucket: 요청이 큐에 쌓이고, 스케줄러가 이를 일정 속도로 소비한다.
Leaky Bucket을 선택한 이유
저는 이번 작업에 있어서 LeakyBucket 알고리즘이 더 적합하다고 판단했습니다.
1) 외부 Rate Limit에 정확히 대응
외부 API 서버가 초당 n건으로 제한되어 있기 때문에, Burst를 허용하는 것 자체가 큰 의미가 없었습니다.
오히려 항상 초당 n건 이하로 일정하게 요청을 흘려보내는 것이 중요했습니다.
2) Burst보다 요청 처리의 안정성이 중요
TokenBucket은 순간적인 Burst를 처리할 수 있지만, 외부 서버가 이를 허용하지 않는다면 오히려 위험 요소가 된다고 판단했습니다. 그러나, LeakyBucket은 처리 속도를 강제로 평탄화하기 때문에 외부 서버에 부담을 주지 않아 안정적인 트래픽 처리가 가능할 것이라고 판단했습니다.
3) 구현 난이도가 비교적 단순
- 요청을 큐에 적재
- 스케줄러가 일정 주기마다 최대 n건 처리
결론
해당 케이스에서는 Burst를 허용하기보다는, 외부 API Rate Limit을 정확히 지키며 안정적으로 요청을 처리하는 것이 더 중요했기 때문에 LeakyBucket 알고리즘을 선택하게 되었습니다. 아래 코드는 단일 서버에서 LeakyBucket을 간단히 구현한 예제입니다. 외부 API의 Rate Limit이 초당 10건으로 고정되어 있다는 것이 전제입니다.
import java.util.Queue;
import java.util.concurrent.*;
public class LeakyBucketScheduler {
private static final int RATE_LIMIT = 10; // 초당 최대 처리 건수
private final Queue<Runnable> requestQueue = new ConcurrentLinkedQueue<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
public LeakyBucketScheduler() {
// 1초마다 최대 RATE_LIMIT 만큼 처리
scheduler.scheduleAtFixedRate(this::drainQueue, 0, 1, TimeUnit.SECONDS);
}
// 외부 API 개별 요청을 큐에 적재
public void submit(Runnable request) {
requestQueue.offer(request);
}
private void drainQueue() {
for (int i = 0; i < RATE_LIMIT; i++) {
Runnable task = requestQueue.poll();
if (task == null) {
break;
}
try {
task.run();
} catch (Exception e) {
// 예외 로깅 및 재시도 정책 처리 가능
e.printStackTrace();
}
}
}
}
// 사용 예시
LeakyBucketScheduler scheduler = new LeakyBucketScheduler();
for (String itemId : itemIds) {
scheduler.submit(() -> callExternalApi(itemId));
}