RequestContextHolder
개요
RequestContextHolder
는 Spring에서 전역으로 Request에 대한 정보를 가져오고자 할 때 사용하는 유틸성 클래스이다.
주로, Controller가 아닌 Business Layer 등에서 Request 객체를 참고하려 할 때 사용한다.
Request Param이라던지.. UserAgent 라던지.. 매번 method의 call param으로 넘기기가 애매할 때 주로 쓰인다.
아래처럼 호출하면 같은 Request Thread
범위에 있는 경우 요청정보를 얻어낼 수 있다.
RequestContextHolder.getRequestAttributes()
다만, 위에처럼 사용하면 Attribute만 얻어올 수 있으므로 아래와 같이 Wrapping해서 사용한다
HttpServletRequest servletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
이렇게 되면 익히 알고 있는 Cookie, Header ... 등의 정보를 얻을 수 있다.
생성
RequestContextHolder
는 언제 생성되는지, 그리고 어디서 호출하던 관계없이 Request 정보를 얻을 수 있는지에 대해 알아보자.
일단 이 클래스가 초기화되는건 Servlet
이 생성될 때 이다.
즉, Http Request가 오는 시점에 생성 및 초기화가 되어지고 Business Layer를 거친 뒤 Servlet 이 destroy될 때 clean되고 있다.
내부 필드로 가지고 있는 놈들을 살펴보자.
private static final boolean jsfPresent = ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");
static으로 선언되었기에 클래스 생성과 동시에 만들어지고, Servlet
이 요청/종료 될 때 마다 일일히 값을 채워넣고, 없애는 작업을 한다.
Http 요청이 오게 되면 FrameworkServlet
클래스의 processRequest
라는 메서드가 호출된다.
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = this.buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
this.initContextHolders(request, localeContext, requestAttributes);
try {
this.doService(request, response);
} catch (IOException | ServletException var16) {
failureCause = var16;
throw var16;
} catch (Throwable var17) {
failureCause = var17;
throw new NestedServletException("Request processing failed", var17);
} finally {
this.resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
this.logResult(request, response, (Throwable)failureCause, asyncManager);
this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
}
}
여기에 눈여겨 봐야 할 부분은 아래 부분이다.
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
...
this.initContextHolders(request, localeContext, requestAttributes);
이전 attribute가 남아있는지 확인하고 새로운 requestAttributes를 만든다.
그리고 ContextHolder를 초기화시킨다. initContextHolders
를 살펴보자.
private void initContextHolders(HttpServletRequest request, @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
if (localeContext != null) {
LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
}
if (requestAttributes != null) {
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
}
}
위에서 attribute를 만들었으므로(이전 request attribute가 남아있는 경우는 null로 return하기 때문에, 무조건 새로운 requestAttribute가 만들어진다고 봐도 된다 예외처리로 보임)
RequestContextHolder
에 값이 생성되게 된다.
이렇게 되면 같은 Request Thread, 즉 Tomcat Thread에서는 어디서든 static한 값을 꺼내 쓸 수 있게 되는 것이다.
기본적인 Tomcat을 예로 들면 nio-8080-exec-1
과 같은 쓰레드가 생성되면서 Servlet을 서빙한다.
@Component
, @Service
, @Repository
등은 Business Layer로 Spring Container에 등록되어 같은 쓰레드에서 동작하므로 잘 동작이 된다.
동작 원리
동작은 매우 간단하다. static한 ThreadLocal에 값을 Write/Read 하는 방식이다. 위에서 같은 쓰레드에서는 값을 꺼내쓸 수 있다고 말한 이유도 이러한 맥락이다.
그러나, 다른 쓰레드(new Thread, 혹은 executor를 사용한 ThreadPool에서의 참조 등) 에서는 RequestContextHolder
의 Request값을 꺼내 쓸 수 없다.
왜냐면 새로운 쓰레드를 생성하는 순간 DispatcherServlet
의 범위에서 벗어나서 새로운 쓰레드가 생성되기 때문이다.
간혹, 아래와 같은 코드를 짠다고 생각해보자.
RequestUtils에는 간단히 static method로 RequestContextHolder
를 가져오는 기능만 한다.
public class RequestUtils {
public static HttpServletRequest getRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
}
이럴 경우, 새로운 쓰레드가 생성되면서 예상했던 동작이 잘 되지 않는다.
Spring Container 밖의 쓰레드가 생성되었기 때문에 참조를 하지 못하는 것이다.
이는 new Thread()
와 같이 쓰레드를 새로 만들던, ThreadPool을 이용해서 만들던 동일한 결과가 나타난다.
만약 CompletableFuture
라던지, 새로운 쓰레드를 생성해서 병렬성을 높이고자 할 때 RequestContextHolder
를 사용해서 Request의 쿠키라던지 값을 참조하고자 할 땐 어떻게 할까?
구글링을 맹신하지 말자
이러한 주제로 필자도 CompletableFuture
안에서 RequestContextHolder
를 사용하고자 했으나, 의도대로 동작하지 않아서 구글링을 시작했다.
CompletableFuture
안에서 참조하고자 하면 아래와 같은 Exception이 발생한다
java.lang.IllegalStateException: 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.
use RequestContextListener or RequestContextFilter to expose the current request.
라고 되어 있어서 얼핏보면 이 클래스들을 사용하면 뭔가 될 것 같이 보인다.
RequestContextListener
request를 expose하려면 이 리스너를 사용하라고 한다
그래서 나온 구글링 결과가 아래처럼 사용을 하면 된다고 한다.
public class InheritableRequestContextListener extends RequestContextListener {
private static final String REQUEST_ATTRIBUTES_ATTRIBUTE =
InheritableRequestContextListener.class.getName() + ".REQUEST_ATTRIBUTES";
@Override
public void requestInitialized(ServletRequestEvent requestEvent) {
if (!(requestEvent.getServletRequest() instanceof HttpServletRequest)) {
throw new IllegalArgumentException("Request is not an HttpServletRequest: " + requestEvent.getServletRequest());
} else {
HttpServletRequest request = (HttpServletRequest) requestEvent.getServletRequest();
ServletRequestAttributes attributes = new ServletRequestAttributes(request);
request.setAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE, attributes);
LocaleContextHolder.setLocale(request.getLocale());
RequestContextHolder.setRequestAttributes(attributes, true);
}
}
}
차이점을 보면, RequestContext Event가 들어왔을 때, 해당 이벤트를 잡아서 Wrapping 한 뒤
RequestContextHolder.setRequestAttributes(attributes, true);
요 방법을 사용해보라는 것이다.
뒤에 2번째오는 true의 인자는 boolean inheritable
의 값인데, ThreadLocal을 Inheritable 하게 만들것인가에 대한 true/false이다.
요것도 될법한데 결국엔 동작하지 않았다. (이유는 아래에서 설명)
RequestContextFilter
이번엔 RequestContextFilter
를 생성한 뒤, DispatcherServlet
이 생성될 때 필터를 끼워넣으라는 방식이다.
public void setThreadContextInheritable(boolean threadContextInheritable) {
this.threadContextInheritable = threadContextInheritable;
}
위에서의 RequestContextListener 처럼 inheriable값이 있어서 이것 또한 동작해볼법 했다.
하지만 이것 또한 실패했다.
왜?
단순히 구글링한 결과에 대해 의문을 가질 게 아니라, 프레임워크 내부 동작을 파악하다보니 원인을 알 수 있었다.
아까 FrameworkServlet 의 로직을 보다보면 실마리가 있다.
위에 두 RequestContextListener
와 RequestContextFilter
는 DispatcherServlet의 processRequest
메서드가 호출되기 이전에 먼저 불린다.
그래서 필터와 리스너에 inheritable값을 전달을 해줘도, DispatcherServlet의 init 하는 부분에서 덮어씌워지게 되는 것이다.
this.initContextHolders(request, localeContext, requestAttributes);
결국 이 method가 호출되면서 Servlet
이 초기화 된다.
private void initContextHolders(HttpServletRequest request, @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
if (localeContext != null) {
LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
}
if (requestAttributes != null) {
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
}
}
값을 세팅하는 부분에 보면 this.threadContextInheritable
을 참조해서 세팅하고 있다.
기본값을 보니 false
로 설정되어 있다.
이 값을 true로 바꾸면 Servlet Thread 밖에서도 호출할 때 잘 동작하지 않을까?
threadContextInheritable
값을 바꾸려면 DispatcherServlet이 생성될 때 변경해줘야 한다.
public class SpringStudyApplication implements WebApplicationInitializer {
public static void main(String[] args) {
SpringApplication.run(SpringStudyApplication.class, args);
}
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext);
dispatcherServlet.setThreadContextInheritable(true);
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcherServlet", dispatcherServlet);
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
}
Spring Boot의 경우는 위와 ServletContextInitializer
인터페이스를 구현해서 같이 초기화 할 수 있다.
WebApplicationInitializer
를 써도 동일한 듯 하지만, Spring Boot에서는 DispatcherServlet을 설정하고자 할 때 ServletContextInitializer
를 사용한다고 한다.
(https://github.com/spring-projects/spring-boot/issues/522)
DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext);
dispatcherServlet.setThreadContextInheritable(true);
대망의 실마리를 잡을 마지막 방법이다.
DispatcherServlet이 생성될 때 setThreadContextInheritable
을 true로 주는 방법이다.
이렇게 되면 매번 Request가 생성될 때 마다 RequestContextHolder
의 setAttribute를 호출할 때 마다 inheritable 값을 true
로 전달해줄 것이다.
간단하게 Controller 하나 만들어서 테스트를 해보면....
@RestController
@Slf4j
public class BaseController {
private static final Executor CACHED_THREAD_POOL = Executors.newCachedThreadPool();
@GetMapping("/")
public ResponseEntity index() throws ExecutionException, InterruptedException {
log.info("in controller {} {}", RequestUtils.getRequest().getRequestURL(), Thread.currentThread());
CompletableFuture<HttpServletRequest> stringCompletableFuture = CompletableFuture.supplyAsync(() -> {
log.info("in completable future {}", RequestUtils.getRequest().getRequestURL());
return RequestUtils.getRequest();
}, CACHED_THREAD_POOL);
HttpServletRequest req2 = stringCompletableFuture.get();
log.info("{}", req2);
return ResponseEntity.ok(req2.getRequestURL());
}
}
로깅만 남기고, request url이 CompletableFuture 안에서 잘 가져왔는지 확인해보자
Controller에서 가져온 RequestContextHolder
의 값과, CompletableFuture안에서 가져온 값이 동일하게 출력되는걸 알 수 있다. (Thread가 다름에도 불구하고)
이는 ThreadLocal을 생성할 때 Inheritable값을 true로 주게 되면, 부모에 set된 값을 child thread에서도 같이 복사해서 가져오는 로직이기 때문이다.
아무 설정이 없다면, RequestContextHolder
의 Request객체는 Servlet Thread 안에서만 사용이 가능하다.
결론
Spring Servlet의 동작 및 ThreadLocal의 Inheritable한지 여부 등에 대해서 확실히 공부할 수 있게 되었다.
p.s 구글링에 나오는 해답이 무조건적인 해답이 아니다. 코드가 의도대로 수행이 안되면 그 코드를 수행하는 상위단계(위 문제에서는 DispatcherServlet
) 의 로직이 어떤지 확인해보도록 하자.
'Development > Spring' 카테고리의 다른 글
Spring Redis Template Transaction (2) | 2021.09.09 |
---|---|
Spring Password Encoder (4) | 2019.04.06 |
Dispatcher Servlet (0) | 2019.04.06 |
Spring Boot Prometheus Converter 406 Not Acceptable (0) | 2019.04.06 |
[JAVA/Spring] BeanUtils 관련 (2) | 2018.07.20 |