Spring RequestContextHolder
Development/Spring

Spring RequestContextHolder

반응형

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 의 로직을 보다보면 실마리가 있다.

 

위에 두 RequestContextListenerRequestContextFilter 는 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