
Reactor의 Context는 한마디로 말하면 Operator 체인 사이에서 전파되는 key-value 기반의 저장소다. 이 저장소는 일반적인 컬렉션과는 다르게 다음과 같은 특징을 갖는다:
Context는 종종 ThreadLocal과 비교되지만, 큰 차이가 있다. ThreadLocal은 하나의 스레드에 값을 저장하고, 해당 스레드가 작업을 수행하는 동안 값을 참조할 수 있도록 한다. 그러나 Reactor는 비동기 시스템이며, 여러 스레드가 하나의 작업을 처리할 수 있다. 즉, ThreadLocal로는 Reactor 흐름 전체에 일관된 정보를 전달할 수 없다.
반면, Context는 subscribe 시점에 고정되며, 흐름이 어떤 스레드를 거치든 상관없이 Context는 유지된다. 이 점에서 리액티브 환경에 훨씬 적합하다.
contextWrite()Context에 값을 쓰기 위해서는 contextWrite() 연산자를 사용한다. 이는 연산자 체인의 어느 위치에서나 사용할 수 있지만, 실제로는 체인의 가장 마지막에 두는 것이 안전하다. 이유는 전파 방향이 아래에서 위로이기 때문이다.
Mono<String> mono = Mono.deferContextual(ctx ->
Mono.just("Hello, " + ctx.get("name"))
).contextWrite(ctx -> ctx.put("name", "Reactor"));
deferContextual() vs transformDeferredContextual()| 메서드 | 용도 |
|---|---|
deferContextual() | Context를 읽기만 할 때 |
transformDeferredContextual() | Context 값을 기준으로 Mono/Flux 체인을 수정할 때 |
예를 들어, 구독 시점에 값을 읽고 데이터를 구성하고자 한다면 deferContextual()을 사용한다. 반면, Context 값에 따라 조건 분기를 하거나 연산자 체인 전체를 바꾸고자 할 때는 transformDeferredContextual()을 사용한다.
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
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는 반드시 체인의 끝에 위치시켜야 한다.
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으로만 전파되며, 역방향 전파는 일어나지 않는다.
put(k, v): 값 저장of(k1, v1, k2, v2, ...): 다수의 값을 한번에 저장putAll(ContextView): 다른 ContextView 병합get(k): 값 조회getOrDefault(k, default): 존재하지 않으면 기본값 반환hasKey(k): 존재 여부 확인delete(k): 삭제size(), isEmpty(): 상태 확인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) {}
}
인증 로직과 실제 비즈니스 로직이 완벽히 분리되며, 코드의 가독성과 유지보수성이 향상된다.
Reactor의 Context는 단순한 상태 저장소 이상의 기능을 제공한다. 비동기 연산자 체인 안에서 스레드 독립적으로 구독 기반으로 전파되는 불변의 정보 저장소라는 점에서, 고급 리액티브 흐름을 설계하는 데 필수적인 도구다.