쓰레드를 오래 붙잡는 블로킹 I/O를 피해서 동시성·처리량을 높이고 지연을 줄이는 것
컨텍스트 전파?
요청을 처리하는 동안 필요한 부가정보를 스레드/코루틴/리액티브 연산자 경계를 넘어 끊기지 않게 가져가는 것으로 비동기,논블로킹에서 스레드가 바뀔때 같은 요청의 연장선임을 보장하고 로깅, 권한, 트랜잭션이 연속성을 갖도록 보장하는 것
이 작업이 가능하기 때문에 Spring WebFlux + Coroutines 을 편하게 사용할 수 있는 것이다.
// WebFilter: 요청마다 traceId 삽입
class TraceFilter : WebFilter {
override fun filter(ex: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val traceId = ex.request.headers.getFirst("X-Trace-Id") ?: "gen-${System.nanoTime()}"
return chain.filter(ex).contextWrite { it.put("traceId", traceId) }
}
}
@RestController
class DemoCtrl(private val webClient: WebClient, private val repo: UserRepo) {
@GetMapping("/demo/{id}")
suspend fun demo(@PathVariable id: Long): String {
// (A) 여기까지는 필터가 심은 Reactor Context가 살아있고,
// 이 Mono 내부에서만 traceId 조회가 됨
val nameFromRepo = repo.findNameById(id) // Mono<String>
.map { s -> "repo:$s" }
.awaitSingle() // 값만 꺼내고 코루틴으로 넘어옴
// (B) 이제 '새' Mono(WebClient)를 만들면 기존 Reactor Context는 자동 미전파
// 아래에서 WebClient 필터가 traceId를 기대해도 못 찾음
val fromApi = webClient.get().uri("/downstream")
.retrieve().bodyToMono(String::class.java)
//.contextWrite { it.put("traceId", ???) } // ← 수동으로 다시 실어줘야 함
.awaitSingle()
return "$nameFromRepo | $fromApi"
}
}
해결: val rctx = coroutineContext[ReactorContext]?.context로 꺼내서 .contextWrite { it.putAll(rctx) }로 새 Mono에 실어주거나, 컨트롤러 입구에서 코루틴 컨텍스트로 옮겨 담아(예: withContext(ReactorContext(ctx))) 일관되게 전파.
@GetMapping("/mdc")
suspend fun mdc(): String {
MDC.put("traceId", "t-123") // ThreadLocal에 저장
val v = repo.findById(1).awaitSingle() // 값은 정상 변환
// 다른 디스패처로 전환(혹은 코루틴 스케줄링으로 스레드 바뀜)
return withContext(Dispatchers.Default) {
// ❌ 여기선 MDC가 비어있거나 다른 값일 수 있음
log.info("traceId={}", MDC.get("traceId")) // null 가능
"ok"
}
}
해결: 코루틴 쪽에서 MDCContext(mapOf("traceId" to ...))로 감싸 전파하거나, 실행기에 TaskDecorator를 붙여 복사/복원.