Multi-thread에서 RequestContextHolder 전달해서 사용하는 방법

#spring#java#backend#multi-thread

2023-02-06 22:02

대표 이미지

멀티 쓰레드 환경에서도 RequestContextHolder를 사용하여 클라이언트의 요청 정보를 쉽게 사용할 수 있는 방법에 대해서 알아봅니다.

개요

RequestContextHolder는 기존 Servlet, Interceptor, Controller 정도에서만 접근 가능한 HttpServletRequest에 대해 Service, Component, DAO 등 전 구간에서 접근하도록 도와주는 유틸성 클래스이다. 파라미터 전달없이 HTTP URL, body 및 header와 session 등의 정보를 바로 확인 할 수 있어 유용하게 쓰인다. 다만 그 범위가 ThreadLocal로 관리되고 있어 추가 설정이 없이는 다른 쓰레드에서 해당 정보를 이용할 수 없다.

ThreadLocal

하나의 쓰레드에서 읽고 쓸 수 있는 지역변수 thread-local을 관리하는 클래스.

An Introduction to ThreadLocal in Java | Baeldung

고성능의 애플리케이션이나 다수의 서비스로부터 데이터를 취합하는 기능을 하는 애플리케이션의 경우 Multi-thread를 통해서 성능을 개선시키고 있지만 이런 경우에 RequestContextHolder를 바로 적용할 수 없는 것이다. 본 글에서는 RequestContextHolder 원리를 파악하여 다른 쓰레드에서도 HttpServletRequest 정보를 사용할 수 있는 방법을 소개한다.

RequestContextHolder

RequestContextHolder는 보통 아래와 같이 Wrapping하여 HttpServletRequest를 추출하여 사용한다.

HttpServletRequest httpServletRequest = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();

이를 통해 RequestUrl, Cookie, Header 등의 요청 정보에 대해 접근할 수 있게 된다. httpServletRequest.png

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를 어디서든 꺼내 쓸 수 있다. default_servlet.png

문제 상황

그렇다면 새로 생성한 쓰레드에서 별다른 설정없이 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일 때 다른 쓰레드로 전달 가능한 inheritableRequestAttributeHolderThreadLocal에 값을 넣어주고 있다. 따라서 별도 설정을 통해 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이 발생한다. nullpointerror.png 즉, 위에 언급한 inheritableRequestAttributeHolderrequestAttribute가 저장되지 않았기 때문에, 새로운 Thread에서 RequestContextHolder.getRequestAttribute() 호출시에 Null을 반환하고 있는 것이다. requestAttibutes_null.png

해결 방법

따라서 새로 생성된 쓰레드에서도 RequestContextHolder를 사용하기 위해선 this.threadContextInheriatable을 true로 설정해줘야 하는데, FrameworkServlet을 상속한 DispatcherSerlvet 설정을 통해 해결할 수 있다.

DispatcherServlet

표현 계층 전면에서 HTTP 프로토콜을 통해 들어오는 모든 요청을 중앙집중식으로 처리하는 Front Controller.

15.2 The DispatcherServlet

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로서, 서블릿 요청이 올때마다 초기화되어 최초 사용자는 상대적으로 응답 시간이 길어질 수 있다. 반면 양수로 설정하게 되면, 어플리케이션 로드시 서블릿을 초기화하여 성능상 유리할 수 있다.

Servlet - Load on startup - GeeksforGeeks

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 인스턴스들을 사용할 수 있다는 장점이 있다.

How to Register a Servlet in Java | Baeldung

결과적으로 두 방법 모두 새로 생성된 쓰레드에서도 성공적으로 RequestcontextHolder를 통해 HttpServletRequest 정보를 가져오는 것을 확인할 수 있었다. pool-1-thread-1.png

결론

ThreadLocal을 통해 요청 정보를 관리하는 RequestContextHolder에 대해 초기화되는 시점과 동작 원리를 알아보고, 새로 생성된 쓰레드에서도 사용할 수 있는 방법을 알아보았다. 이를 통해 멀티 쓰레드의 모든 레이어에서도 클라이언트의 요청 정보를 사용할 수 있게 되었고, 특히 고성능 애플리케이션을 개발할 때 좀 더 유연한 구조로 설계하는 데에 도움이 될 수 있을 것으로 기대한다.