Spring WebFlux ๊ธฐ๋ฐ MDC ์ ์ฉํ๊ธฐ (+์ฝ๋ฃจํด)
MDC ๋?
MDC ๋ Mapped Diagnostic Context ์ ์ฝ์๋ก key์ value๋ฅผ ์ ์ฅํ๋ Map ํ์์ผ๋ก slf4j, logback, log4j ๋ฑ ๋ก๊น ํ๋ ์์ํฌ์์ ํ์ํ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ณ ์ถ์ ๋ ์ฌ์ฉํ๋ค.
ThreadLocal ์ ๊ธฐ๋ฐ์ผ๋ก ์ปจํ ์คํธ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ธฐ ๋๋ฌธ์ ๋ฉํฐ ์ฐ๋ ๋ ํ๊ฒฝ์์ ์ ์ฉํ๊ฒ ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค. (๋ฉํฐ ์ฐ๋ ๋ ํ๊ฒฝ์์๋ ๋ก๊ทธ ๋ฉ์์ง์ ๊ฐ ์ฐ๋ ๋์ ๋ํ ๋ก๊ทธ๊ฐ ์์ด๊ธฐ ๋๋ฌธ)
์์ฒญ์ด ์ด๋ค ์ฌ์ฉ์๋ก๋ถํฐ ๋ค์ด์จ ๊ฒ์ธ์ง ์ ์ฅํ๊ฑฐ๋ ์์ฒญ๋ณ๋ก ์๋ณ์๋ฅผ ๋ง๋ค์ด ํน์ ์์ฒญ์ ๋ํ ๋ก๊ทธ๋ง ์ถ์ ํ ์ ์๋ค.
์ฐธ๊ณ :
๋ก๊ทธ์์คํ #4-MDC๋ฅผ ์ด์ฉํ์ฌ ์ฐ๋ ๋๋ณ๋ก ๋ก๊ทธ ๋ถ๋ฅํ๊ธฐ
MDC ์ ์ฉํ๊ธฐ
Spring Framework๋ฅผ ์ฌ์ฉํ๊ณ ์๋ค๋ฉด AOP๋ฅผ ์ด์ฉํด ์ฝ๊ฒ ์ ์ฉ์ด ๊ฐ๋ฅํ๋ค.
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-aop")
์์ฒญ๋ณ๋ก ์๋ณ์(traceId)๋ฅผ ์ ์ฅํ๊ณ ์๋ณ์๋ฅผ ํตํด ํน์ ์์ฒญ์ ๋ํ ๋ก๊ทธ๋ง ๋ชจ์๋ณผ ์ ์๋ค.
์์ฒญ์ ๋ํ ๊ณตํต์ ์ฒ๋ฆฌ๋ฅผ ์ํ๋ค๋ฉด ์๋์ ๊ฐ์ด AOP๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ Interceptor, Filter ๋ฑ์ ์ฌ์ฉํด ๊ตฌํ์ด ๊ฐ๋ฅํ๋ค.
@Aspect
@Component
class MdcAspect {
@Around("@annotation(RequestMapping)")
fun setMdcTraceId(joinPoint: ProceedingJoinPoint): Any? {
try {
val traceId = UUID.randomUUID().toString()
MDC.put("traceId", traceId)
return joinPoint.proceed()
} finally {
MDC.remove("traceId")
}
}
์๋์ ๊ฐ์ด ๋ก๊ทธ ์ค์ ํ์ผ์ MDC์ ์ ์ฅํ key(traceId)๋ฅผ ์ฐ์ด๋ณผ ์ ์๋ค.
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [traceId: %X{traceId}] - %msg%n</pattern>
๋น๋๊ธฐ ํ๊ฒฝ์์๋ ์ด๋ป๊ฒ ์ ์ฉํ ์ ์์๊น?
ํ์ง๋ง ๋น๋๊ธฐ ํ๊ฒฝ์ด๋ผ๋ฉด ์กฐ๊ธ ๋ ๊น๋ค๋ก์์ง๋ค.
ThreadLocal์์ ๋์ํ๋ MDC ํน์ฑ์ ํ๋์ ์ฐ๋ ๋์์ ์ฌ๋ฌ๊ฐ์ ์์ฒญ์ ์ฒ๋ฆฌํ ์ ์๋ ๋น๋๊ธฐ ํ๊ฒฝ์ด๋ผ๋ฉด MDC ์ ๋ณด๊ฐ ์ ์ง๋์ง ์์ ์ ์๋ค.
์ฆ, ํ๋์ ์์ฒญ์ ์ฒ๋ฆฌํ ๋ ์ฐ๋ ๋๊ฐ ๋ฐ๋๊ฒ ๋๋ฉด MDC ์ ๋ณด๊ฐ ์ ์ง๋์ง ์์ ์ ์๋ค.
ํ์ฌ ํ๋ด์์ Spring WebFlux์ Armeria, Coroutine์ ์ฌ์ฉํด ๋น๋๊ธฐ ํ๊ฒฝ์ ์๋ฒ๋ฅผ ๊ตฌํํ๊ณ ์๊ธฐ ๋๋ฌธ์ ์ ๋๋ก ๋ ๋ก๊ทธ ์ถ์ ์ ์ํด์๋ ์ด์ ๋ํ ์ฒ๋ฆฌ๊ฐ ํ์ํ๋ค.
์ฝ๋ฃจํด ์ค์ฝํ ๋ด๋ถ์ ์ค๋ ๋์๋ MDC ์ ๋ณด๋ฅผ ๋๊ฒจ์ฃผ์ด์ผ ํ๊ณ ๋คํํ๋ ์ฝํ๋ฆฐ์ MDCContext๋ผ๋ ๋ก๊น ์ ์ํ ์ฝ๋ฃจํด MDC ์ปจํ ์คํธ๋ฅผ ์ ๊ณตํ๊ณ ์๋ค.
์ฝ๋ฃจํด ์ปจํ ์คํธ๊ฐ ์ ํ๋๋ ๋ถ๋ถ์ ์๋์ ๊ฐ์ด MDCContext๋ฅผ ์ถ๊ฐํ์ฌ ์ฝ๋ฃจํด์ผ๋ก ์ ๋ฌ๋๋๋ก ํ ์ ์๋ค.
withContext(Dispatchers.IO + MDCContext()){
...
}
๋ด๋ถ๋ฅผ ๋ค์ฌ๋ค๋ณด๋ฉด contextMap ์ด๋ผ๋ Map ํ์ ์ ์๋ฃํ ๋ณ์๋ฅผ ๊ฐ์ง๊ณ ์๋ค. ์ด ๋ณ์๊ฐ ์ด๊ธฐํ๋ ๋ ์ธ๋ถ์ MDC ์ปจํ ์คํธ๋ฅผ ๊ฐ์ ธ์ ์ ์ฅํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
ThreadContextElement ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๊ณ ์๊ธฐ ๋๋ฌธ์ ์ฝ๋ฃจํด ์ปจํ ์คํธ๋ก ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ฉฐ ThreadContext๊ฐ ๋ฐ๋๋๋ง๋ค MDC ์ปจํ ์คํธ๋ฅผ ๋ณต์ฌํด์ฃผ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
๋ง์ฝ ์ฝ๋ฃจํด ๊ธฐ๋ฐ์ด ์๋ Mono์ Flux ๊ธฐ๋ฐ ๋ฆฌ์กํฐ๋ธ ํ์ ์ ์ฌ์ฉํ๊ณ ์๋ค๋ฉด Reactor์์ ์ ๊ณตํ๋ Context๋ฅผ ์ฌ์ฉํด MDC๋ฅผ ๋ณต์ฌํ๋ ๋ฐฉ๋ฒ๋ ์กด์ฌํ๋ค.
์ฐธ๊ณ :
How can the MDC context be used in the reactive Spring applications | Novatec
Armeria ServiceRequestContext ์ด์ฉํ๊ธฐ
Armeria์์๋ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํด Spring AOP์ ๊ฐ์ด ์์ฒญ์ ์๋ค๋ก ๊ณตํต ๊ด์ฌ์ฌ๋ฅผ ๋ถ๋ฆฌํด ์ฝ๋๋ฅผ ์์ฑํด์ค ์ ์๋ค.
์ฌ๋ด์์ ์ฌ์ฉํ๊ณ ์๋ ์์ ์๋ฒ๋ค์ Spring Framework๋ฅผ ์ฌ์ฉํ๊ณ ์์ง ์๊ธฐ ๋๋ฌธ์ Armeria Decorator๋ฅผ ์ฌ์ฉํด API ์์ฒญ์ด ๋ค์ด์ฌ ๋ MDC์ traceId ์ ๋ณด๋ฅผ ์ ์ฅํด์ฃผ์๋ค.
Armeria๋ RequestContext๋ฅผ ์ ๊ณตํ๋ค. ํธ์ถํ๋ ๋ชจ๋ ์์ฒญ์ ๋ํด ๊ณ ์ ํ ์ปจํ ์คํธ๋ฅผ ๊ฐ์ง๋ฉฐ ์์ฒญ์ ๋ํ ๋ค์ํ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์๋ค. ์ด ์ปจํ ์คํธ์ id๋ ๊ณ ์ ํ๊ธฐ ๋๋ฌธ์ traceId ๋ก ์ ํฉํ๋ค.
ํ๋์ ์์ฒญ์ ๋ํ ์ค๋ ๋๊ฐ ๋ฐ๋๋๋ผ๋ ์ด ์ปจํ ์คํธ๋ ์ ์ง๋๊ธฐ ๋๋ฌธ์ ์ถ์ ์ด ๊ฐ๋ฅํ๋ค๋ ์ฅ์ ์ด ์กด์ฌํ๋ค.
const val TRACE_ID = "traceId"
class MdcDecoratingService(delegate: HttpService) : SimpleDecoratingHttpService(delegate) {
override fun serve(ctx: ServiceRequestContext, req: HttpRequest): HttpResponse {
MDC.put(TRACE_ID, ctx.id().toString())
val delegate = unwrap() as HttpService
return delegate.serve(ctx, req)
}
}
ํ์ง๋ง Armeria๋ MDC์ ์ง์ ๊ฐ์ ๋ฃ์ด์ฃผ์ง ์์๋ RequestContextExportingAppender ๋ผ๋ Logback Appender ๋ฅผ ์ ๊ณตํ๋ค. ์ด appender๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐ๋ก ์ฒ๋ฆฌ๋ฅผ ํด์ฃผ์ง ์์๋ ์์ ํ๊ฒ ๋ฒ์ฉ์ ์ผ๋ก ํด๋ผ์ด์ธํธ ์์ฒญ ์ ๋ณด๋ฅผ ์ถ์ ํ ์ ์๋ค.
Armeria์์ RequestContext๋ ์์์ ๋งํ๋ ๊ฒ์ฒ๋ผ ๋น๋๊ธฐํ๊ฒฝ์์๋ ์์ฒญ๋ณ๋ก ๊ณ ์ ํ ์ปจํ ์คํธ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์๊ธฐ ๋๋ฌธ์ ์ฐ๋ ๋๊ฐ ์์๋ก ๋ฐ๋๋ค ํ๋๋ผ๋ ๊ทธ๋๋ก ์์ฒญ ์ ๋ณด๋ฅผ ์ ์งํ ์ ์๋ค.
ํ์ฌ ์ฒ๋ฆฌ์ค์ธ ์์ฒญ์ ๋ฉํ๋ฐ์ดํฐ๋ค์ ์ถ์ถํ๊ฑฐ๋ ์ปค์คํ ํด์ ์ ๋ณด๋ค์ ์ ์ฅํ ์ ์๋ Armeria์์ ์์ฃผ ์ค์ํ ์ปดํฌ๋ํธ ์ค ํ๋๋ค.
RequestContextExportingAppender ๋ ํ์ฌ RequestContext ์ ๋ณด๋ฅผ MDC๋ก ๋ด๋ณด๋ด๋ ๊ฒ์ด ๊ฐ๋ฅํ appender์ด๋ค.
์๋์ ๊ฐ์ด ์์กด์ฑ์ ์ถ๊ฐํด์ค ํ,
dependencies {
implementation platform('com.linecorp.armeria:armeria-bom:1.24.3')
...
implementation 'com.linecorp.armeria:armeria-logback'
}
logback.xml์์ ํ์ํ ์์ฒญ ์ ๋ณด๋ฅผ exportํด ๋ก๊ทธ ๋ฉ์์ง๋ก ์ถ๋ ฅํ ์ ์๋ค.
<appender name="LOG_FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/logback_info.log</file>
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} %-5level --- [%thread][traceId=%X{traceId}] %logger{36} [%F:%M:%line] : %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
appender์์ ์์ฒญ๋ณ ๋ก๊ทธ ์ ๋ณด ์ถ์ ์ ์ํด req.id๋ฅผ exportํ์ฌ traceId๋ก ๋ก๊ทธ ๋ฉ์์ง์ ๊ณตํต์ ์ผ๋ก ์ถ๋ ฅํด์ฃผ์๋ค.
<appender name="RCEA" class="com.linecorp.armeria.common.logback.RequestContextExportingAppender">
<export>traceId=req.id</export>
<appender-ref ref="STDOUT" />
<appender-ref ref="LOG_FILE_INFO"/>
</appender>
์ด๋ ๊ฒ ํ๋ฉด ๊ตณ์ด ๋ฐ์ฝ๋ ์ดํฐ๋ AOP๋ก MDC์ ์ผ์ผ์ด traceId๋ฅผ ์ ์ฅํด์ฃผ์ง ์์๋ ๋ก๊ทธ์ ํ์ํ MDC ์ ๋ณด๋ฅผ ์ถ๋ ฅํ ์ ์๋ค.
๋ํ ๋น๋๊ธฐ ํ๊ฒฝ์์ ๋งค๋ฒ ์ปจํ ์คํธ๊ฐ ๋ฐ๋ ๋ MDC๋ฅผ ๊ด๋ฆฌํด์ค ํ์๋ ์์ด ์ถ๊ฐ์ ์ธ ์ถ์ ๋น์ฉ์ด ๋ค์ง ์๋๋ค.
์ฐธ๊ณ :
https://easywritten.com/post/using-mdc-with-armeria/
์์ฝ
1. armeria ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ๊ณ ์๋ค๋ฉด RequestContextExportingAppender ๋ฅผ ์ฌ์ฉํด๋ณด์.
2. aremria์ ๊ฐ์ ๋น๋๊ธฐ ์ง์ ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ๊ณ ์์ง ์๋ค๋ฉด WebFilter๋ฅผ ์ฌ์ฉํด ๋ชจ๋ ์์ฒญ์ MDC ์ ๋ณด๋ฅผ ์ ์งํ๋๋ก ์ ์ฉ
WebFilter์ ์ ์ฉํ ๋ก์ง์ ์๋ 2๊ฐ์ง ์ค ์ ํํ ์ ์๋ค.
1) Reactor ๊ธฐ๋ฐ : CoreSubscriber ๋ฅผ ๊ตฌํํด onNext ๋ฉ์๋์ MDC ์ ์ง๋๋๋ก ์ธํ
2) Coroutine ๊ธฐ๋ฐ : ThreadContextElement ๋ฅผ ๊ตฌํํด ์ปจํ ์คํธ์ MDC ๋ณต์ฌ ๋ก์ง ์ธํ OR MDCContext ์ฌ์ฉ