ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Context-Propagation Support
    Spring/Webflux 2025. 10. 4. 15:04

     

     

    https://projectreactor.io/docs/core/release/reference/advanced-contextPropagation.html

     

    Context-Propagation Support :: Reactor Core Reference Guide

    Both default and automatic modes have an impact on performance. Accessing ThreadLocal variables can impact a reactive pipeline significantly. If the highest scalability and performance is the goal, more verbose approaches for logging and explicit argument

    projectreactor.io

     

    1. 컨텍스트 전파 지원이란?

    Reactor-Core는 3.5.0 버전부터 io.micrometer:context-propagation SPI(Service Provider Interface)에 대한 지원을 내장하고 있습니다.

    이 라이브러리의 목적은 Reactor의 Context (ContextView/Context) 개념과 ThreadLocal 변수 간의 상태를 쉽게 상호 적응시키고 전파하는 수단을 제공하는 것입니다.

     

    ReactorContextAccessor: 이 클래스는 Context-Propagation 라이브러리가 Reactor의 Context와 ContextView를 이해할 수 있도록 돕습니다. SPI를 구현하며  java.util.ServiceLoader를 통해 로드됩니다.

     

    사용자 조치: 사용자는 reactor-coreio.micrometer:context-propagation 두 라이브러리 모두에 대한 의존성을 추가하는 것 외에는 별다른 조치가 필요하지 않습니다.

     

    ReactorContextAccessor 사용: 이 클래스는 공개(public)되어 있지만, 일반적인 사용자 코드가 직접 접근할 필요는 없습니다.

    Reactor-Core는 io.micrometer:context-propagation와 함께 두 가지 작동 모드를 지원합니다:

    1. the default (limited) mode
    2. 자동 모드 (automatic mode): Hooks.enableAutomaticContextPropagation()을 통해 활성화됩니다.

    이 두 모드의 주요 차이점은

    Reactor Context에 데이터를 쓰는 방식현재 연결된 Subscriber의 Context 내용을 반영하는 ThreadLocal 상태에 접근하는 방식에서 논의됩니다. 자동 모드는 새로운 구독에만 적용되므로, 애플리케이션 시작 시 이 훅을 활성화하는 것이 권장됩니다.

    @Configuration
    public class ContextPropagationConfig {
    
        // PostConstruct를 사용하여 애플리케이션 초기화 시점에 훅과 레지스트리를 설정합니다.
        @PostConstruct
        void setupHooks() {
            // 1. Hooks 활성화: Reactor에게 Context 자동 전파를 지시합니다.
            Hooks.enableAutomaticContextPropagation();
        }
    }
     

    2. Context 에 쓰기 (Writing to Context)

    애플리케이션에 따라, 이미 채워진 ThreadLocal 상태를 Context의 항목으로 저장해야 하거나, 단순히 Context를 직접 채워야 할 수 있습니다.

    2.1. contextWrite Operator

    ThreadLocal로 접근할 값들이 구독 시점(subscription time)에 존재하지 않거나(또는 그럴 필요가 없거나), Context에 즉시 저장될 수 있는 경우에 사용합니다. 이 연산자는 수동으로 Context에 값을 설정합니다.

    // assuming TL is known to Context-Propagation as key TLKEY.
    static final ThreadLocal<String> TL = new ThreadLocal<>();
    
    // in the main Thread, TL is not set
    Mono.deferContextual(ctx ->
      Mono.delay(Duration.ofSeconds(1))
          // we're now in another thread, TL is not explicitly set
          .map(v -> "delayed ctx[" + TLKEY + "]=" + ctx.getOrDefault(TLKEY, "not found") + ", TL=" + TL.get()))
    .contextWrite(ctx -> ctx.put(TLKEY, "HELLO"))
    .block(); // returns "delayed ctx[TLKEY]=HELLO, TL=null" in default mode
              // returns "delayed ctx[TLKEY]=HELLO, TL=HELLO" in automatic mode

     

    Mono.delay(Duration.ofSeconds(1)): 1초 지연을 발생시키는 비동기 연산자
    중요: 이 연산자는 일반적으로 다른 스케줄러(다른 스레드)에서 실행되도록 스레드를 전환합니다. 
    따라서 이후의 .map() 연산은 메인 스레드가 아닌 다른 스레드에서 실행됩니다.

     

    context 는 밑에서 위로 흐르므로,

    context 에 TLKEY,"HELLO" 를 기록했을 때, 자동모드에서는 전파된다

     

    모드 결과 예시 설명
    기본 모드 delayed ctx[TLKEY]=HELLO, TL=null Context에는 값이 저장되었지만, 체인 실행 중 ThreadLocal로 복원되지 않음.
    자동 모드 delayed ctx[TLKEY]=HELLO, TL=HELLO Context에 저장된 값이 체인 실행 중 자동으로 해당 ThreadLocal에 복원됨.
     
     

    2.2. contextCapture Operator

    이 연산자는 구독 시점ThreadLocal 값(들)을 캡처하여 이 값들을 업스트림 연산자를 위해 Reactor Context에 반영해야 할 때 사용됩니다.

    • 작동 원리: 수동 contextWrite 연산자와 달리, contextCapture는 context-propagation API를 사용하여 ContextSnapshot을 얻은 다음, 이 스냅샷을 사용하여 Reactor Context를 채웁니다.
    • 결과: 구독 단계에서 ThreadLocalAccessor가 등록된 ThreadLocal 값이 있었다면, 그 값들이 Reactor Context에 저장되어 런타임에 업스트림 연산자에서 볼 수 있게 됩니다.
    모드 결과 예시 설명
    기본 모드 delayed ctx[TLKEY]=HELLO, TL=null Context에는 값이 캡처되었지만, 체인 실행 중 ThreadLocal로 복원되지 않음.
    자동 모드 delayed ctx[TLKEY]=HELLO, TL=HELLO Context에 캡처된 값이 체인 실행 중 자동으로 해당 ThreadLocal에 복원됨.
     

    자동 모드에서의 최적화: Flux#blockFirst(), Mono#block()블로킹 연산자들은 contextCapture()를 투명하게(transparently) 수행하므로, 대부분의 경우 자동 모드에서는 이 연산자를 명시적으로 추가할 필요가 없습니다.

    // assuming TL is known to Context-Propagation as key TLKEY.
    static final ThreadLocal<String> TL = new ThreadLocal<>();
    
    // in the main Thread, TL is set to "HELLO"
    TL.set("HELLO");
    
    Mono.deferContextual(ctx ->
      Mono.delay(Duration.ofSeconds(1))
          // we're now in another thread, TL is not explicitly set
          .map(v -> "delayed ctx[" + TLKEY + "]=" + ctx.getOrDefault(TLKEY, "not found") + ", TL=" + TL.get()))
    .contextCapture() // can be skipped in automatic mode when a blocking operator follows
    .block(); // returns "delayed ctx[TLKEY]=HELLO, TL=null" in default mode
              // returns "delayed ctx[TLKEY]=HELLO, TL=HELLO" in automatic mode

     

    .contextCapture() 메소드를 사용하면, TL=Hello 값이 캡처되어 전파 된다

    설명을 위해 명시적으로 .contextCapture() 를 사용했지만

    Flux#blockFirst(), Flux#blockLast(), Flux#toIterable(), Mono#block(), Mono#blockOptional() 의  메소드에서는 투명하게(명백하게) 수행되므로, 굳이 추가할 필요는 없다.

     

    3. ThreadLocal 상태에 접근 (Accessing ThreadLocal state)

    Reactor-Core 3.5.0부터 ThreadLocal 상태는 제한된 연산자 집합에서 복원(restoration)됩니다.

    이것을  기본(제한된) 모드라고 합니다.

    3.5.3 버전에서는 자동 모드가 추가되어 반응형 체인 전체에서 ThreadLocal 값에 접근할 수 있게 되었습니다.

    • 복원 메커니즘: Reactor-Core는 Context에 저장된 값과 ContextRegistry에 등록된 ThreadLocalAccessor 인스턴스를 사용하여 ThreadLocal 상태 복원을 수행합니다.

     

    3.1. 기본(default) 모드 연산자 for snapshot restoration: handle and tap

    기본 모드에서는 Flux와 Mono의 handletap variants 들이 런타임에 Context-Propagation 라이브러리가 사용 가능하면 동작이 약간 수정됩니다.

    구체적으로, 다운스트림 ContextView가 비어 있지 않으면(either manually or via the contextCapture() operator) 이 연산자들은 스냅샷이 이미 캡처되었다고 가정하고, 그 스냅샷에서 ThreadLocal을 투명하게 복원하려 시도합니다. ContextView에 해당 키가 없으면 그 ThreadLocal은 건드리지 않습니다.

    이 연산자들은 사용자 제공 코드 주변에서 복원이 수행되도록 보장합니다:

    • handle은 BiConsumer를 ThreadLocal을 복원하는 래퍼로 감쌉니다.
    • tap 계열은 각 메서드 주위에 동일한 종류의 래핑을 하도록 SignalListener 를 감쌉니다 (여기에는 addToContext도 포함됩니다).

    의도는 가능한 최소한의 연산자 집합이 투명(명확)하게 복원을 수행하도록 하는 것입니다.

    그래서 변환 능력이 있는 하나(handle)부수효과(side-effect)용 하나(tap)를 선택했습니다.

     

    //assuming TL is known to Context-Propagation.
    static final ThreadLocal<String> TL = new ThreadLocal<>();
    
    //in the main thread, TL is set to "HELLO"
    TL.set("HELLO");
    
    Mono.delay(Duration.ofSeconds(1))
      //we're now in another thread, TL is not set yet
      .doOnNext(v -> System.out.println(TL.get()))
      //inside the handler however, TL _is_ restored
      .handle((v, sink) -> sink.next("handled delayed TL=" + TL.get()))
      .contextCapture()
      .block(); // prints "null" and returns "handled delayed TL=HELLO"

     

    handle 내부에서는 ThreadLocal이 복원되어 HELLO가 출력되지만, 그 이전 doOnNext에서는 복원되지 않아 null이 출력됩니다.

    이는 기본 모드가 선택된 연산자의 사용자 코드 실행 주변으로만 ThreadLocal 상태를 제한하기 때문입니다.

    * default mode 에서는 handletap 에서만 context propagation 

     

    3.2. 자동 모드 (Automatic mode)

    Automactic mode 에서는 모든 연산자Thread 경계를 넘어 ThreadLocal 상태를 복원합니다.

    vs 기본 모드에서는 선택된 연산자(handle and tap ) 만 복원합니다.

     

    활성화: Hooks.enableAutomaticContextPropagation()을 애플리케이션 시작 시 호출하여 활성화합니다.

    이 모드는 새로운 구독에만 적용되므로, 애플리케이션 시작 시 이 훅을 켜는 것이 좋습니다.

     

    Reactive Streams 사양은 연산 체인을 스레드에 무관하게 만드는(spec-agnostic) 것이기 때문에, 이 기능을 구현하는 것은 쉬운 일이 아닙니다. Reactor-Core는 스레드 전환의 근원을 최대한 제어하고 Reactor Context를 진실의 근원(source of truth)으로 처리하여 스냅샷 복원을 수행합니다.

     

    Warning

    기본 모드가 사용자 코드(선택된 연산자 인자)로 한정하여 ThreadLocal을 복원한다면, 자동 모드는 연산자 경계를 가로질러 ThreadLocal 상태가 전달될 수 있게 합니다. 이 동작은 동일 스레드를 재사용하는 다른 코드로 상태가 유출되는 것을 방지하기 위한 적절한 정리(cleanup)를 필요로 합니다. 

    This requires proper cleanup to avoid leaking the state to unrelated code which reuses the same Thread

     

    이를 위해서는 Context 내에 등록된 ThreadLocalAccessor 인스턴스에서 해당 키가 존재하지 않을 경우,
    그것을 해당 ThreadLocal 값을 제거하라는 신호로 처리해야 합니다.

    This requires to treat absent keys in the Context for registered instances of ThreadLocalAccessor as signals to clear the corresponding ThreadLocal state.

     

    이는 빈 Context (empty Context) 의 경우 특히 중요합니다. 빈 Context는 등록된 ThreadLocalAccessor 인스턴스에 대해 모든 상태를 지우도록 합니다

    This is especially important for an empty Context, which clears all state for registered ThreadLocalAccessor instances

     

    요약

     

    • ✅ 사용하는 라이브러리(MDC, Micrometer, SecurityContextHolder 등)가 ThreadLocalAccessor를 제대로 등록했는가
    • ✅ 각 ThreadLocalAccessor가 “없음 → clear()” 로직을 구현했는가
    • ✅ Reactor Context가 비워지는 시점에 ThreadLocal이 완전히 정리되는가

    4. 어떤 모드를 선택해야 할까요? (Which mode should I choose?)

    기본 모드와 자동 모드 모두 성능에 영향을 미칩니다.

    ThreadLocal 변수에 접근하는 것은 Reactive Pipeline 에 상당한 영향을 줄 수 있습니다.

    최고 수준의 확장성과 성능을 목표로 한다면, 로깅을 더 상세하게 하거나 인자를 명시적으로 전달하는 방식처럼 ThreadLocal 의존을 피하는 더 장황한 접근법을 고려할 수 있습니다.

     

    라이브러리 사용 타협: Micrometer  SLF4J와 같이 ThreadLocal 상태를 편의상 사용하여 유의미한 생산 등급 기능을 제공하는 확립된 관찰성(Observability) 라이브러리에 대한 접근이 이해된 타협점이라면, 모드 선택은 또 다른 타협점이 됩니다.

    If access to established libraries in the space of Observability, such as Micrometer and SLF4J, which use ThreadLocal state for convenience to provide meaningful production grade features is an understood compromise, the choice of the mode is yet another compromise to make

     

    모드 선택 기준: Automatic mode 는 애플리케이션의 흐름과 사용된 연산자의 양에 따라 default mode 보다 더 좋을 수도, 더 나쁠 수도 있습니다.

     

    결론: 유일한 권장 사항은 측정하는 것입니다.

    예상되는 부하가 주어졌을 때 애플리케이션이 어떻게 작동하고 어떤 확장성 및 성능 특성을 얻는지 측정해 보아야 합니다

     

    Vocabulary

    popu·late [ |pɑːpjuleɪt ]  2.이주시키다(전파하다) 1.살다  3.덧붙이다

    * restored : 복원하다 ; 다른 쓰레드에 전파되어 복원된다는 의미로 사용 TL 의 value 가 다른 쓰레드로 전파되어 resotred 되었다

    * transparently : 투명하게, 명확하게, 보이지 않게 ( context propagation 이 사용자에게 보이지 않고, 자동으로, 암시적으로 동작한다는 뜻

     

    'Spring > Webflux' 카테고리의 다른 글

    Huge Performance Overhead From ContextPropagation With Micrometer Tracing  (1) 2025.10.06
    14장 Operator 8 - multicast  (0) 2023.07.30
    14장 Operator 7 - split  (0) 2023.07.29
    14장 Operator 6 - time  (0) 2023.07.26
    14장 Operator 5 - Error  (1) 2023.07.22

    댓글

Designed by Tistory.