Skip to content

Commit 764edf8

Browse files
committed
Write Kafka Post " Kafka Interceptor로 MDC 로깅 분리하기 "
1 parent 499e662 commit 764edf8

5 files changed

+502
-97
lines changed

_posts/2025-04-23-SpringBoot-Logging-Filter.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
layout: post
33
title: " 2편. Spring Boot 요청 흐름 추적: Logging Filter와 traceId 적용기 "
4-
categories: [SpringBoot, Technology]
4+
categories: [SpringBoot]
55
author: devFancy
66
---
77
* content

_posts/2025-06-03-SpringBoot-Coupon-System-Redisson.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
layout: post
33
title: " 쿠폰 시스템 개선기: SETNX에서 Redisson RLock과 AOP를 활용한 분산락 적용 "
4-
categories: [SpringBoot, Technology]
4+
categories: [SpringBoot]
55
author: devFancy
66
---
77
* content

_posts/2025-06-07-SpringBoot-Monitoring-Prometheus-Grafana.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
layout: post
33
title: " 3편. Prometheus와 Grafana로 Spring Boot 기반 모니터링 대시보드 구축하기 "
4-
categories: [SpringBoot, Technology]
4+
categories: [SpringBoot]
55
author: devFancy
66
---
77
* content

_posts/2025-06-18-SpringBoot-Distributed-Tracing-With-MDC.md

Lines changed: 101 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
---
22
layout: post
3-
title: " MDC와 GlobalTraceId를 활용한 분산 추적 "
4-
categories: SpringBoot Technology
3+
title: " MDC와 GlobalTraceId를 활용한 분산 추적 "
4+
categories: SpringBoot
55
author: devFancy
66
---
7+
78
* content
89
{:toc}
910

@@ -15,7 +16,7 @@ author: devFancy
1516

1617
이때 각 시스템에 분산된 로그를 하나의 흐름으로 묶어 추적할 수 없다면, 장애 발생 시 원인을 파악하기 매우 어려워진다.
1718

18-
* 현재 진행 중인 개인 프로젝트(쿠폰 시스템)에서 쿠폰 발급 요청이
19+
* 현재 진행 중인 개인 프로젝트(쿠폰 시스템)에서 쿠폰 발급 요청이
1920

2021
API 서버에서 시작되어 카프카(Kafka)를 통해 컨슈머 서버로 전달되는 과정이 있다.
2122

@@ -36,7 +37,7 @@ author: devFancy
3637

3738
핵심 흐름은 다음과 같다.
3839

39-
* HTTP Request -> `쿠폰 API 서버` -> Kafka Produce -> Kafka Consume -> `컨슈머 서버` -> DB 저장
40+
* HTTP Request -> `쿠폰 API 서버` -> Kafka Producer -> Kafka Consumer -> `컨슈머 서버` -> DB 저장
4041

4142
이 구조에서 Spring Boot Actuator와 Micrometer Tracing 같은 라이브러리를 사용하면 각 애플리케이션 내에서는 `traceId``spanId`가 자동으로 생성되어 로그에 포함된다.
4243

@@ -73,7 +74,6 @@ author: devFancy
7374
21:29:03.488| INFO|6852b10febe4df79b383d66d36df8483,b383d66d36df8483|d.b.c.k.c.a.CouponIssueConsumer |쿠폰 발급 완료...
7475
```
7576

76-
7777
## 해결 방안: GlobalTraceId를 이용한 수동 전파
7878

7979
> GlobalTraceId를 개인 프로젝트에 적용한 부분과 관련된 코드는 깃허브 [PR](https://github.com/devFancy/springboot-coupon-system/pull/33) 에서 확인할 수 있다.
@@ -84,39 +84,41 @@ author: devFancy
8484

8585
* 참고) Micrometer의 자동 전파 기능과 `GlobalTraceId`의 차이점
8686

87-
* Spring Boot 3.x 환경에서 `micrometer-tracing-bridge-brave``micrometer-tracing-bridge-otel` 의존성을 추가하면,
88-
Micrometer가 자동으로 Kafka Producer와 Consumer를 계측하여 트레이스 컨텍스트(traceId, spanId)를 전파해 준다.
89-
Spring Boot 2.x에서는 Spring Cloud Sleuth가 이 역할을 했다.
87+
* Spring Boot 3.x 환경에서 `micrometer-tracing-bridge-brave``micrometer-tracing-bridge-otel` 의존성을 추가하면,
88+
Micrometer가 자동으로 Kafka Producer와 Consumer를 계측하여 트레이스 컨텍스트(traceId, spanId)를 전파해 준다.
89+
Spring Boot 2.x에서는 Spring Cloud Sleuth가 이 역할을 했다.
9090

91-
* 하지만 이 글에서 다루는 `GlobalTraceId`는 필자가 **직접 만든 커스텀 필드**이므로 이러한 자동 전파의 대상이 아니다.
92-
이처럼 라이브러리가 모르는 커스텀 식별자를 서비스 간에 전달해야 할 때는,
93-
이 글에서 소개한 것처럼 **직접 헤더에 담아 전달하는 수동 전파 방식**이 필요하다.
91+
* 하지만 이 글에서 다루는 `GlobalTraceId`는 필자가 **직접 만든 커스텀 필드**이므로 이러한 자동 전파의 대상이 아니다.
92+
이처럼 라이브러리가 모르는 커스텀 식별자를 서비스 간에 전달해야 할 때는,
93+
이 글에서 소개한 것처럼 **직접 헤더에 담아 전달하는 수동 전파 방식**이 필요하다.
9494

95-
* 이는 분산 추적의 핵심 원리를 이해하고 우리가 원하는 식별자를 직접 제어할 수 있다는 장점이 있다.
95+
* 이는 분산 추적의 핵심 원리를 이해하고 우리가 원하는 식별자를 직접 제어할 수 있다는 장점이 있다.
9696

9797
### 1. Logback 설정: GlobalTraceId 출력 필드 추가
9898

99-
먼저, 로그 패턴에 `GlobalTraceId`를 출력할 수 있도록 `logback-spring.xml` 설정에 globalTraceId 필드를 추가한다.
99+
먼저, 로그 패턴에 `GlobalTraceId`를 출력할 수 있도록 `logback-spring.xml` 설정에 globalTraceId 필드를 추가한다.
100100

101101
이 필드는 MDC에 해당 키가 존재할 경우 그 값을 출력한다.
102102

103103
> logback-local.xml
104104
105105
```xml
106+
106107
<configuration>
107-
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
108-
<encoder>
109-
<pattern>%clr(%d{HH:mm:ss.SSS}){faint}|%clr(${level:-%5p})|%32X{globalTraceId:-}|%32X{traceId:-},%16X{spanId:-}|%clr(%-40.40logger{39}){cyan}%clr(|){faint}%m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>
110-
<charset>utf8</charset>
111-
</encoder>
112-
</appender>
108+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
109+
<encoder>
110+
<pattern>
111+
%clr(%d{HH:mm:ss.SSS}){faint}|%clr(${level:-%5p})|%32X{globalTraceId:-}|%32X{traceId:-},%16X{spanId:-}|%clr(%-40.40logger{39}){cyan}%clr(|){faint}%m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}
112+
</pattern>
113+
<charset>utf8</charset>
114+
</encoder>
115+
</appender>
113116
</configuration>
114117
```
115118

116-
117119
### 2. Filter: GlobalTraceId 생성 및 MDC 적용
118120

119-
HTTP 요청이 들어오는 가장 앞단에서 `GlobalTraceId`를 생성하거나,
121+
HTTP 요청이 들어오는 가장 앞단에서 `GlobalTraceId`를 생성하거나,
120122

121123
외부 시스템으로부터 이미 전달받았다면 해당 값을 사용하도록 필터를 구현한다.
122124

@@ -133,36 +135,35 @@ HTTP 요청이 들어오는 가장 앞단에서 `GlobalTraceId`를 생성하거
133135
```java
134136
public class HttpRequestAndResponseLoggingFilter extends OncePerRequestFilter {
135137

136-
private static final String GLOBAL_TRACE_ID_HEADER = "X-Global-Trace-Id";
137-
private static final String GLOBAL_TRACE_ID_KEY = "globalTraceId";
138+
private static final String GLOBAL_TRACE_ID_HEADER = "X-Global-Trace-Id";
139+
private static final String GLOBAL_TRACE_ID_KEY = "globalTraceId";
138140

139-
@Override
140-
protected void doFilterInternal(@NonNull final HttpServletRequest request,
141-
@NonNull final HttpServletResponse response,
142-
@NonNull final FilterChain filterChain) {
143-
// 중간 생략 (request/response wrapper)
141+
@Override
142+
protected void doFilterInternal(@NonNull final HttpServletRequest request,
143+
@NonNull final HttpServletResponse response,
144+
@NonNull final FilterChain filterChain) {
145+
// 중간 생략 (request/response wrapper)
144146

145-
String globalTraceId = request.getHeader(GLOBAL_TRACE_ID_HEADER);
146-
if (!StringUtils.hasText(globalTraceId)) {
147-
globalTraceId = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
148-
}
149-
MDC.put(GLOBAL_TRACE_ID_KEY, globalTraceId);
150-
151-
try {
152-
filterChain.doFilter(request, response);
153-
// 중간 생략 (로깅 처리)
154-
} catch (Exception e) {
155-
// 중간 생략 (예외 처리)
156-
} finally {
157-
// 요청 처리가 끝나면 반드시 MDC에서 제거해야 한다.
158-
MDC.remove(GLOBAL_TRACE_ID_KEY);
147+
String globalTraceId = request.getHeader(GLOBAL_TRACE_ID_HEADER);
148+
if (!StringUtils.hasText(globalTraceId)) {
149+
globalTraceId = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
150+
}
151+
MDC.put(GLOBAL_TRACE_ID_KEY, globalTraceId);
152+
153+
try {
154+
filterChain.doFilter(request, response);
155+
// 중간 생략 (로깅 처리)
156+
} catch (Exception e) {
157+
// 중간 생략 (예외 처리)
158+
} finally {
159+
// 요청 처리가 끝나면 반드시 MDC에서 제거해야 한다.
160+
MDC.remove(GLOBAL_TRACE_ID_KEY);
161+
}
159162
}
160-
}
161-
// 뒷부분 생략
163+
// 뒷부분 생략
162164
}
163165
```
164166

165-
166167
### 3.Kafka Producer: 메시지 헤더에 GlobalTraceId 주입
167168

168169
API 서버에서 카프카로 메시지를 보낼 때, 현재 스레드의 MDC에서 `globalTraceId`를 꺼내 카프카 메시지 헤더에 추가한다.
@@ -172,32 +173,37 @@ API 서버에서 카프카로 메시지를 보낼 때, 현재 스레드의 MDC
172173
> CouponIssueProducer.java
173174
174175
```java
176+
175177
@Component
176178
public class CouponIssueProducer {
177179

178-
private final KafkaTemplate<String, Object> kafkaTemplate;
179-
private static final String GLOBAL_TRACE_ID_HEADER = "globalTraceId";
180+
private final KafkaTemplate<String, Object> kafkaTemplate;
181+
private static final String GLOBAL_TRACE_ID_HEADER = "globalTraceId";
182+
private static final Logger log = LoggerFactory.getLogger(CouponIssueProducer.class);
180183

181-
// 중간 생략 (생성자)
184+
// 중간 생략 (생성자)
182185

183-
public void issue(final UUID userId, final UUID couponId) {
184-
CouponIssueMessage payload = new CouponIssueMessage(userId, couponId);
185-
// MDC에서 GlobalTraceId를 가져온다.
186-
String globalTraceId = MDC.get("globalTraceId");
186+
/**
187+
* 쿠폰 발급 요청 메시지를 동기적으로 Kafka에 발행한다.
188+
* .join()을 호출하여 메시지 전송이 완료될 때까지 현재 스레드를 블로킹한다.
189+
*/
190+
public void issue(final UUID userId, final UUID couponId) {
191+
CouponIssueMessage payload = new CouponIssueMessage(userId, couponId);
192+
String globalTraceId = MDC.get("globalTraceId");
187193

188-
Message<CouponIssueMessage> message = MessageBuilder
189-
.withPayload(payload)
190-
.setHeader(KafkaHeaders.TOPIC, "coupon_issue")
191-
// Kafka 메시지 헤더에 GlobalTraceId를 추가한다.
192-
.setHeader(GLOBAL_TRACE_ID_HEADER, globalTraceId)
193-
.build();
194+
ProducerRecord<String, Object> record = new ProducerRecord<>(KafkaTopic.COUPON_ISSUE.getTopicName(), payload);
194195

195-
kafkaTemplate.send(message);
196-
}
196+
if (globalTraceId != null) {
197+
record.headers().add(GLOBAL_TRACE_ID_HEADER, globalTraceId.getBytes(StandardCharsets.UTF_8));
198+
}
199+
200+
kafkaTemplate.send(record).whenComplete((result, ex) -> {
201+
// ...
202+
}).join();
203+
}
197204
}
198205
```
199206

200-
201207
### 4. Kafka Consumer: 헤더에서 GlobalTraceId 추출 및 MDC 탑재
202208

203209
마지막으로, 컨슈머는 메시지를 수신할 때 헤더에 포함된 `globalTraceId`를 추출하여 자신의 MDC에 설정한다.
@@ -207,45 +213,47 @@ public class CouponIssueProducer {
207213
> CouponIssueConsumer.java
208214
209215
```java
216+
210217
@Component
211218
public class CouponIssueConsumer {
212219

213-
private final IssuedCouponRepository issuedCouponRepository;
214-
private static final String GLOBAL_TRACE_ID_KEY = "globalTraceId";
215-
private static final String GLOBAL_TRACE_ID_HEADER = "globalTraceId";
216-
217-
// 중간 생략 (생성자, 로거)
218-
219-
@KafkaListener(topics = "coupon_issue", groupId = "group_1")
220-
public void listener(final CouponIssueMessage message,
221-
@Header(name = GLOBAL_TRACE_ID_HEADER, required = false) String globalTraceId) {
222-
try {
223-
// 수신한 헤더의 globalTraceId를 컨슈머의 MDC에 설정한다.
224-
if (StringUtils.hasText(globalTraceId)) {
225-
MDC.put(GLOBAL_TRACE_ID_KEY, globalTraceId);
226-
}
227-
log.info("발급 처리 메시지 수신: {}", message);
228-
229-
// ... 쿠폰 발급 비즈니스 로직 ...
230-
IssuedCoupon issuedCoupon = new IssuedCoupon(message.userId(), message.couponId());
231-
issuedCouponRepository.save(issuedCoupon);
232-
log.info("쿠폰 발급 완료: {}", issuedCoupon);
233-
234-
} catch (Exception e) {
235-
// 중간 생략 (예외 처리)
236-
} finally {
237-
// 메시지 처리가 끝나면 반드시 MDC에서 제거한다.
238-
MDC.remove(GLOBAL_TRACE_ID_KEY);
220+
private final CouponIssuanceService couponIssuanceService;
221+
private final Logger log = LoggerFactory.getLogger(CouponIssueConsumer.class);
222+
private static final String GLOBAL_TRACE_ID_KEY = "globalTraceId";
223+
224+
// 중간 생략 (생성자)
225+
226+
/**
227+
* Kafka 토픽으로부터 메시지를 수신하여 쿠폰을 발급한다.
228+
*/
229+
@KafkaListener(topics = "...", groupId = "...")
230+
public void listener(final CouponIssueMessage message,
231+
@Header(name = GLOBAL_TRACE_ID_KEY, required = false) final String globalTraceId,
232+
final Acknowledgment ack) {
233+
234+
if (!Objects.isNull(globalTraceId)) {
235+
MDC.put(GLOBAL_TRACE_ID_KEY, globalTraceId);
236+
}
237+
log.info("발급 처리 메시지 수신: {}", message);
238+
239+
try {
240+
couponIssuanceService.process(message);
241+
ack.acknowledge();
242+
} catch (Exception e) {
243+
log.error("메시지 처리 실패, 재처리를 위해 커밋하지 않음: {}", message, e);
244+
} finally {
245+
// 메시지 처리가 끝나면 반드시 MDC에서 제거한다.
246+
MDC.remove(GLOBAL_TRACE_ID_KEY);
247+
}
239248
}
240-
}
241249
}
242250
```
243251

244252
## 결과: 통합된 로그
245253

246254
이제 다시 애플리케이션 2대를 실행하고, API 테스트 도구(예. Postman)를 사용하여 쿠폰 발급 요청을 보낼 때, 헤더에 `X-Global-Trace-Id`를 담아 보내보자
247255

248-
* (예: X-Global-Trace-Id: gtxid-coupon-issue-test)
256+
* (예: X-Global-Trace-Id: gtxid-coupon-issue-test)
249257

250258
> API 서버 로그 (GlobalTraceId가 gtxid-coupon-issue-test로 동일)
251259
@@ -262,7 +270,6 @@ public class CouponIssueConsumer {
262270
21:49:30.825| INFO| gtxid-coupon-issue-test|...|d.b.c.k.c.a.CouponIssueConsumer |쿠폰 발급 완료...
263271
```
264272

265-
266273
---
267274

268275
만약 헤더에 `X-Global-Trace-Id`를 보내지 않더라도 필터에서 랜덤 ID(98ac71d9...)가 생성되어 API 서버와 컨슈머 서버에서 동일하게 사용되는 것을 확인할 수 있다.
@@ -281,7 +288,7 @@ public class CouponIssueConsumer {
281288
21:46:15.846| INFO|98ac71d934194fd59a2fb04b94021234|...|d.b.c.k.c.a.CouponIssueConsumer |쿠폰 발급 완료...
282289
```
283290

284-
두 서버의 로그에 동일한 `GlobalTraceId`가 남음으로써, 특정 요청의 전체 처리 과정을 한눈에 추적할 수 있게 되었다.
291+
두 서버의 로그에 동일한 `GlobalTraceId`가 남음으로써, 특정 요청의 전체 처리 과정을 한눈에 추적할 수 있게 되었다.
285292

286293
이제 로그 분석 시스템에서 `globalTraceId`로 필터링하기만 하면 분산된 로그를 손쉽게 모아볼 수 있다.
287294

@@ -297,11 +304,11 @@ public class CouponIssueConsumer {
297304

298305
* `구현`
299306

300-
* 최초 진입점(Filter)에서 GlobalTraceId를 생성하여 MDC에 저장한다.
307+
* 최초 진입점(Filter)에서 GlobalTraceId를 생성하여 MDC에 저장한다.
301308

302-
* 프로듀서에서 메시지 발행 시 MDC의 GlobalTraceId를 메시지 헤더에 포함시킨다.
309+
* 프로듀서에서 메시지 발행 시 MDC의 GlobalTraceId를 메시지 헤더에 포함시킨다.
303310

304-
* 컨슈머에서 메시지 수신 시 헤더의 GlobalTraceId를 추출하여 MDC에 다시 저장한다.
311+
* 컨슈머에서 메시지 수신 시 헤더의 GlobalTraceId를 추출하여 MDC에 다시 저장한다.
305312

306313
* `결과`: 모든 분산 로그에 동일한 식별자가 기록되어, **Observability가 향상**되고 장애 추적이 용이해진다.
307314

0 commit comments

Comments
 (0)