Backend

<Spring> Webflux + Coroutine + MDC

wjdtkdgns 2024. 5. 10. 23:38

๐Ÿ“Œ MDC๋ž€?

Mapped Diagnostic Context์˜ ์•ฝ์ž๋กœ,

๋กœ๊น… ์‹œ์Šคํ…œ์—์„œ ์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์ด๋‚˜ ํŠน์ • ์‹คํ–‰ ๊ฒฝ๋กœ๋ฅผ ์ถ”์ ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ๋„๊ตฌ์ด๋‹ค.

๋‹ค์ค‘ ์Šค๋ ˆ๋“œ ์ƒํ™ฉ์—์„œ ์š”์ฒญ ๊ฐ„ ๊ตฌ๋ถ„์„ ์œ„ํ•ด ์ฃผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

Java์˜ ๋กœ๊ฑฐ์ธ SLF4J๊ฐ€ ์ด๋ฅผ ๋‚ด๋ถ€์ ์œผ๋กœ ์ง€์›ํ•ด ์ค€๋‹ค.

 

๐Ÿ“Œ Webflux์™€ MDC

MDC๋Š” ๋ณดํ†ต ์Šค๋ ˆ๋“œ ๋กœ์ปฌ์„ ์‚ฌ์šฉํ•œ๋‹ค.

ํ•˜์ง€๋งŒ ์–ด๋–ค ์Šค๋ ˆ๋“œ์—์„œ ์ž‘์—…์„ ์ง„ํ–‰ํ• ์ง€ ํ™•์‹ ํ•  ์ˆ˜ ์—†๋Š” Webflux์˜ ํŠน์„ฑ์ƒ ์Šค๋ ˆ๋“œ ๋กœ์ปฌ ์‚ฌ์šฉ์ด ์–ด๋ ต๋‹ค.

์ด์— MVC์™€ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด์•ผ ํ–ˆ๋‹ค.

 

์•„๋ž˜ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋Š”๋ฐ, ์•„๋ž˜ ๋ธ”๋กœ๊ทธ๋ฅผ ๋งŽ์ด ์ฐธ๊ณ ํ–ˆ๋‹ค.

 

๋ฐฐ๋‹ฌ์˜๋ฏผ์กฑ ์ตœ์ „๋ฐฉ ์‹œ์Šคํ…œ! ‘๊ฐ€๊ฒŒ๋…ธ์ถœ ์‹œ์Šคํ…œ’์„ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค. | ์šฐ์•„ํ•œํ˜•์ œ๋“ค ๊ธฐ์ˆ ๋ธ”๋กœ๊ทธ

{{item.name}} ์•ˆ๋…•ํ•˜์„ธ์š” ์šฐ์•„ํ•œํ˜•์ œ๋“ค ํ”„๋ก ํŠธ๊ฒ€์ƒ‰์„œ๋น„์ŠคํŒ€ ๊ถŒ์šฉ๊ทผ์ž…๋‹ˆ๋‹ค. ์ €๋Š” "๋จผ๋ฐ์ด ํ”„๋กœ์ ํŠธ" ๋ผ๋Š” 2019๋…„ ๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ์—์„œ ์š”๋ž€ํ•˜๊ฒŒ ํƒ„์ƒํ•˜์˜€๊ณ , ํƒ„์ƒํ•œ ์ˆœ๊ฐ„๋ถ€ํ„ฐ ์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ๋‹ฌ์˜๋ฏผ์กฑ ์ตœ

techblog.woowahan.com

 

๐Ÿ”ต MDC ์„ค์ •

Webflux์—์„œ ์š”์ฒญ์ด ์ „๋‹ฌ๋˜๋Š” ๊ณผ์ •์„ ๋ณด์•˜์„ ๋•Œ, ์ด๋Ÿฐ ๊ณผ์ •์„ ๊ฑฐ์ณ์„œ ํ˜๋Ÿฌ๊ฐ€๊ฒŒ ๋œ๋‹ค.

์š”์ฒญ์ด ๋“ค์–ด์™”์„ ๋•Œ, ์š”์ฒญ์— ๋Œ€ํ•œ TraceId ๊ฐ’์„ MDC์— ๋„ฃ์–ด์ค˜์•ผ ํ•˜๋Š”๋ฐ,

์ด ๊ณผ์ •์„ WebFilter๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌํ–ˆ๋‹ค.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
class MdcLoggingFilter : WebFilter {
    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
        return chain.filter(exchange)
            .contextWrite { it.createPutWithMDC("traceId", UUID.randomUUID().toString()) }
    }
}

Webflux๋Š” Filter๊ฐ€ ์•„๋‹ˆ๋ผ WebFilter๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

์ด ํ•„ํ„ฐ์—์„œ Context์™€ MDC์— ๊ฐ’์„ ๋„ฃ์–ด์ฃผ๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ–ˆ๋‹ค.

์•„๋ž˜๋Š” ์œ„ ์ž‘์—…์— ๋Œ€ํ•œ ์ฝ”๋“œ์ด๋‹ค

private fun Context.toMap(): Map<String, String> = this.stream()
    .map { ctx -> ctx.key.toString() to ctx.value.toString() }
    .toList().toMap()

fun Context.createPutWithMDC(key: String, value: String): Context {
    val mapOfContext = this.stream()
        .map { ctx -> ctx.key to ctx.value }
        .toList().toMap().toMutableMap()
    mapOfContext[key] = value
    MDC.put(key, value)
    return Context.of(mapOfContext)
}

์œ„ ๊ณผ์ •์„ ํ†ตํ•ด TraceId๋ฅผ MDC, Context์— ๋„ฃ์–ด์คฌ๋‹ค.

 

๐Ÿ”ต ์Šค๋ ˆ๋“œ ๊ฐ„ ์ „ํŒŒ

์œ„์—์„œ ์–ธ๊ธ‰ํ•œ ๊ฒƒ์ฒ˜๋Ÿผ, WebFlux๋Š” ์ž‘์—…์ด ํ•œ ์Šค๋ ˆ๋“œ์—์„œ ์ง„ํ–‰๋œ๋‹ค๋Š” ๋ณด์žฅ์„ ํ•  ์ˆ˜ ์—†๋‹ค.

์ž‘์—… ์ค‘ ์Šค๋ ˆ๋“œ ์ „ํ™˜์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด ์ƒํ™ฉ์—์„œ๋„ TraceId๊ฐ€ ๋ณด์กด๋˜์–ด์•ผ ํ•œ๋‹ค.

์ด๋ฅผ ์œ„ํ•ด, ์œ„์—์„œ ์–ธ๊ธ‰ํ•œ ๋ธ”๋กœ๊ทธ์—์„œ ๋ณธ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

ํ•˜๋‚˜ํ•˜๋‚˜ ์•Œ์•„๋ณด์ž

/**
 * @link https://www.novatec-gmbh.de/en/blog/how-can-the-mdc-context-be-used-in-the-reactive-spring-applications/
 */
@Configuration
class MdcContextLifterConfiguration {
    companion object {
        val MDC_CONTEXT_REACTOR_KEY: String = MdcContextLifterConfiguration::class.java.name
    }

    @PostConstruct
    fun contextOperatorHook() {
        Hooks.onEachOperator(
            MDC_CONTEXT_REACTOR_KEY,
            Operators.lift { _, subscriber ->
                MdcContextLifter<Any?>(subscriber)
            }
        )
    }

    @PreDestroy
    fun cleanupHook() {
        Hooks.resetOnEachOperator(MDC_CONTEXT_REACTOR_KEY)
    }
}

์œ„ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด Mono/Flux๋กœ ์ƒ์„ฑ๋œ operator์— ๋Œ€ํ•˜์—ฌ ์ ์šฉํ•  Publisher ๋Œ€ํ•ด ์ปค์Šคํ…€ํ•œ๋‹ค.

๊ธฐ๋ณธ subscriber๋ฅผ MdcContextLifter๋กœ wrapping ํ•˜๋„๋ก ํ•œ๋‹ค.

/**
 * Helper that copies the state of Reactor [Context] to MDC on the #onNext function.
 */
class MdcContextLifter<T>(
    private val coreSubscriber: CoreSubscriber<T>,
) : CoreSubscriber<T> {
    override fun onSubscribe(subscription: Subscription) {
        coreSubscriber.onSubscribe(subscription)
    }

    override fun onNext(t: T) {
        coreSubscriber.currentContext().copyToMdc()
        coreSubscriber.onNext(t)
    }

    override fun onError(throwable: Throwable) {
        coreSubscriber.onError(throwable)
    }

    override fun onComplete() {
        coreSubscriber.onComplete()
    }

    override fun currentContext(): Context {
        return coreSubscriber.currentContext()
    }
}

๊ธฐ์กด subscriber์™€ ๋‹ค๋ฅธ ์ ์€ onNext์— MDC ๊ฐ’์„ ๋„ฃ์–ด์ฃผ๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•œ ๊ฒƒ์ด๋‹ค.

reactor๊ฐ€ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์„ ์ฒ˜๋ฆฌํ•˜๋ฉฐ ์Šค๋ ˆ๋“œ ์ „ํ™˜์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, onNext์— ๋กœ์ง์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

์ด ๋กœ์ง์„ ์ถ”๊ฐ€ํ•จ์œผ๋กœ์จ onNext ๋งˆ๋‹ค Context์˜ ๋‚ด์šฉ์„ MDC๋กœ ๋ฎ์–ด์“ฐ๋„๋ก ํ•˜์—ฌ, ๋กœ๊น…์— ํ•„์š”ํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์Šค๋ ˆ๋“œ๋งˆ๋‹ค ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

๋ฎ์–ด์“ฐ๋Š” ๋กœ์ง์€ ๋‹ค์Œ ์ฝ”๋“œ์— ๋‚˜์™€์žˆ๋‹ค.

fun Context.copyToMdc() {
    if (!this.isEmpty) {
        val map = this.toMap()
        MDC.setContextMap(map)
    } else {
        MDC.clear()
    }
}

private fun Context.toMap(): Map<String, String> = this.stream()
    .map { ctx -> ctx.key.toString() to ctx.value.toString() }
    .toList().toMap()

 

๐Ÿ“Œ Coroutine๊ณผ MDC

๐Ÿ”ต Coroutine์— MDC ์ ์šฉ

Coroutine์˜ ํŠน์„ฑ์ƒ, ํ˜„์žฌ ์ง„ํ–‰ํ•˜๋Š” Coroutine์ด ์ค‘์ง€ ํ›„ ์žฌ๊ฐœ๋  ๋•Œ ์–ด๋–ค ์Šค๋ ˆ๋“œ์—์„œ ์ž‘๋™ํ• ์ง€ ๋ชจ๋ฅธ๋‹ค.

์ด์— MDC์— ๊ฐ’์„ ๋„ฃ์–ด๋‘๋ฉด, ๋‹ค์Œ ์ž‘์—… ์žฌ๊ฐœ ์‹œ์ ์— ์ด ๊ฐ’์ด ์‚ฌ๋ผ์ ธ ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

Coroutine์—๋Š” MDC๋ฅผ ์ง€์›ํ•˜๊ธฐ ์œ„ํ•ด, MDCContext๊ฐ€ ์กด์žฌํ•œ๋‹ค.

MDCContext๋Š” CoroutineContext๋ฅผ ์œ„ํ•œ MDC context ์š”์†Œ์ด๋‹ค.

 

Coroutine์„ ์‚ฌ์šฉํ•  ๋•Œ, MDCContext๋ฅผ ํ†ตํ•ด ๊ธฐ์กด MDC์— ์กด์žฌํ•˜๋˜ ์š”์†Œ๋“ค์„ ๋ณต์‚ฌํ•˜์—ฌ ์ฝ”๋ฃจํ‹ด ์Šค์ฝ”ํ”„๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ๋‹ค.

์•„๋ž˜๋Š” ํ•ด๋‹น ๋กœ์ง์„ ๊ตฌํ˜„ํ•œ ์ปค์Šคํ…€ Coroutine ์ƒ์„ฑ ๊ด€๋ จ ํ•จ์ˆ˜์ด๋‹ค.

suspend fun <T> withMDCContext(
    context: CoroutineContext = Dispatchers.IO,
    block: suspend () -> T,
): T {
    val contextMap = MDC.getCopyOfContextMap() ?: emptyMap()
    return withContext(context + MDCContext(contextMap)) { block() }
}
fun mdcCoroutineScope(context: CoroutineContext, traceId: String): CoroutineScope {
    val contextMap = MDC.getCopyOfContextMap() ?: emptyMap()
    contextMap.plus("traceId" to traceId)
    return CoroutineScope(context + MDCContext(contextMap))
}

 

๐Ÿ”ต Reactor์™€ Coroutine

Reactor์—์„œ์˜ MDC ๊ฐ’ ์ „ํŒŒ, Coroutine์—์„œ์˜ MDC ๊ฐ’ ์ „ํŒŒ์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์•˜๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์ด ๋‘˜์„ ๊ฐ™์ด ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ์—”, ์–ด๋–ป๊ฒŒ ๊ฐ’์ด ์ „๋‹ฌ๋˜๋Š”๊ฐ€?

์ด๋Š” ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•ด ๋ณด๋ฉด ๋œ๋‹ค.

// ์—ฌ๊ธฐ๋ถ€ํ„ฐ ์‹œ์ž‘
public fun <T> mono(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> T?
): Mono<T> {
    require(context[Job] === null) { "Mono context cannot contain job in it." +
            "Its lifecycle should be managed via Disposable handle. Had $context" }
    return monoInternal(GlobalScope, context, block)
}

private fun <T> monoInternal(
    scope: CoroutineScope, // support for legacy mono in scope
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T?
): Mono<T> = Mono.create { sink ->
    val reactorContext = context.extendReactorContext(sink.currentContext())
    val newContext = scope.newCoroutineContext(context + reactorContext)
    val coroutine = MonoCoroutine(newContext, sink)
    sink.onDispose(coroutine)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
}

internal fun CoroutineContext.extendReactorContext(extensions: ContextView): CoroutineContext =
    (this[ReactorContext]?.context?.putAll(extensions) ?: extensions).asCoroutineContext()

Webflux์—์„œ Coroutine์œผ๋กœ ๋„˜์–ด๊ฐ€๋Š” ๋ถ€๋ถ„์—์„œ ์œ„ ์ฝ”๋“œ๋“ค์ด ๋™์ž‘ํ•œ๋‹ค.

์ฝ”๋“œ์˜ ํ๋ฆ„์„ ๋”ฐ๋ผ๊ฐ€ ๋ณด๋ฉด, ReactorContext๊ฐ€ CoroutineContext๋กœ ์ „ํ™˜๋˜๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค.

์ด์ฒ˜๋Ÿผ ReactorContext ๋‚ด๋ถ€์ ์œผ๋กœ MDC ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋ฉด, ์ฝ”๋ฃจํ‹ด์œผ๋กœ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ๋œ๋‹ค.

๋ณ„๋‹ค๋ฅธ ์ž‘์—…์ด ํ•„์š” ์—†๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

 

* Webflux์—์„œ Coroutine์œผ๋กœ ์ „ํ™˜๋˜๋Š” ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์•„๋ž˜ ๊ธ€์„ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”

[Backend] - SUSU์˜ Coroutine

 

ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์„œ ์˜๋ฌธ์ด ์žˆ๋‹ค.

MDC์— ๊ฐ’์„ ๋„ฃ์–ด์ฃผ๋Š” ๋ถ€๋ถ„์ด ์—†๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ฐํžŒ ๋กœ๊ทธ์—๋Š” TraceId๊ฐ€ ์ž˜ ๋‚˜์˜จ๋‹ค.

์ด๋Š” Coroutine Context์— ๋Œ€ํ•œ ์„ค์ •์ด ์—†์œผ๋ฉด, ๊ธฐ๋ณธ์ ์œผ๋กœ Dispatcher.Unconfined๋กœ ์ž‘๋™ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ๋ณด์ž

if (isSuspendingFunction) {
    Object coroutineContext = exchange.getAttribute(COROUTINE_CONTEXT_ATTRIBUTE);
    if (coroutineContext == null) {
        return CoroutinesUtils.invokeSuspendingFunction(method, target, args);
    }
    else {
        return CoroutinesUtils.invokeSuspendingFunction((CoroutineContext) coroutineContext, method, target, args);
    }
}

 

์„ค์ •๋œ ๊ฐ’์ด ์—†์œผ๋ฉด ์œ„๋กœ ์ง„ํ–‰ํ•˜๊ฒŒ ๋˜๋Š”๋ฐ,

public static Publisher<?> invokeSuspendingFunction(Method method, Object target, @Nullable Object... args) {
    return invokeSuspendingFunction(Dispatchers.getUnconfined(), method, target, args);
}

์ด ํ•จ์ˆ˜๋ฅผ ๋ณด๋ฉด Dispatcher.Unconfined๋ฅผ ๊ธฐ๋ณธ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ฒŒ ๋œ๋‹ค.

 

์ฆ‰, ๊ฐ™์€ ์Šค๋ ˆ๋“œ์—์„œ ์ฝ”๋ฃจํ‹ด์ด ์‹คํ–‰๋œ๋‹ค๋Š” ๊ฒƒ์ด๊ณ , ์ด๋Š” ์Šค๋ ˆ๋“œ ๋กœ์ปฌ ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ๋“ค๊ณ  ์˜จ๋‹ค๋Š” ๋ง๋กœ ์ด์–ด์ง„๋‹ค.

๊ทธ๋Ÿฌ๋ฏ€๋กœ ๋ณ„๋‹ค๋ฅธ ์ž‘์—… ์—†์ด MDC ๊ฐ’์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด๋‹ค.

 

์œ„์™€ ๊ฐ™์ด ๋‹ค๋ฅธ Dispatcher๋กœ์˜ ๋ณ€๊ฒฝ์ด ์—†๋‹ค๋ฉด, MDC ๊ด€๋ จ ๋กœ์ง์ด ์ถ”๊ฐ€๋  ํ•„์š” ์—†์„ ๊ฒƒ์ด๋‹ค.

ํ•˜์ง€๋งŒ Dispatcher๋ฅผ ๋ฐ”๊พธ๊ฒŒ ๋œ๋‹ค๋ฉด, ์ถ”๊ฐ€์ ์ธ ๋กœ์ง์ด ํ•„์š”ํ•  ๊ฒƒ ๊ฐ™๋‹ค.

 

๐Ÿ“Œ MDC Logging

MDC์„ ๋กœ๊น…์— ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ์–ด๋ ต์ง€ ์•Š๋‹ค.

%X{๊ฐ’ ์ด๋ฆ„} ํ˜•์‹์œผ๋กœ ํŒจํ„ด์— ์ ์–ด์ฃผ๋ฉด MDC ๊ฐ’์ด ์ถœ๋ ฅ๋œ๋‹ค.

 

<property name="STDOUT_LOG_PATTERN"
              value="%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) [%25.25thread] %clr([traceId=%X{traceId}]){faint} %clr(---){faint} %clr(${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>${STDOUT_LOG_PATTERN}</pattern>
    </encoder>
</appender>

<springProfile name="default, dev">
    <root level="${logLevel}">
        <appender-ref ref="STDOUT"/>
    </root>
</springProfile>

SUSU ์„œ๋น„์Šค์—์„œ๋Š” MDC ๊ฐ’์„ ๋กœ๊ทธ์— ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด ์ปค์Šคํ…€ ๋กœ๊ทธ ํŒจํ„ด์„ ์‚ฌ์šฉํ•œ๋‹ค.

STDOUT_LOG_PATTER ์ด ์ปค์Šคํ…€ ๋กœ๊ทธ ํŒจํ„ด์ด๋‹ค

org/springframework/boot/logging/logback/defaults.xml ์— ์œ„์น˜ํ•˜๋Š” CONSOLE_LOG_PATTERN์—์„œ ์กฐ๊ธˆ ๋ณ€ํ˜•ํ•œ ๊ตฌ์กฐ์ด๋‹ค.

 

์œ„๋ฅผ ์ด์šฉํ•˜์—ฌ ๋กœ์ง์„ ์ถœ๋ ฅํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ถœ๋ ฅ๋œ๋‹ค.

 

๐Ÿ“Œ ๋งˆ๋ฌด๋ฆฌ

ํ•˜์ง€๋งŒ ์•„์ง๋„ ๋ฏธํกํ•œ ๋ถ€๋ถ„์ด ๋‚จ์•„์žˆ๋‹ค.

2024-05-10T23:35:11.682 DEBUG [       reactor-http-nio-5] [traceId=] --- o.s.w.s.adapter.HttpWebHandlerAdapter    : [c0d73824-2] HTTP GET "/api/v1/users/my-info"
2024-05-10T23:35:11.704 DEBUG [efaultDispatcher-worker-6] [traceId=7618fd85-b1e1-4b62-a968-34d944a9ee7c] --- o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [com.oksusu.susu.api.log.application.SystemActionLogService.record]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

... ์ƒ๋žต

2024-05-10T23:35:11.847 DEBUG [efaultDispatcher-worker-6] [traceId=7618fd85-b1e1-4b62-a968-34d944a9ee7c] --- o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(921624389<open>)] after transaction
2024-05-10T23:35:11.853 DEBUG [       reactor-http-nio-5] [traceId=7618fd85-b1e1-4b62-a968-34d944a9ee7c] --- o.s.w.s.adapter.HttpWebHandlerAdapter    : [c0d73824-2] Completed 200 OK

๋งจ ์ฒซ ์ค„์— traceId๊ฐ€ ์•ˆ ์ฐํžŒ๋‹ค.

์ด์œ ๋Š” ๊ฐ„๋‹จํ•˜๋‹ค.

ํ•„ํ„ฐ๋ณด๋‹ค ์•ž์—์„œ ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

requestId๊ฐ€ ๋กœ๊ทธ์— ๋‚จ๊ธฐ ๋•Œ๋ฌธ์—, ์–ด๋–ค ์š”์ฒญ์ธ์ง€ ์‹๋ณ„์€ ๊ฐ€๋Šฅํ•˜๋‹ค.

 

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ด์•ผ๊ฒ ๋‹ค.

 

๐Ÿ“Œ ๊ฐœ์„  1. HttpHandler์—์„œ MDC ๊ฐ’ ์ฃผ์ž…

๋ฐฉ๋ฒ•์„ ์ฐพ๋‹ค๊ฐ€ ์•„๋ž˜ ๊ธ€์„ ๋ดค๋‹ค

 

WebFlux Log Tracing

WebFlux๋ฅผ ์ฒ˜์Œ ์‚ฌ์šฉํ•ด๋ณด๋ฉด์„œ ๋กœ๊น…ํ•˜๋Š”๋ฐ ๋ฉฐ์น ์„ ์• ๋จน์—ˆ๋‹ค. Spring MVC์™€๋Š” ๋‹ค๋ฅด๊ฒŒ ๋ ˆํผ๋Ÿฐ์‹ฑํ•  ์ˆ˜ ์žˆ๋Š” ์ž๋ฃŒ๋“ค์ด ๊ฑฐ์˜ ์—†์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋‹ค๋ฅธ ๋ถ„๋“ค์˜ ์‹œ๊ฐ„์„ ๋‹จ์ถ•์‹œ์ผœ์ฃผ๊ณ ์ž ๊ธ€์„ ์ ์–ด๋ณธ๋‹ค. (์ข€ ๋” ์ข‹

mjin1220.tistory.com

HttpHandlerDecoratorFactory๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

HttpHandlerDecoratorFactory๋Š” ์„ค๋ช…์— 'Contract for applying a decorator to an HttpHandler'๋ผ๊ณ  ์“ฐ์—ฌ์žˆ๋Š”๋ฐ, ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด HttpHandler๋ฅผ ๋ณ€ํ˜•ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋ง์ด๋‹ค.

HttpHandler์—์„œ MDC ๊ฐ’์„ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ฐ”๊พธ๋ฉด ์œ„ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐ ๊ฐ€๋Šฅํ• ๊ฑฐ๋ผ ํŒ๋‹จํ–ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ฐ”๊ฟจ๋‹ค.

@Component
class MdcHttpHandlerDecoratorFactory: HttpHandlerDecoratorFactory {
    val logger = KotlinLogging.logger { }
    override fun apply(httpHandler: HttpHandler): HttpHandler {
        return HttpHandler { request, response ->
            val uuid = UUID.randomUUID().toString()
            try {
                MDC.put(TRACE_ID, uuid)
                httpHandler.handle(request, response)
                    .contextWrite { it.insert(TRACE_ID, uuid) }
            } finally {
                MDC.clear()
            }

        }
    }
}

์ฒ˜์Œ์—” MDC๋ฅผ ์œ„์—์„œ ์‚ฌ์šฉํ–ˆ๋˜ createPutWithMDC๋ฅผ ์ด์šฉํ•˜์—ฌ ์ฃผ์ž…ํ•ด ์คฌ๋‹ค.

ํ•˜์ง€๋งŒ ๋ฌธ์ œ๋Š” ์—ฌ์ „ํ–ˆ๋‹ค.

์ด์œ ๋Š” httpHandler.handle์˜ ๊ตฌํ˜„์„ ๋ณด๋ฉด ์•Œ ์ˆ˜ ์žˆ๋‹ค.

@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
	... // ์ƒ๋žต
    LogFormatUtils.traceDebug(logger, traceOn ->
            exchange.getLogPrefix() + formatRequest(exchange.getRequest()) +
                    (traceOn ? ", headers=" + formatHeaders(exchange.getRequest().getHeaders()) : ""));
	... // ์ƒ๋žต
}

ํ•ด๋‹น ํ•จ์ˆ˜์˜ ์ค‘๊ฐ„์— log๋ฅผ ๋‚จ๊ธฐ๋„๋ก ๋˜์–ด์žˆ๋‹ค.

๊ทธ๋Ÿฌ๋ฏ€๋กœ createPutWithMDC์—์„œ MDC ๊ฐ’์„ ์„ค์ •ํ•˜๋”๋ผ๋„, ์œ„์—์„œ ๋‚จ๊ธฐ๋Š” ๋กœ๊ทธ์—๋Š” traceId๊ฐ€ ๋‚จ์ง€ ์•Š๋Š”๋‹ค.

๊ทธ๋ž˜์„œ MDC ๊ฐ’์„ ๋ฏธ๋ฆฌ ๋„ฃ์–ด์คฌ๊ณ , handle ์ดํ›„์— MDC ๊ฐ’์„ context์— ๋„ฃ์–ด์ฃผ๋Š” ์ž‘์—…๋งŒ ํ•ด์คฌ๋‹ค.

์•„๋ž˜๋Š” insert์˜ ์ฝ”๋“œ์ด๋‹ค.

fun Context.insert(key: String, value: String): Context {
    val mapOfContext = this.stream()
        .map { ctx -> ctx.key to ctx.value }
        .toList().toMap().toMutableMap()
    mapOfContext[key] = value
    return Context.of(mapOfContext)
}

 

์œ„ ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  webfilter๋ฅผ ์‚ญ์ œํ–ˆ๋‹ค.

๊ฒฐ๊ณผ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

2024-05-20T21:19:01.607 DEBUG [       reactor-http-nio-3] [traceId=2342c601-a4a6-4f9c-81d9-64bc877d28ca] --- o.s.w.s.adapter.HttpWebHandlerAdapter    : [3f51cbb7-1] HTTP GET "..."
2024-05-20T21:19:01.735 DEBUG [       reactor-http-nio-3] [traceId=2342c601-a4a6-4f9c-81d9-64bc877d28ca] --- o.s.w.s.adapter.HttpWebHandlerAdapter    : [3f51cbb7-1] Completed 200 OK

์ž˜ ๋‚˜์˜จ๋‹ค.

 

๐Ÿ“Œ ๊ฐœ์„  2. parZip MDC ๊ฐ’ ํ‘œ์‹œํ•˜๋„๋ก ํ•˜๊ธฐ

parZip์„ ์‹คํ–‰์‹œํ‚ค๋ฉด traceId๊ฐ€ ์ฐํžˆ์ง€ ์•Š๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ๋‹ค.

 

ํ‰์†Œ parZip์„ ์•„๋ž˜์™€ ๊ฐ™์ด ์ด์šฉํ–ˆ์—ˆ๋‹ค.

parZip(
    { voteHistoryService.findByUidAndPostId(user.uid, postId) },
    { voteOptionService.validateCorrespondWithVote(postId, request.optionId) }
) { history, _ -> 
    ...
}

์ด๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ parZip ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰์‹œํ‚จ๋‹ค

public suspend inline fun <A, B, C> parZip(
  crossinline fa: suspend CoroutineScope.() -> A,
  crossinline fb: suspend CoroutineScope.() -> B,
  crossinline f: suspend CoroutineScope.(A, B) -> C
): C = parZip(Dispatchers.Default, fa, fb, f)

์ด ํ•จ์ˆ˜๋ฅผ ํƒ€๊ณ  ๋“ค์–ด๊ฐ€๋ณด๋ฉด, ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

public suspend inline fun <A, B, C> parZip(
  ctx: CoroutineContext = EmptyCoroutineContext,
  crossinline fa: suspend CoroutineScope.() -> A,
  crossinline fb: suspend CoroutineScope.() -> B,
  crossinline f: suspend CoroutineScope.(A, B) -> C
): C = coroutineScope {
  val faa = async(ctx) { fa() }
  val fbb = async(ctx) { fb() }
  val (a, b) = awaitAll(faa, fbb)
  f(a as A, b as B)
}

์ธ์ž๋กœ ์ฃผ์–ด์ง„ suspend function์„ async๋กœ ์‹คํ–‰์‹œํ‚จ๋‹ค.

๋‹ค๋ฅธ ์ฝ”๋ฃจํ‹ด์„ ์‹คํ–‰์‹œํ‚จ๋‹ค๋Š” ๋œป์ด๋‹ค.

 

๊ทธ๋ ‡๋‹ค๋ฉด traceId๊ฐ€ ์ฐํžˆ์ง€ ์•Š๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์ผ๊นŒ?

์ด์— ๋Œ€ํ•œ ์›์ธ์€ ๊ฐ„๋‹จํ•˜๋‹ค.

๋‹ค๋ฅธ ์ฝ”๋ฃจํ‹ด ์‹คํ–‰์‹œ, MDCContext๊ฐ€ ์ „๋‹ฌ๋˜์ง€ ์•Š์•˜๊ณ , ์ด ๋•Œ๋ฌธ์— MDC์—๋Š” traceId ๊ฐ’์ด ์กด์žฌํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๊ทธ๋ž˜์„œ traceId๋Š” ์ฐํžˆ์ง€ ์•Š๋Š”๋‹ค.

 

์ด๋ฅผ ๊ฐ„๋‹จํžˆ ์ปค์Šคํ…€ํ•˜์—ฌ ํ•ด๊ฒฐํ–ˆ๋‹ค.

MDCContext๊ฐ€ ์ „๋‹ฌ๋˜์ง€ ์•Š๋Š” ๊ฒŒ ๋ฌธ์ œ์˜€์œผ๋‹ˆ, ์ „๋‹ฌํ•˜๋ฉด ํ•ด๊ฒฐ๋œ๋‹ค.

suspend inline fun <A, B, C> parZipWithMDC(
    crossinline fa: suspend CoroutineScope.() -> A,
    crossinline fb: suspend CoroutineScope.() -> B,
    crossinline f: suspend CoroutineScope.(A, B) -> C,
): C {
    val ctx = Dispatchers.Default + MDCContext()
    return parZip(ctx, fa, fb, f)
}

์œ„์—์„œ ๋ณธ ์ผ๋ฐ˜์ ์ธ parZip๊ณผ ์œ ์‚ฌํ•˜๋‹ค.

์ฐจ์ด์ ์€ context์— MDCContext๋„ ๋„˜๊ฒจ์ฃผ๋Š” ๊ฒƒ์ด๋‹ค.

 

์ด๋ ‡๊ฒŒ ๋  ๊ฒฝ์šฐ, MDCContext๊ฐ€ ๋‹ค๋ฅธ ์ฝ”๋ฃจํ‹ด์—์„œ๋„ MDC์— ๊ฐ’์„ ๋„ฃ์–ด์ค„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, traceId๋ฅผ ๋กœ๊น…ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๐Ÿ“Œ ๊ฐœ์„  3. Coroutine ๋‚ด๋ถ€ MDC ์ „ํŒŒ

๐Ÿ”ต CoWebFilter

@Component
class CoMdcFilter : CoWebFilter() {
    override suspend fun filter(exchange: ServerWebExchange, chain: CoWebFilterChain) {
        withContext(Dispatchers.IO) {
            chain.filter(exchange)
        }
    }
}

CoWebfilter๋Š” WebFilter๋ฅผ ์ƒ์†ํ•œ๋‹ค.

์ด์— ํ•„ํ„ฐ ์ฒด์ธ์— ์†ํ•ด์„œ ์ž‘๋™ํ•˜๊ฒŒ ๋œ๋‹ค.

ํ•˜์ง€๋งŒ ์ฐจ์ด์ ์ด ์กด์žฌํ•œ๋‹ค. 

final override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
    val context = exchange.attributes[COROUTINE_CONTEXT_ATTRIBUTE] as CoroutineContext?
    return mono(context ?: Dispatchers.Unconfined) {
        filter(exchange, object : CoWebFilterChain {
            override suspend fun filter(exchange: ServerWebExchange) {
                exchange.attributes[COROUTINE_CONTEXT_ATTRIBUTE] = currentCoroutineContext().minusKey(Job.Key)
                chain.filter(exchange).awaitSingleOrNull()
            }
        })}.then()
}

protected abstract suspend fun filter(exchange: ServerWebExchange, chain: CoWebFilterChain)

์ด๋Š” CoWebFilter์˜ ํ•จ์ˆ˜์ด๋‹ค.

์ž์„ธํžˆ ๋ณด๋ฉด CoWebFilter์˜ ์ž‘๋™ํ•  ๋•Œ, ํ˜„์žฌ CoroutineContext๋ฅผ exchange์— ๋„ฃ์–ด์ค€๋‹ค.

์ด ๊ฐ’์€ ์œ„์—์„œ ์„ค๋ช…ํ–ˆ๋˜ Suspend ํ•จ์ˆ˜์ธ์ง€ ๊ฒ€์‚ฌํ•˜๋Š” ๋ถ€๋ถ„์—์„œ ์“ฐ์ธ๋‹ค.

CoWebFilter๋ฅผ ํ†ตํ•ด Context๋ฅผ ์ง€์ •ํ•ด ์ฃผ๋ฉด, ๊ฐ’์ด null์ด ์•„๋‹ˆ๋ฏ€๋กœ ์ด์—์„œ ์ง€์ •ํ•œ Context๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Coroutine์ด ์ž‘๋™ํ•˜๊ฒŒ ๋œ๋‹ค.

 

๐Ÿ”ต ๋ฌธ์ œ์ 

์œ„์—์„œ ์ œ์‹œํ•œ CoWebFilter์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•  ๊ฒฝ์šฐ, Coroutine์ด ์ฒ˜์Œ ์‹œ์ž‘๋  ๋•Œ Dispatcher.IO๋กœ ์‹œ์ž‘ํ•˜๊ฒŒ ๋œ๋‹ค.

๋˜ํ•œ ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ๊ฒฝ์šฐ, ๊ฒฐ๊ด๊ฐ’์€ ์•„๋ž˜ ์‚ฌ์ง„๊ณผ ๊ฐ™๋‹ค.

suspend fun getAllVotes(
    user: AuthUser,
    searchRequest: SearchVoteRequest,
    pageRequest: SusuPageRequest,
): Slice<VoteAndOptionsWithCountResponse> {
    logger.info { "1" }
    val userAndPostBlockIdModel = blockService.getUserAndPostBlockTargetIds(user.uid)

    logger.info { "7" }
    ...
}

suspend fun getUserAndPostBlockTargetIds(uid: Long): UserAndPostBlockIdModel {
    logger.info { "2" }
    val blocks = findAllByUid(uid)
    logger.info { "6" }
    ...
}

suspend fun findAllByUid(uid: Long): List<UserBlock> {
    logger.info { "3" }
    return withMDCContext(Dispatchers.IO) {
        logger.info { "4" }
        val a = userBlockRepository.findAllByUid(uid)
        logger.info { "5" }
        a
    }
}

traceId๊ฐ€ ์—†๋‹ค.

Coroutine์„ ํ†ตํ•ด ์Šค๋ ˆ๋“œ๊ฐ€ ์ „ํ™˜๋˜์—ˆ์ง€๋งŒ, ์ „ํ™˜๋œ ์Šค๋ ˆ๋“œ์—์„œ MDC ๊ฐ’์„ ๋„ฃ์–ด์ฃผ์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ด ๊ฒฝ์šฐ๋Š” Unconfined๋กœ ์„ค์ •ํ–ˆ๋‹ค๋ฉด ๋ณผ ์ˆ˜ ์—†์„ ๊ฒƒ์ด๋‹ค.

 

๋˜ํ•œ ์•„๋ž˜์™€ ๊ฐ™์€ ์ƒํ™ฉ์—์„œ ์ด์™€ ๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

์ž์‹ Coroutine์ด ๋๋‚˜์„œ ๋ถ€๋ชจ Coroutine์œผ๋กœ ๋Œ์•„์™”์ง€๋งŒ, MDC ๊ฐ’์ด ์ดˆ๊ธฐํ™”๋˜์–ด traceId๊ฐ€ ์ฐํžˆ์ง€ ์•Š๋Š”๋‹ค.

์ด ๋’ค๋กœ ๋๊นŒ์ง€ traceId๋Š” ์ฐํžˆ์ง€ ์•Š์•˜๋‹ค.

์ด ๋˜ํ•œ ์›์ธ์€ ๊ฐ™์•˜๋‹ค.

์ด ๋ถ€๋ถ„์€ Dispatcher์— ์ƒ๊ด€์—†์ด ๋ฐœ์ƒํ–ˆ๋‹ค.

 

๐Ÿ”ต ์‚ฝ์งˆ

MDCContext๊ฐ€ Coroutine์˜ ์ค‘๋‹จ, ์‹คํ–‰ ์‹œ MDC ๊ฐ’์„ ์œ ์ง€ํ•ด ์ค„ ์ˆ˜ ์žˆ๋‹ค.

์ด๋ฅผ ์ฝ”๋ฃจํ‹ด์ด ์ฒ˜์Œ ์ƒ์„ฑ๋  ๋•Œ, Context์— ๋„ฃ์–ด์ฃผ๋ฉด ๋˜์ง€ ์•Š์„๊นŒ ์ƒ๊ฐํ–ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌ์„ฑํ–ˆ๋‹ค.

class CoMdcFilter : CoWebFilter() {
    override suspend fun filter(exchange: ServerWebExchange, chain: CoWebFilterChain) {
        withContext(Dispatchers.Unconfined + MDCContext()){
            chain.filter(exchange)
        }
    }
}

Dispatcher.Unconfined๋กœ ํ•ด๋‘” ์ด์œ ๋Š”, blocking ๋กœ์ง์ด ์•„๋‹ˆ๋ผ์„œ ๋ฐ”๊ฟ€ ๊ธฐ์กด ๋””ํดํŠธ ๊ฐ’์„ ๋ฐ”๊ฟ€ ํ•„์š”๊ฐ€ ์—†๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.

๊ทธ๋ž˜์„œ Context๋ฅผ Dispatcher.Unconfined + MDCContext() ์กฐํ•ฉ์œผ๋กœ ๊ตฌ์„ฑํ–ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด๋Š” ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š์•˜๋‹ค.

๊ทธ ์ด์œ ๋Š” MDCContext์˜ ๋™์ž‘ ๋ฐฉ์‹์— ์กด์žฌํ•œ๋‹ค.

MDCContext๋Š” ์ƒ์„ฑ ์‹œ context๋ฅผ ์ง€์ •ํ•ด์ฃผ์ง€ ์•Š์„ ๊ฒฝ์šฐ, MDC.getCopyOfContextMap() ๊ฐ’์„ ๊ฐ€์ง„๋‹ค.

์ฝ”๋ฃจํ‹ด์ด ์‹œ์ž‘ํ•  ๋•Œ, ์ด ๊ฐ’์€ Null์ด๋‹ค. 

CoWebFilter์—์„œ MDCContext()๋ฅผ ์ดˆ๊ธฐํ™”ํ•  ๋•Œ, ๋“ค์–ด๊ฐ„ ๊ฐ’์ด Null์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๊ทธ๋Ÿฌ๋ฏ€๋กœ Coroutine์ด ๋‹ค์‹œ ์‹œ์ž‘๋˜๋ฉฐ MDC ๊ฐ’์ด ์ดˆ๊ธฐํ™”๋  ์ˆ˜ ์—†๋‹ค.

 

๊ฒฐ๋ก ์ ์œผ๋กœ, CoWebFilter์—์„œ Context์— MDCContext()๋ฅผ ๋„ฃ๋Š” ๋ฐฉ์‹์€ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

๐Ÿ”ต ํ•ด๊ฒฐ

Coroutine์ด resumeWith ๋  ๋•Œ, ์ฆ‰ Coroutine์ด ์ˆ˜ํ–‰๋  ๋•Œ๋งˆ๋‹ค MDC ๊ฐ’์„ ์ฃผ์ž…ํ•ด ์ฃผ๋ฉด ๋  ๊ฑฐ๋ผ ์ƒ๊ฐํ–ˆ๋‹ค.

์ด๋ฅผ ContinuationInterceptor๋ฅผ ์ปค์Šคํ…€ํ•˜์—ฌ ๊ตฌํ˜„ํ–ˆ๋‹ค.

class MdcContinuationInterceptor(private val dispatcher: CoroutineDispatcher) : ContinuationInterceptor {
    override val key: CoroutineContext.Key<*>
        get() = ContinuationInterceptor

    override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
        return MdcContinuation(dispatcher.interceptContinuation(continuation))
    }
}

class MdcContinuation<T>(private val continuation: Continuation<T>) : Continuation<T> {
    val logger = KotlinLogging.logger {  }

    override val context: CoroutineContext
        get() = continuation.context

    override fun resumeWith(result: Result<T>) {
    	logger.info { "resume" }
        continuation.context[ReactorContext]?.context?.get<String>(MDC_KEY_TRACE_ID)?.run {
            MDC.put(MDC_KEY_TRACE_ID, this)
        }
        continuation.resumeWith(result)
    }
}

 

์ด์— MDC ๊ฐ’์„ ๋„ฃ์–ด์ฃผ๋Š” ๋กœ์ง์ด ํ•„์š”ํ•˜๋‹ค ์ƒ๊ฐํ–ˆ๋‹ค.

Dispatcher์˜ Continuation์ด resumeWith ๋  ๋•Œ๋งˆ๋‹ค Context์— ์žˆ๋Š” traceId ๊ฐ’์„ MDC์— ๋„ฃ์–ด์ฃผ๋„๋ก ํ–ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์ด๋ฅผ CoWebFilter์— ์ ์šฉํ–ˆ๋‹ค. 

@Component
class CoMdcFilter : CoWebFilter() {
    override suspend fun filter(exchange: ServerWebExchange, chain: CoWebFilterChain) {
        withContext( MdcContinuationInterceptor(Dispatchers.Unconfined)) {
            chain.filter(exchange)
        }
    }
}

์ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ๋กœ๊ทธ๊ฐ€ ๋‚จ๋Š”๋‹ค.

์ฝ”๋ฃจํ‹ด์ด ์‹คํ–‰๋˜๋ฉฐ resumeWith์ด ์ˆ˜ํ–‰๋˜๊ณ , ์ด์™€ ๋™์‹œ์— MDC ๊ฐ’์ด ์ฃผ์ž…๋ผ์„œ traceId๊ฐ€ ํ‘œ์‹œ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

์ด๋ฅผ ํ†ตํ•ด, Coroutine์— ๊ณ„์† MDC ๊ฐ’์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‚ด๋ถ€ MDCContext์—๋„ ๊ฐ’์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.