No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
Spring MVC 에서는 웹 요청이 들어오면 각 요청마다 고유한 "요청 컨텍스트"를 생성한다.
요청 컨텍스트(Context)란?
하나의 HTTP 요청과 관련된 정보(Request, Response 객체 등)를 담고 있는 객체이다.
요청 컨텍스트와 스레드의 관계
- 요청 컨텍스트를 스레드에 바인딩해서 사용한다.
- 하나의 스레드가 여러 요청을 순차적으로 처리할 수 있다.
- 하나의 요청이 여러 스레드에 걸쳐 처리될 수도 있다.(이 경우 컨텍스트 전파가 필요하다.)
다음 rateLimit 메서드에서 HttpServletResponse를 얻기 위해 RequestContextHolder를 사용하고 있는데, RequestContextHolder는 이 요청 컨텍스트를 ThreadLocal 에 저장해서 같은 스레드 내라면 어디서든 현재 요청 정보를 가져올 수 있게 해준다.
그러나 비동기 처리 시(CompletableFuture.runAsync()를 사용)에는 새로운 스레드에서 작업이 실행되고, 이 새로운 스레드는 원래 웹 요청을 처리하던 스레드와 다르다.
따라서 response.setHeader() 를 호출하려고 할 때 원래 요청의 컨텍스트 정보를 가지고 있지 않아 response 객체를 가져올 수 없어 발생하는 문제였다.
@Around("@annotation(rateLimit)")
public Object rateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
// APIRateLimiter 객체를 사용하여 API 키에 해당하는 버킷에서 토큰을 소비하려고 시도한다.
// 토큰이 충분하면 요청이 성공적으로 처리되고, 그렇지 않으면 예외가 발생한다.
String key = evaluateKey(joinPoint, rateLimit.key());
long remainingTokens = apiRateLimiter.tryConsume(key, rateLimit.limit(),
rateLimit.period());
if (remainingTokens >= 0) {
Object result = joinPoint.proceed();
log.debug("Method execution result: {}", result);
// 현재 실행 컨텍스트에서 HttpServletResponse 얻기
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
if (response != null) {
response.setHeader("X-RateLimit-Remaining", String.valueOf(remainingTokens));
log.debug("Added X-RateLimit-Remaining header: {}", remainingTokens);
} else {
log.warn("Unable to set X-RateLimit-Remaining header: HttpServletResponse is null");
}
return result;
} else {
throw rateLimit.exceptionClass().getDeclaredConstructor(String.class)
.newInstance("Rate limit exceeded for key: " + key);
}
}
기존에 헤더로 남은 횟수를 돌려주었는데, 현재 해당 기능 전용 API 를 만든 상태이기 때문에 코드 삭제를 통해 해결하였다.
만약 기능 유지를 원한다면 컨텍스트 전파를 이용할 수 있을 것 같다.