Multi-thread에서 RequestContextHolder 전달해서 사용하는 방법
2023-02-06 22:02
멀티 쓰레드 환경에서도 RequestContextHolder를 사용하여 클라이언트의 요청 정보를 쉽게 사용할 수 있는 방법에 대해서 알아봅니다.
개요
RequestContextHolder는 기존 Servlet, Interceptor, Controller 정도에서만 접근 가능한 HttpServletRequest에 대해 Service, Component, DAO 등 전 구간에서 접근하도록 도와주는 유틸성 클래스이다. 파라미터 전달없이 HTTP URL, body 및 header와 session 등의 정보를 바로 확인 할 수 있어 유용하게 쓰인다. 다만 그 범위가 ThreadLocal로 관리되고 있어 추가 설정이 없이는 다른 쓰레드에서 해당 정보를 이용할 수 없다.
ThreadLocal
하나의 쓰레드에서 읽고 쓸 수 있는 지역변수
thread-local
을 관리하는 클래스.
고성능의 애플리케이션이나 다수의 서비스로부터 데이터를 취합하는 기능을 하는 애플리케이션의 경우 Multi-thread를 통해서 성능을 개선시키고 있지만 이런 경우에 RequestContextHolder를 바로 적용할 수 없는 것이다. 본 글에서는 RequestContextHolder 원리를 파악하여 다른 쓰레드에서도 HttpServletRequest 정보를 사용할 수 있는 방법을 소개한다.
RequestContextHolder
RequestContextHolder는 보통 아래와 같이 Wrapping하여 HttpServletRequest
를 추출하여 사용한다.
HttpServletRequest httpServletRequest = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
이를 통해 RequestUrl, Cookie, Header 등의 요청 정보에 대해 접근할 수 있게 된다.
RequestContextHolder 생성
RequestContextHolder는 HTTP 요청이 인입될 때 생성 및 초기화가 진행되고, 로직을 수행한 뒤 Servlet이 destroy될 때 clean된다. 내부 필드값들을 보면 static으로 선언되어 있어 클래스 생성과 동시에 생성되며, Servlet의 생성/종료될 때마다 해당 값들을 채워주고 초기화하는 작업을 한다.
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");
HttpServletRequest
을 처리하는 FrameworkServlet 클래스의 processRequest
메서드를 보면,RequestContextHolder
도 함께 초기화되어 설정되는 것을 확인해 볼 수 있다.
/**
* Process this request, publishing an event regardless of the outcome.
* <p>The actual event handling is performed by the abstract
* {@link #doService} template method.
*/
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
initContextHolders(request, localeContext, requestAttributes);
...
}
15~16번째 라인을 보면, RequestContextHolder
에 이전 requestAttribute
를 가져와 새로운 requestAttribute
를 생성하고, 21번째 줄에서 해당 값을 통해 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);
}
}
이를 통해 Request Servlet을 처리하는 기본 Tomcat Thread에서는, 같은 ThreadLocal에서 관리되고 있는 RequestAttributes
를 어디서든 꺼내 쓸 수 있다.
문제 상황
그렇다면 새로 생성한 쓰레드에서 별다른 설정없이 RequestContextHolder를 바로 사용하면 어떻게 될까?
initContextHolders
메서드를 다시 보면, this.threadContextInheriatable
도 전달하는 것을 볼 수 있다.
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
ServletFramework에 정의되어 있는 해당 값에 대한 설명을 보면, 자식 쓰레드에게 attribute값들을 전달할 지 여부를 결정한다고 되어 있고 기본값으로는 false로 설정되어 있다.
/** Expose LocaleContext and RequestAttributes as inheritable for child threads?. */
private boolean threadContextInheritable = false;
RequestContextHolder에서는 해당값을 어떻게 처리하는지 살펴보자.
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
} else if (inheritable) {
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();
} else {
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
inheritable
값에 따라 서로 다른 ThreadLocal로 값을 세팅해주고 있으며, true일 때 다른 쓰레드로 전달 가능한 inheritableRequestAttributeHolder
ThreadLocal에 값을 넣어주고 있다.
따라서 별도 설정을 통해 this.threadContextInheriatable
을 true로 변경해주지 않으면, 다른 쓰레드(new Thread
또는 Executors
를 통해 생성된 ThreadPool의 쓰레드)에서 사용시 정상적으로 동작을 하지 않을 것이다.
테스트를 위해 아래와 같이 간단한 Controller를 생성하여 확인해 보자.
@RestController
@Slf4j
public class ThreadTestController {
private static final Executor THREAD_POOL = Executors.newFixedThreadPool(3);
@GetMapping("")
public ResponseEntity test() throws ExecutionException, InterruptedException {
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
log.info("Servlet Thread: {} {}", httpServletRequest.getRequestURL(), Thread.currentThread());
CompletableFuture<HttpServletRequest> future = CompletableFuture.supplyAsync(() -> {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
log.info("CompletableFuture Thread: {} {}", ((ServletRequestAttributes) requestAttributes).getRequest().getRequestURL(), Thread.currentThread());
return ((ServletRequestAttributes) requestAttributes).getRequest();
}, THREAD_POOL);
HttpServletRequest futureHttpServletRequest = future.get();
log.info("futureHttpServletRequest: {}", futureHttpServletRequest.getRequestURL());
return ResponseEntity.ok(futureHttpServletRequest.getRequestURL());
}
}
Tomcat의 경우를 예시로 들면 Request Servlet을 처리하는 nio-8080-exec-2
과 같은 기본 쓰레드에서는 정상적으로 RequestContextHolder를 통해 HttpServletRequest
를 가져와 HttpUrl
을 출력하지만, CompletableFuture를 통해 새로 생성된 Thread에서는 NullPointerException
이 발생한다.
즉, 위에 언급한
inheritableRequestAttributeHolder
에 requestAttribute
가 저장되지 않았기 때문에, 새로운 Thread에서 RequestContextHolder.getRequestAttribute()
호출시에 Null
을 반환하고 있는 것이다.
해결 방법
따라서 새로 생성된 쓰레드에서도 RequestContextHolder를 사용하기 위해선 this.threadContextInheriatable
을 true로 설정해줘야 하는데, FrameworkServlet을 상속한 DispatcherSerlvet 설정을 통해 해결할 수 있다.
DispatcherServlet
표현 계층 전면에서 HTTP 프로토콜을 통해 들어오는 모든 요청을 중앙집중식으로 처리하는 Front Controller.
Springboot에서 DispatcherServlet 설정을 커스터마이징하는 데에는 대표적으로 Bean으로 설정하는 방법 또는 ServletContextInitializer를 통한 프로그래밍 방법으로 두 가지가 있다.
1) ServletRegistrartionBean 설정
서블릿을 관리하는 ServletRegistrationBean
을 참조하여 등록된 서블릿에 대한 threadContextInheritable
옵션을 true
로 설정하여 추가해준다.
@Configuration
public class WebConfig {
@Bean
@Primary
public ServletRegistrationBean servletRegistration(ServletRegistrationBean registration) {
registration.setLoadOnStartup(1);
registration.addInitParameter("threadContextInheritable", "true");
return registration;
}
}
load-on-startup
서블릿이 로드되어야 하는 순서값. 기본값은 -1로서, 서블릿 요청이 올때마다 초기화되어 최초 사용자는 상대적으로 응답 시간이 길어질 수 있다. 반면 양수로 설정하게 되면, 어플리케이션 로드시 서블릿을 초기화하여 성능상 유리할 수 있다.
2) ServletContextInitializer 구현
ServletContextInitializer를 상속하여 어플리케이션 구동시 동작(onStartup
)에 ThreadContextInheritable
옵션을 true
로 설정한 DispatcherServlet
을 생성한 뒤 등록해준다.
@SpringBootApplication
public class RequestContextHolderStudyApplication implements ServletContextInitializer {
public static void main(String[] args) {
SpringApplication.run(RequestContextHolderStudyApplication.class, args);
}
@Override
public void onStartup(ServletContext servletContext) {
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext);
dispatcherServlet.setThreadContextInheritable(true);
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcherServlet", dispatcherServlet);
servlet.setLoadOnStartup(1);
servlet.addMapping("/");
}
}
WebApplicationInitializer를 써도 동일하게 구현하여 설정할 수 있지만, DispatcherServlet을 설정하고자 할 땐 ServletContextInitializer를 사용한다고 한다. (https://github.com/spring-projects/spring-boot/issues/522)
비교
Bean을 통한 방법은 여러 개의 서로 다른 종류의 서블릿을 등록하여 관리할 수 있고, ServletContextinitializer에서 처리하는 DispatcherServlet보다 더 간편한 HttpServlet 인스턴스들을 사용할 수 있다는 장점이 있다.
결과적으로 두 방법 모두 새로 생성된 쓰레드에서도 성공적으로 RequestcontextHolder를 통해 HttpServletRequest 정보를 가져오는 것을 확인할 수 있었다.
결론
ThreadLocal을 통해 요청 정보를 관리하는 RequestContextHolder에 대해 초기화되는 시점과 동작 원리를 알아보고, 새로 생성된 쓰레드에서도 사용할 수 있는 방법을 알아보았다. 이를 통해 멀티 쓰레드의 모든 레이어에서도 클라이언트의 요청 정보를 사용할 수 있게 되었고, 특히 고성능 애플리케이션을 개발할 때 좀 더 유연한 구조로 설계하는 데에 도움이 될 수 있을 것으로 기대한다.