고민한 부분
서비스를 배포하게 되면 한 사용자가 악의적으로 트래픽을 많이 보내게 될 경우 서버가 다운될 수도 있고 특정 기능은 외부 api를 사용하는 기능도 있어서 금전적으로 손해를 볼 수도 있겠다고 생각했습니다.
그래서 특정 위치에서 발생하는 과도한 트래픽을 제한해야겠다고 생각했습니다.
구현 방법
Bucket4j
토큰 버킷(Token Bucket) 알고리즘을 사용하여 API 요청률을 제한하는 데 사용됩니다.
동작원리
각 사용자 또는 API 키에 대한 버킷을 생성한다. 버킷으로 토큰의 개수를 제한할 수 있습니다.
일정한 속도로 버킷에 토큰이 추가됩니다.
API가 호출이 될 때 마다 버킷에 있는 토큰을 사용하게 되고 토큰이 부족하면 요청을 거부하거나 지연시킵니다.
구현
HTTP 요청에 대한 Rate Limiting을 구현했습니다.
- HTTP 요청 수신: 클라이언트가 서버로 HTTP 요청을 보냅니다.
- 인터셉터 작동: HttpInterceptor가 요청을 가로챕니다.
- 버킷 조회: RateLimitService에서 클라이언트별로 버킷을 조회하거나 생성합니다.
- 토큰 소비: 요청을 처리하기 전에 버킷에서 토큰을 소비합니다.
토큰이 남아 있으면 요청을 처리합니다.
토큰이 부족하면 요청을 거부하고, HTTP 429 상태 코드(Too Many Requests)를 반환합니다. - 요청 처리: 요청이 허용되면 컨트롤러 메서드가 실행되고, 응답이 반환됩니다.
의존성 추가
implementation 'com.giffing.bucket4j.spring.boot.starter:bucket4j-spring-boot-starter:0.2.0'
사용자별 버킷을 생성하고 제한하는 서비스 로직을 작성합니다.
@Service
public class RateLimitService {
private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
private String getHost(HttpServletRequest httpServletRequest){
return httpServletRequest.getHeader("Host");
}
//접속 제한하기
public Bucket resolveBucket(HttpServletRequest httpServletRequest) {
return cache.computeIfAbsent(getHost(httpServletRequest), this::newBucket);
}
private Bucket newBucket(String apiKey) {
return Bucket4j.builder()
// 10개의 클라이언트가 10초에 1000개씩 보낼 수 있는 대역폭
//.addLimit(Bandwidth.classic(1000, Refill.intervally(10, Duration.ofSeconds(10))))
//10개의 클라이언트가 10초에 1000개씩 보낼 수 있는 대역폭
.addLimit(Bandwidth.classic(10, Refill.intervally(10, Duration.ofSeconds(10))))
.build();
}
}
HttpInterceptor 클래스는 Spring MVC 인터셉터로, HTTP 요청을 가로채고 Rate Limiting을 적용합니다.
@Component
@Log4j2
public class HttpInterceptor extends HandlerInterceptorAdapter {
RateLimitService rateLimitService = new RateLimitService();
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
Bucket bucket = rateLimitService.resolveBucket(request);
log.info("================ Before Method");
log.info("접속 ip 주소 '{}'", request.getRemoteAddr());
log.info(request.getRemoteAddr());
if (bucket.tryConsume(1)) { // 1개 사용 요청
// 초과하지 않음
log.info("초과 안함");
return true;
} else {
// 제한 초과
log.info("{} 트래픽 초과!!!", request.getRemoteAddr());
return false;
}
}
@Override
public void postHandle( HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) {
log.info("================ Method Executed");
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
log.info("================ Method Completed");
}
}
WebConfig 클래스는 Spring의 WebMvcConfigurer를 구현하여 인터셉터를 설정합니다.
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
HttpInterceptor httpInterceptor = new HttpInterceptor();
registry.addInterceptor(httpInterceptor);
}
}
테스트
연속으로 api 요청을 했을때 트래픽이 제한되는 것을 확인할 수 있습니다.
'Dev' 카테고리의 다른 글
일단 단위 테스트부터 테스트 코드 작성해보기 (0) | 2024.12.04 |
---|---|
상품 주문하기 동시성 문제 해결하기 (0) | 2024.12.04 |
GCP에 ElasticSearch 띄워서 검색 기능 구현하기 (2) | 2024.12.04 |
위치 기반으로 글 조회 기능 구현 (1) | 2024.12.04 |
fcm 이용해서 앱 푸쉬 구현 (0) | 2024.12.04 |