-
๐ MDC๋?
-
๐ Webflux์ MDC
-
๐ต MDC ์ค์
-
๐ต ์ค๋ ๋ ๊ฐ ์ ํ
-
๐ Coroutine๊ณผ MDC
-
๐ต Coroutine์ MDC ์ ์ฉ
-
๐ต Reactor์ Coroutine
-
๐ MDC Logging
-
๐ ๋ง๋ฌด๋ฆฌ
-
๐ ๊ฐ์ 1. HttpHandler์์ MDC ๊ฐ ์ฃผ์
-
๐ ๊ฐ์ 2. parZip MDC ๊ฐ ํ์ํ๋๋ก ํ๊ธฐ
-
๐ ๊ฐ์ 3. Coroutine ๋ด๋ถ MDC ์ ํ
-
๐ต CoWebFilter
-
๐ต ๋ฌธ์ ์
-
๐ต ์ฝ์ง
-
๐ต ํด๊ฒฐ
๐ 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์ผ๋ก ์ ํ๋๋ ์์ธํ ๋ด์ฉ์ ์๋ ๊ธ์ ์ฐธ๊ณ ํด ์ฃผ์ธ์
ํ์ง๋ง ์ฌ๊ธฐ์ ์๋ฌธ์ด ์๋ค.
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์๋ ๊ฐ์ ์ ์งํ ์ ์๋๋ก ํ ์ ์์๋ค.
'Backend' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
<Spring> Webflux + Coroutine vs MVC (0) | 2024.06.12 |
---|---|
<Spring> SUSU์ Coroutine (0) | 2024.05.11 |
<Spring> WARN ๋ ๋ฒจ ์ด์ ๋ก๊ทธ Slack ์๋ฆผ ๋ณด๋ด๊ธฐ (0) | 2024.03.17 |
<Spring> Spring Bean, IoC/DI ์ ๋ฆฌ 2 (0) | 2023.10.03 |
<Spring> Spring Bean, IoC/DI ์ ๋ฆฌ 1 (0) | 2023.10.03 |
๐ 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์ผ๋ก ์ ํ๋๋ ์์ธํ ๋ด์ฉ์ ์๋ ๊ธ์ ์ฐธ๊ณ ํด ์ฃผ์ธ์
ํ์ง๋ง ์ฌ๊ธฐ์ ์๋ฌธ์ด ์๋ค.
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์๋ ๊ฐ์ ์ ์งํ ์ ์๋๋ก ํ ์ ์์๋ค.
'Backend' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
<Spring> Webflux + Coroutine vs MVC (0) | 2024.06.12 |
---|---|
<Spring> SUSU์ Coroutine (0) | 2024.05.11 |
<Spring> WARN ๋ ๋ฒจ ์ด์ ๋ก๊ทธ Slack ์๋ฆผ ๋ณด๋ด๊ธฐ (0) | 2024.03.17 |
<Spring> Spring Bean, IoC/DI ์ ๋ฆฌ 2 (0) | 2023.10.03 |
<Spring> Spring Bean, IoC/DI ์ ๋ฆฌ 1 (0) | 2023.10.03 |