Spring Boot에서 API Rate Limiting 구현하기
Spring Boot 애플리케이션에서 트래픽 제어와 안정성 확보를 위해 API Rate Limiting 구현 원리와 Bucket4j 기반 설정
목차
개요
서비스 안정성을 위해 API 요청량을 제한하는 것은 기본적인 설계 요소다. 이 글에서는 Spring Boot 환경에서 API Rate Limiting 개념을 설명하고, 실제로 적용 가능한 예제 코드를 통해 동작 원리와 설정 방법을 제시한다. 초보자도 이해하기 쉽게 흐름을 따라가도록 구성했다.
Rate Limiting 개념과 접근법
왜 필요한가
과도한 요청은 서비스 응답 지연과 장애를 야기한다. Rate Limiting은 클라이언트별, 엔드포인트별 또는 전체 시스템 단위로 요청을 제어하여 안정성을 확보하는 기법이다.
주요 전략
- 고정 윈도우(Fixed Window): 일정 기간마다 카운트를 초기화하는 방식
- 슬라이딩 윈도우(Sliding Window): 시간 범위를 이동시키며 더 부드러운 제한 제공
- 토큰 버킷(Token Bucket): 토큰을 소비하는 방식으로 버스트 허용
- 리키 버킷(Leaky Bucket): 일정한 처리율을 유지하는 방식
실전에서는 토큰 버킷 기반 라이브러리인 Bucket4j를 자주 사용한다. 유연성과 다양한 저장소 연동 지원으로 널리 채택되어 있다.
Bucket4j 소개
Bucket4j는 토큰 버킷 알고리즘을 구현한 라이브러리다. 메모리 기반으로 간단히 사용하거나 Redis와 연동해 분산 환경에서도 동작하도록 구성할 수 있다. Spring Boot와의 결합으로 api rate limiting spring boot 구현이 간단해진다.
구현 방식 선택
- 단일 인스턴스: 애플리케이션 인스턴스가 하나이거나 상태 공유가 불필요한 경우 인메모리 버킷 사용
- 분산 환경: 여러 인스턴스가 존재하는 경우 Redis 등의 외부 저장소와 연동
의존성 추가
Maven 기준으로 Bucket4j와 Redis(분산 환경 사용 시) 의존성을 추가한다.
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.3.0</version>
</dependency>
<!-- Redis 연동 시 추가 -->
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-redis-jedis</artifactId>
<version>8.3.0</version>
</dependency>
간단한 Spring Boot 예제 (인메모리)
아래 코드는 클라이언트 IP 기준으로 token bucket을 적용하는 필터 예제다. spring boot rate limiter 예제 용도로 충분히 간단하다.
package com.example.ratelimit;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill;
import org.springframework.stereotype.Component;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class RateLimitFilter extends HttpFilter {
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
private Bucket createNewBucket() {
Bandwidth limit = Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(1)));
return Bucket4j.builder().addLimit(limit).build();
}
@Override
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
String ip = req.getRemoteAddr();
Bucket bucket = buckets.computeIfAbsent(ip, k -> createNewBucket());
if (bucket.tryConsume(1)) {
chain.doFilter(req, res);
} else {
res.setStatus(429);
res.getWriter().write("Too Many Requests");
}
}
}
분산 환경에서 Redis 사용 예
여러 인스턴스가 존재하는 환경에서는 Redis 기반 GridBucket을 사용한다. Redis 클라이언트를 통해 GridBucketState를 유지하면 중앙에서 요청량을 조절할 수 있다.
package com.example.ratelimit.redis;
import io.github.bucket4j.grid.jedis.JedisProxyManager;
import io.github.bucket4j.grid.GridBucketState;
import io.github.bucket4j.grid.ProxyManager;
import io.github.bucket4j.BucketConfiguration;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Refill;
import redis.clients.jedis.JedisPool;
// JedisPool 빈을 주입받아 ProxyManager<GridBucketState> 생성 후 사용
JedisPool jedisPool = new JedisPool("localhost", 6379);
ProxyManager<GridBucketState> proxy = new JedisProxyManager(jedisPool);
Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)));
BucketConfiguration config = BucketConfiguration.builder().addLimit(limit).build();
// proxy을 통해 키별 버킷을 가져와 사용 가능
테스트와 모니터링
- 단위 테스트: 다양한 요청량으로 응답 코드(200, 429) 시나리오 검증
- 통합 테스트: 분산 환경에서 Redis 연결과 동작 확인
- 모니터링: 제한 위반 빈도와 차단된 요청 수를 로그 또는 메트릭으로 수집
메트릭은 Prometheus나 Micrometer를 통해 수집하면 추후 정책 조정에 도움된다.
정책 설계 시 고려사항
- 엔드포인트별 요구사항: 인증이 필요한 API와 공개 API의 한도는 다를 수 있다
- 버스트 허용 범위: 토큰 버킷은 순간적인 버스트를 허용하므로 적정 값을 결정해야 한다
- 키 선정: 사용자 토큰, IP, API 키 등 어떤 키로 제한할지 명확히 정의
- 토큰 재충전 정책: 초당 재충전량과 최대 용량을 조합하여 설계
맺음말
API Rate Limiting은 서비스 안정성과 사용자 경험을 동시에 지키는 중요한 수단이다. bucket4j spring boot 설정 사례를 통해 간단한 인메모리부터 Redis 기반 분산 환경까지 적용 방법을 살펴봤다. 요구사항에 맞는 전략을 선택하면 과도한 트래픽으로 인한 문제를 효과적으로 완화할 수 있다.