예전에 새벽1시에 슬랙에 뭐하는지 모르겠는데 어떤 유저가 추천 단건조회에 계속 요청을 보냈었다 그래서 무서워서 급하게 알아보고 RateLimit을 걸었다

어떻게 1초동안 10번을 보낸건진 모르겠다...
그리고 중복 요청 이슈가 발생하여 알림톡이 두번 나갔는데 이것도 유저 입장에선 우리 서비스가 너무 허술해 보이기도 하고 알림톡 자체가 비싸진 않지만 유료인 점을 감안해서 중복 요청 방지도 구현했었다
이미 코드는 짜져있고 이것을 커스텀 어노테이션으로 좀 이쁘게 바꿔보자
public class WebConfig implements WebMvcConfigurer {
@Value("${cors.allowed.origins:}")
private String[] ALLOWED_CORS_URLS;
private final TokenInterceptor tokenInterceptor;
private final AdminTokenInterceptor adminTokenInterceptor;
private final ApiThrottlingInterceptor apiThrottlingInterceptor;
private final UserIdentifierArgumentResolver userIdentifierArgumentResolver;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.order(1);
registry.addInterceptor(adminTokenInterceptor)
.order(2);
registry.addInterceptor(apiThrottlingInterceptor)
.order(3);
}
이런식으로 일단 해당 인터셉터를 타게 했다
우리가 볼 내용은 ApiThrottlingInterceptor이다
일단 어노테이션 두개 커스텀 해서 만들어준다
@HandleDuplicateRequest: 중복 요청을 방지하겠다는 뜻
@ApiThrottled: rate limit를 걸었다는 뜻
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HandleDuplicateRequest {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ApiThrottled {
}
인터셉터 내용이다
@Component
@RequiredArgsConstructor
public class ApiThrottlingInterceptor implements HandlerInterceptor {
private final RateLimitTracker rateLimitTracker;
private final DuplicateRequestTracker duplicateRequestTracker;
@Override
public boolean preHandle(final HttpServletRequest request,
final HttpServletResponse response,
final Object handler) {
if (!(handler instanceof final HandlerMethod handlerMethod)) {
return true;
}
boolean isDuplicateRequest = handlerMethod.hasMethodAnnotation(HandleDuplicateRequest.class);
boolean isApiThrottled = handlerMethod.hasMethodAnnotation(ApiThrottled.class);
String clientIp = ClientIpUtil.getClientIp(request);
if (isDuplicateRequest) {
duplicateRequestTracker.resolveBucket(clientIp);
}
if (isApiThrottled) {
rateLimitTracker.resolveBucket(clientIp);
}
return true;
}
}
일단
if (!(handler instanceof final HandlerMethod handlerMethod)) {
return true;
}
이 코드는 먼저 handler가 HandlerMethod의 인스턴스인지 확인한다 쉽게 말해서 handler가 컨트롤러인지 확인, mvc에서 정적 파일 요청도 가능하다 컨트롤러가 아니면 뒤의 로직 스킵한다
다음 부분은
boolean isDuplicateRequest = handlerMethod.hasMethodAnnotation(HandleDuplicateRequest.class);
boolean isApiThrottled = handlerMethod.hasMethodAnnotation(ApiThrottled.class);
hasMethodAnnotation은 HandlerMethod 클래스의 메서드로, 핸들러 메서드에 지정된 어노테이션이 있는지 확인 한다
handlerMethod.hasMethodAnnotation(HandleDuplicateRequest.class)는 핸들러 메서드에 @HandleDuplicateRequest 어노테이션이 있는지 확인한다
handlerMethod.hasMethodAnnotation(ApiThrottled.class)는 핸들러 메서드에 @ApiThrottled 어노테이션이 있는지 확인한다
이제 그 다음 로직 중 rateLimitTracker는 Bucket4J를 사용하여 Rate Limit를 하는건데 저번에 기록 했으니 따로 기록하진 말자
@ApiThrottled
@HandleDuplicateRequest
@Operation(description = "나에게 추천된 유저 프로필 조회")
@GetMapping("/v3/recommendations/profile")
public ResponseEntity<GetRecommendedUserProfileInfoResponse> getRecommendedUserProfile(생략) {
return ResponseEntity.ok().body(recommendationService.getRecommendedUserProfile(생략));
}
이제 이런식으로 api 컨트롤러 메서드에 rate limit이나 중복 요청 방지를 쏙쏙 골라서 할 수 있다
처음엔 이걸 AOP로 구현할까 생각해서 구현해봤는데 막상 구현해 보니 인터셉터를 사용하여 레이트 리미팅을 구현하는 것이 더 나아 보였다
스프링 인터셉터는 요청이 컨트롤러에 도달하기 전에 처리되기 때문에, 이를 사용하여 레이트 리미팅을 구현하면 코드가 더 명확하고 재사용성이 높아진다고 생각해서 인터셉터와 커스텀 어노테이션 선에서 하는 것으로 했다