티스토리 뷰

들어가기에 앞서..


회사 프로젝트에서 API Request Header 에 포함되어 들어온 사용자정보를 Interceptor 에서 검증하고, 이를 UserContext 에 담아 전역적으로 사용하기 위한 구성을 했고, 이를 구현하는데 ThreadLocal<UserContext> 를 사용했다.

 

Request Per Thread 방식으로 동작하는 Tomcat 기반의 서버였기 때문에 예상한대로 동작했다.

 

WebFlux 에서 기본적으로 사용되는 Event-Loop 방식의 Netty 에서는 어떤 문제가 발생하고, 이를 어떤 방식으로 해결해 나갈수 있는지 고민한 과정을 포스팅에 담아보려고 한다.

 

ThreadLocal 이란?


ThreadLocal 은 쉽게 말해 Thread 별로 가지는 변수다. 내부적으로 Thread ID 를 Key 로하는 Map<K, V> 에 데이터를 저장하기 때문에 동일한 Thread 라면 어디서든 해당 값에 접근할 수 있다.

 

이러한 ThreadLocal 은 실제로 Spring Security 의 SecurityContext, Spring Web MVC 의 RequestContext, Logback 의 MDC 등에서 사용되고 있는 방식이다.

 

WebFlux 에서 사용해보자


Request Per Thread 에서는 요청이 들어오고 응답이 나갈때까지 동일한 Thread 가 요청을 처리하기 때문에 ThreadLocal 을 사용해도 문제가 없다.

 

하지만, Non-Blocking Event-Loop 방식으로 동작하는 WebFlux(Netty) 에서는 하나의 요청을 처리되는 Thread 가 계속 변경될 수 있기 때문에 ThreadLocal 에 저장된 값이 없거나 뒤죽박죽 변경될 수 있다. 따라서 이러한 처리되는 Thread 가 변경되는 환경에서는 ThreadLocal 사용에 반드시 주의를 기울여야 한다.

 

예를 들어, ThreadLocal 에 데이터를 넣는 시점에는 Thread-A 였지만, 요청이 처리되는 중간에 Thread-B 로 변경되고 ThreadLocal 에서 데이터를 꺼낸다면 기존에 저장된 데이터가 없을 것이다. 심지어 다른 요청이 저장한 값이 잘못 확인될 수도 있다.

 

실제로 이커머스를 대표하는 모 대기업에서 Spring Web MVC 에서 Spring WebFlux 로 전환하던 중 ThreadLocal 을 고려하지 못해 마이페이지에 다른 사람의 정보가 노출되는 이슈가 있었다고 한다.

 

이제 테스트를 통해 실제로 이런 문제가 발생하는지 확인해보고 개선해보자.

 

 

테스트 환경은 아래와 같다.

이런 구조에서 Delay 를 2초로 준다면 2초 조금 넘는 정도에서 100개의 요청이 모두 처리된다. 실제로 처리된 로그를 보자.

 

동일한 요청임에도 start, end 를 처리하는 Thread 가 다르다는 것을 확인할 수 있다. 이런 상황에서 ThreadLocal 에 데이터를 넣는다면 값이 없거나 다른 요청이 넣은 값을 잘못 가져오는 등의 문제가 발생할 수 있다.

 

이제 ThreadLocal 변수를 만들어서 테스트해보자. 

요청에 포함된 Path Variable 값이 ID 를 ThreadLocal 변수에 저장하고 꺼내서 로그를 찍는 방식으로 확인해보자.

 

start 시점엔 동일하게 넣었는데, end 시점에는 이상한 값이 꺼내지는 것을 확인할 수 있다. 처리하는 Thread 가 변경되면서 기존에 저장한 값이 아니라 다른 요청에 의해서 저장된 다른 값이 나오게된다.

 

InheritableThreadLocal 을 통해서 테스트도 해봤는데 마찬가지로 동일한 결과가 나왔다.

당연한 결과인데 Event-Loop 에서 사용되는 각 Thread 가 서로 부모-자식 관계가 아니기 때문에 이 상황에서의 해결책이 될 수 없다.

 

Reactor 의 contextWrite & deferContextual


Reactor 에서는 contextWritedeferContextual 을 이용해 ThreadLocal 문제를 해결한다. 옛날 버전에서는 contextWrite 대신 subscriberContext 을 사용했는데 현재는 Deprecated 상태이다.

 

val key: String = "KEY"
val result: Mono<String> = Mono.deferConetxtual { ctx -> Mono.just("Hello " + ctx.get(KEY)) }
    	.contextWrite { ctx -> ctx.put(KEY, "World") }

결과적으로 이런 구조를 만들어야한다.

 

간략한 구조를 설명하면 아래와 같다.

 

  1. deferContextual { } 을 통해서 Context 에 값이 들어있다고 가정하고 로직을 작성한다.
  2. 뒤에서 contextWrite 를 통해서 실제 값을 넣어준다.

이러한 구조를 사용하는 이유는 Reactor 의 Flow 는 Subscribe 가 발생하는 순간 흘러가는 구조를 가지기 때문이다.

 

위 구조를 사용해 Spring MVC 에서 사용하던 RequestContextHolder & RequestContext 구조를 만들어보자!



 

Request Context 에 넣을 정보는 ServerWebExchange.request 값이고, 이 작업은 Controller 에 가기 전에 RequestContextInterceptor 에서 수행한다.

그리고 이렇게 저장된 RequestContext 는 위 코드와 같이 어디서든 사용이 가능하다.

 

이러한 코드 구조를 이용해서 제일 처음 발생했던 이슈를 해결해보자.

 

마지막 테스트


UserContextInterceptor 에서 Request Header 에 포함된 사용자 정보를 contextWrite 를 통해 UserContext 에 값을 저장한다.

 

그리고 어플리케이션 코드에서는 UserContextHolder.getContext() 호출시 Mono.deferContextual 을 통해 UserContext 에 저장된 사용자 정보를 꺼내 사용할 수 있다.

 

마지막으로 사용하는쪽 코드를 살펴보자.

처음에 테스트 했던 코드와 동일한 로직을 수행한다.

 

그 결과는...

start 에서 넣었던 ID 가 동일하게 end 시점에도 확인되는 것을 확인할 수 있다. 

 

 

또 다른 예시로 Logback 의 MDC 를 WebFlux 환경에서 사용하기 위한 코드를 소개하면 이번 포스팅을 마친다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함