[Project Reactor] 11. Reactor Context: 리액티브 흐름 속의 상태 공유와 제어

y001·2025년 5월 29일

Reactive Programming

목록 보기
11/30
post-thumbnail

1. Context란 무엇인가?

1.1 개념 정의

Reactor의 Context는 한마디로 말하면 Operator 체인 사이에서 전파되는 key-value 기반의 저장소다. 이 저장소는 일반적인 컬렉션과는 다르게 다음과 같은 특징을 갖는다:

  • 불변성(Immutable): 연산자 간 충돌을 방지하고 안전한 상태 전달을 위해 불변 객체로 구현된다.
  • 구독 기반 전파: 스레드가 아닌 subscribe() 호출을 기준으로 생성 및 전파된다.
  • 전파 방향: 연산자 체인의 아래에서 위로(Downstream → Upstream) 전파된다.

1.2 Context vs ThreadLocal

Context는 종종 ThreadLocal과 비교되지만, 큰 차이가 있다. ThreadLocal은 하나의 스레드에 값을 저장하고, 해당 스레드가 작업을 수행하는 동안 값을 참조할 수 있도록 한다. 그러나 Reactor는 비동기 시스템이며, 여러 스레드가 하나의 작업을 처리할 수 있다. 즉, ThreadLocal로는 Reactor 흐름 전체에 일관된 정보를 전달할 수 없다.

반면, Contextsubscribe 시점에 고정되며, 흐름이 어떤 스레드를 거치든 상관없이 Context는 유지된다. 이 점에서 리액티브 환경에 훨씬 적합하다.


2. Context 사용 방식

2.1 Context 작성: contextWrite()

Context에 값을 쓰기 위해서는 contextWrite() 연산자를 사용한다. 이는 연산자 체인의 어느 위치에서나 사용할 수 있지만, 실제로는 체인의 가장 마지막에 두는 것이 안전하다. 이유는 전파 방향이 아래에서 위로이기 때문이다.

Mono<String> mono = Mono.deferContextual(ctx ->
    Mono.just("Hello, " + ctx.get("name"))
).contextWrite(ctx -> ctx.put("name", "Reactor"));

2.2 Context 읽기: deferContextual() vs transformDeferredContextual()

메서드용도
deferContextual()Context를 읽기만 할 때
transformDeferredContextual()Context 값을 기준으로 Mono/Flux 체인을 수정할 때

예를 들어, 구독 시점에 값을 읽고 데이터를 구성하고자 한다면 deferContextual()을 사용한다. 반면, Context 값에 따라 조건 분기를 하거나 연산자 체인 전체를 바꾸고자 할 때는 transformDeferredContextual()을 사용한다.


3. Context의 전파와 특징

3.1 구독마다 독립된 Context 생성

Reactor Context는 subscribe()가 호출될 때마다 독립적으로 생성된다. 이는 동일한 Publisher를 여러 번 구독하더라도, 각각의 Context는 서로 영향을 주지 않도록 보장한다.

Mono<String> mono = Mono.deferContextual(ctx ->
    Mono.just("Company: " + ctx.get("company"))
);

mono.contextWrite(ctx -> ctx.put("company", "Apple"))
    .subscribe(System.out::println);

mono.contextWrite(ctx -> ctx.put("company", "Microsoft"))
    .subscribe(System.out::println);

출력:

Company: Apple  
Company: Microsoft

3.2 동일 키 덮어쓰기와 전파 우선순위

Reactor Context는 Operator 체인 아래에서 위로 전파된다. 따라서 동일한 key를 여러 번 설정하면 아래쪽의 contextWrite가 우선순위를 가진다.

Mono.deferContextual(ctx -> Mono.just(ctx.get("company")))
    .contextWrite(ctx -> ctx.put("company", "A"))
    .contextWrite(ctx -> ctx.put("company", "B"))
    .subscribe(System.out::println);

출력:

B

이는 실수로 contextWrite의 순서를 잘못 지정하면 의도치 않게 다른 값을 읽는 오류로 이어질 수 있음을 의미하며, contextWrite는 반드시 체인의 끝에 위치시켜야 한다.


4. Context 내부 구조와 연산자 흐름

4.1 Inner Sequence와 Outer Context

flatMap 같은 연산자를 사용하면 내부적으로 또 다른 Mono/Flux 흐름이 생성된다. 이때, Inner 흐름에서는 Outer Context에 접근할 수 있지만, 그 반대는 불가능하다.

Mono.just("outer")
    .flatMap(data ->
        Mono.deferContextual(innerCtx ->
            Mono.just(innerCtx.get("key") + " / " + data)
        ).contextWrite(ctx -> ctx.put("key", "inner"))
    )
    .contextWrite(ctx -> ctx.put("key", "outer"))
    .subscribe(System.out::println);

출력:

inner / outer

만약 외부 연산자가 inner에서 설정한 Context 값을 읽으려 한다면 예외가 발생한다. Context는 Downstream에서 Upstream으로만 전파되며, 역방향 전파는 일어나지 않는다.


5. 자주 사용하는 Context API

  • put(k, v): 값 저장
  • of(k1, v1, k2, v2, ...): 다수의 값을 한번에 저장
  • putAll(ContextView): 다른 ContextView 병합
  • get(k): 값 조회
  • getOrDefault(k, default): 존재하지 않으면 기본값 반환
  • hasKey(k): 존재 여부 확인
  • delete(k): 삭제
  • size(), isEmpty(): 상태 확인

6. 실무 활용 예시: 인증 토큰 전달

Context는 인증 토큰, 트랜잭션 ID, 지역 설정 등 비즈니스 로직과 분리된 독립적인 정보를 흐름 속에 주입할 때 매우 유용하다.

public class Example {
    public static void main(String[] args) {
        Mono<String> result = postBook(Mono.just(new Book("123", "Reactor", "Kevin")))
            .contextWrite(Context.of("authToken", "Bearer abc.def.ghi"));

        result.subscribe(System.out::println);
    }

    static Mono<String> postBook(Mono<Book> book) {
        return Mono.zip(
            book,
            Mono.deferContextual(ctx -> Mono.just(ctx.get("authToken")))
        ).map(tuple ->
            "POST Book(" + tuple.getT1().title + ") with token: " + tuple.getT2()
        );
    }

    record Book(String isbn, String title, String author) {}
}

인증 로직과 실제 비즈니스 로직이 완벽히 분리되며, 코드의 가독성과 유지보수성이 향상된다.


마무리: Context는 부가 정보의 안전한 컨테이너

Reactor의 Context는 단순한 상태 저장소 이상의 기능을 제공한다. 비동기 연산자 체인 안에서 스레드 독립적으로 구독 기반으로 전파되는 불변의 정보 저장소라는 점에서, 고급 리액티브 흐름을 설계하는 데 필수적인 도구다.

  • 구독 기반으로 Context가 생성되고
  • Operator 체인을 따라 흐름과 함께 전파되며
  • 연산자 간 분리된 관심사를 유지하면서도
  • 공통의 상태나 설정을 안전하게 공유할 수 있게 해준다.

0개의 댓글