2025. 10. 28. 15:49ใSpring/[2025] Spring Boot
๐ฏ ์ค๋ ํ์ต ๋ชฉํ
๋ถ์ฐ ํธ๋ ์ด์ฑ์ ๊ธฐ๋ณธ ๊ตฌ์กฐ (TraceID, SpanID) ๋ฅผ ์ดํดํ๊ณ ์ค์ Spring Boot ๋ก๊ทธ์์ TraceID ์ด๋ป๊ฒ ํ์ฉ๋๋์ง ํ์ธํด๋ณด๊ธฐ !
1๏ธโฃ TraceID / SpanID ๊ฐ๋
๐งฉ ๊ธฐ๋ณธ ์ฉ์ด ์ ๋ฆฌ
์ฉ์ด ์๋ฏธ ๋น์
| Trace | ํ๋์ ์์ฒญ ์ ์ฒด ํ๋ฆ | ์ฌ์ฉ์๊ฐ “์ฑ์์ ๋ฒํผ์ ๋๋ฌ ์์ฒญ์ ๋ณด๋ธ ์ ์ฒด ์ฌ์ ” |
| Span | Trace ์์ ํ ๋จ๊ณ (์์ ๋จ์) | Controller → Service → DB ๊ฐ๊ฐ ํ “์คํ ” |
| TraceID | Trace ์ ์ฒด๋ฅผ ์๋ณํ๋ ๊ณ ์ ID | “ํ๋ฐฐ ์ก์ฅ๋ฒํธ”์ฒ๋ผ ์ ์ฒด ์ฌ์ ์ ์ถ์ ํ๋ ๋ฒํธ |
| SpanID | ๊ฐ๊ฐ์ ์์ (Span)์ ๊ตฌ๋ถํ๋ ๊ณ ์ ID | ์ก์ฅ ์์ “์ธ๋ถ ๊ตฌ๊ฐ ๋ฒํธ” (์: ํ๋ธ ์ด๋ ๋จ๊ณ) |
TraceID ๋ ๋ณดํต ‘ํ๋ฐฐ ์ก์ฅ๋ฒํธ’๋ก ๋น์ ๊ฐ ๋ง์ด ๋๋ ๊ฒ ๊ฐ๋ค. ์ก์ฅ๋ฒํธ๋ก ๋ฐฐ์ก ๊ณผ์ ์ ์ถ์ ํ๋ ๊ฒ์ฒ๋ผ TraceID๋ก ํ๋์ ์์ฒญ ํ๋ฆ์ ์ถ์ ํ ์ ์๋ค. ํ๋ฐฐ์ ์ก์ฅ๋ฒํธ = TraceID ๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค.
SpanID๋ Trace ์์ ๊ฐ๊ฐ์ ์์ ์ ์๋ฏธํ๋ ๊ฒ์ผ๋ก ํ๋ฐฐ ์ก์ฅ ์์ ์ธ๋ถ ๊ตฌ๊ฐ ๋ฒํธ ? ๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค.
- TraceID ์ SpanID ์ ์ฐจ์ด
์ฌ์ฉ์ ๋ก๊ทธ์ธ ์์ฒญ (TraceID: abc123) โ โโ [span-001] API Gateway ํต๊ณผ โ โ โ โโ [span-002] ์ธ์ฆ ์๋น์ค โ โ โ โโ [span-003] DB ์กฐํ โ โ โ โโ [span-004] Redis ์บ์ ํ์ธ โ โโ ์๋ต ์๋ฃ - TraceID ๋ ์ ์ฒด ์์ฒญ์ ์ถ์ ํ๊ธฐ ๋๋ฌธ์ ์์๋ถํฐ ๋๊น์ง ๋์ผํ์ง๋ง, SpanID๋ ๊ฐ ๋จ๊ณ๋ฅผ ๊ตฌ๋ถํ๋ ID๋ก ์๋น์ค๋ง๋ค ๋ค๋ฅด๋ค. Trace๋ ์ฌ๋ฌ Span์ผ๋ก ์ด๋ฃจ์ด์ง ๊ฒ !
๐ฑ TraceID๊ฐ ์์ผ๋ฉด ๋ฌด์จ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊น ?
์๋น์ค์๋ ํ ๊ณ ๊ฐ๋ง ์์ฒญํ๋๊ฒ ์๋๋ผ ์ฌ๋ฌ ๊ณ ๊ฐ๋ค์ด ๋์์ ์์ฒญ์ ๋ณด๋ธ๋ค. ๋ฐ๋ผ์ TraceID ์์ด ๋ก๊ทธ๋ฅผ ์ฐ๊ฒ ๋๋ฉด ์ด๋ค ๊ณ ๊ฐ์ด ์ด๋ค ์์ฒญ์ ํ๋์ง ์ถ์ ํ๊ธฐ ์ด๋ ต๋ค. (์ค์ ๋ก ํ์ฌ ๋ด๊ฐ ์ด์ํ๋ ์๋ ํฐ์งํด์ด ์๋น์ค๋ TraceID ๊ฐ ์์ด์ ๋ก๊ทธ ์ถ์ ์ด ๋๋ฌด ๋๋ฌด ์ด๋ ค์.. ๋ฏผ์ ๋ค์ด์ค๋ฉด ๊ฐ๋ฒ์ผ๋ก ์ถ์ ํด์ผ ํด์ ์๊ฐ์ด ๊ต์ฅํ ์ค๋๊ฑธ๋ฆฌ๋ ํธ)
๋ก๊ทธ๊ฐ ์ด๋ ๊ฒ ์์ฌ์๋ค๋ฉด?
[15:30:01] UserService - ๋ก๊ทธ์ธ ์์
[15:30:01] OrderService - ์ฃผ๋ฌธ ์์ฑ ์์
[15:30:02] UserService - DB ์กฐํ ์๋ฃ
[15:30:02] OrderService - ์ฌ๊ณ ํ์ธ
[15:30:03] UserService - ๋ก๊ทธ์ธ ์๋ฃ
[15:30:03] OrderService - ์ฃผ๋ฌธ ์๋ฃ
→ ๋์์ 100๋ช ์ด ์์ฒญํ๋ฉด ๊ตฌ๋ถ์ด ์ด๋ ค์,, ์๋ฌ ๋ฐ์ ์ ์ด๋ค ์์ฒญ์์ ๋ฐ์ํ ๋ฌธ์ ์ธ์ง๋ ์ฐพ๊ธฐ ์ด๋ ค์
TraceID, SpanID๋ฅผ ์ฌ์ฉํ๋ฉด:
[15:30:01] [trace-001,span-A] UserService - ๋ก๊ทธ์ธ ์์
[15:30:01] [trace-002,span-X] OrderService - ์ฃผ๋ฌธ ์์ฑ ์์
[15:30:02] [trace-001,span-A] UserService - DB ์กฐํ ์๋ฃ
[15:30:02] [trace-002,span-X] OrderService - ์ฌ๊ณ ํ์ธ
[15:30:03] [trace-001,span-A] UserService - ๋ก๊ทธ์ธ ์๋ฃ
[15:30:03] [trace-002,span-X] OrderService - ์ฃผ๋ฌธ ์๋ฃ
→ ์ด๋ฐ์์ผ๋ก TraceID์ SpanID๋ฅผ ์ฌ์ฉํ๋ฉด grep ์ผ๋ก “trace-001” ๋ง ๊ฒ์ํ๋ฉด ๋๋๊น ์ถ์ ํ๊ธฐ ํจ์ฌ ํธํ๋ค.
๐ ๋ง์ดํฌ๋ก์๋น์ค์์ TraceID
TraceID ๋ MSA ๊ตฌ์กฐ์์ ์ฌ์ฉํ๋ฉด ๊ทธ ์ง๊ฐ๋ฅผ ๋ฐํํ๋๋ฐ ๊ทธ ์ด์ ๋ ๋จ์ผ ์๋ฒ์์๋ ๋น๊ต์ ๋ก๊ทธ ์ถ์ ์ด ์ฝ์ง๋ง MSA ๊ตฌ์กฐ์์๋ ์๋น์ค๋ง๋ค ์๋ฒ๊ฐ ๋ถ๋ฆฌ๋์ด ์๊ธฐ ๋๋ฌธ์ ๋ก๊ทธ๊ฐ ๋ถ์ฐ๋ผ์ ๋ก๊ทธ ์ถ์ ์ด ์ด๋ ค์์ง๋ค.
์ด๋, TraceID ๋ฅผ ์ฌ์ฉํ๋ฉด
[์๋ฒ1] [trace-001,span-A] Gateway - ์์ฒญ ๋ฐ์ (0ms)
[์๋ฒ2] [trace-001,span-X] Auth - ์ธ์ฆ ์์ (5ms)
[์๋ฒ3] [trace-001,span-B] User - ์ฌ์ฉ์ ์กฐํ (15ms) โ ๏ธ ๋๋ฆผ!
[์๋ฒ4] [trace-001,span-Y] Notify - ์๋ฆผ ๋ฐ์ก (2ms)
์ด๋ฐ์์ผ๋ก trace-001 ๋ก ๊ฒ์ํ๋ฉด 4๊ฐ ์๋ฒ์์ ๋ชจ๋ ๋ก๊ทธ๋ฅผ ํ์ธํ ์ ์๊ณ , ์๋ฒ 3์์ ๋ฌธ์ ๊ฐ ์๋ค๋ ๊ฑธ ํ์ธํ ์ ์๋ค.
โ๏ธ Trace Context ์ ํ (Propagation)
๋ถ์ฐ ์์คํ ์์๋ ์๋น์ค๊ฐ ์ฌ๋ฌ ๊ฐ๋ก ๋๋์ด ์๊ธฐ ๋๋ฌธ์ (MSA) TraceID์ SpanID๊ฐ ๋ค์ ์๋ฒ๋ก ์ ๋ฌ๋์ด์ผ ํ๋ค. ์ ํ ๋ฐฉ์์ ์ฃผ๋ก HTTP Header ๋ฅผ ์ฌ์ฉํ๋ค.
traceparent ๊ตฌ์กฐ
00-{traceId}-{parentSpanId}-{flags}
์์
GET /api/users HTTP/1.1
Host: service-b.com
traceparent: 00-1234abcd5678ef90-7890abcd1234ef56-01
โโ โ โ โโ flags (01=sampled)
โโ โ โโโโโโโโโโโโ parent spanId
โโ โโโโโโโโโโโโโโโโโโโโโโโโ traceId (128bit)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ version (00)
tracestate: vendor1=value1,vendor2=value2
- 00 : Version
- 1234abcd5678ef90: TraceID
- 7890abcd1234ef56 : SpanID
- 01 : Trace flags
์๋น์ค A → ์๋น์ค B๋ก ์์ฒญ์ ๋ณด๋ผ ๋, HTTP ํค๋์ Trace ์ ๋ณด๋ฅผ ์ค์ด ๋ณด๋ด๋ฉด ์๋น์ค B์์๋ ๊ฐ์ TraceID ๋ก ๋ก๊ทธ๋ฅผ ๋จ๊ธธ ์ ์๋ ๊ตฌ์กฐ๋ค.
2๏ธโฃ ์์ ํ๋ก์ ํธ ๋ง๋ค์ด๋ณด๋ฉฐ TraceID, SpanID ์ค์ตํด๋ณด๊ธฐ
์ค์ (build.gradle, application.yml)
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.7'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
description = 'Demo project for Spring Boot'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// Micrometer Tracing (OpenTelemetry bridge)
implementation 'io.micrometer:micrometer-tracing-bridge-otel'
implementation 'io.opentelemetry:opentelemetry-exporter-zipkin'
// For @Observed (AOP-based observations around methods)
implementation 'org.springframework.boot:spring-boot-starter-aop'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
application.yml
spring:
application:
name: spring-tracing-quickstart
server:
port: 8085
management:
tracing:
enabled: true
sampling:
probability: 1.0 # sample 100% for practice
zipkin:
tracing:
endpoint: <http://localhost:9411/api/v2/spans>
endpoints:
web:
exposure:
include: health,info
logging:
pattern:
console: '[%d{yyyy-MM-dd HH:mm:ss}] [%X{traceId},%X{spanId}] [%thread] %highlight(%-5level) %logger %msg%n'
file:
path: /var/www/log/tracing-study
name: console.log
level:
root: info
Tracing ๊ธฐ๋ณธ ์ค์ (Micrometer Tracing)
management:
tracing:
enabled: true
sampling:
probability: 1.0 # sample 100% for practice
- managment.tracing.enabled : Micrometer Tracing ๊ธฐ๋ฅ์ ํค๋ ๊ฒ
- Micrometer Tracing ์ด TraceID๋ฅผ ์์ฑํ๊ณ ๊ด๋ฆฌํด์ฃผ๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ์ด๊ฑธ ์ํค๋ฉด TraceID๊ฐ ์์ฑ๋์ง๋, ์ ํ๋์ง๋ ์์ !
- sampling.probability : 1.0 : ํธ๋ ์ด์ค๋ฅผ ๋ช % ์์งํ ์ง ๊ฒฐ์ ํ๋ ์ํ๋ง ๋น์จ
- 1.0 ์ด๋ฉด 100% ์์งํ๊ฒ ๋ค๋ ๊ฒ์ผ๋ก ํ์ต, ๊ฐ๋ฐ์ ์ ์ฉํจ
- ์ด์์์ ๋ณดํต 0.01 ~ 0.2 ์ ๋๋ก ๋ฎ์ถ๋ค๊ณ ํจ. ํธ๋ํฝ์ด ๋ง์์๋ก ๋ฎ์ถฐ์ผ ํจ
โ๏ธ Micrometer Tracing ์ด๋?
์ ํ๋ฆฌ์ผ์ด์ ์ ์์ฒญ ํ๋ฆ(Trace/Span) ์ ์ถ์ ํ๋ ๊ด์ธก์ฑ ํ๋ ์์ํฌ
์๋น์ค ์์์ “์ด ์์ฒญ์ด ์ด๋์ ์์๋์ด, ์ด๋ค ์ปดํฌ๋ํธ๋ฅผ ๊ฑฐ์ณ, ์ด๋์ ์ง์ฐ์ด ๋ฐ์ํ๋๊ฐ”๋ฅผ ์ถ์ ํ๋ ์์คํ → ์ด ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Zipkin, Jaeger, Tempo, Grafana ๋ฑ์์ ์๊ฐ์ ์ผ๋ก ํธ๋ ์ด์ค๋ฅผ ๋ณผ ์ ์์
๋์ ๋ฐฉ์
Client → Controller → Service → DB
1๏ธโฃ Controller ์ง์ ์ traceId, spanId ์์ฑ
2๏ธโฃ ๊ฐ ๋จ๊ณ๋ณ๋ก child span ์์ฑ
3๏ธโฃ MDC(Context)์ trace ์ ๋ณด ์ฝ์ → ๋ก๊ทธ์ [traceId][spanId] ์ถ๋ ฅ
4๏ธโฃ Exporter๊ฐ Zipkin/Tempo/Jaeger ๋ฑ์ผ๋ก ๋ฐ์ดํฐ ์ ์ก
โ๏ธ ์ด์์์ ์ํ๋ง ๋น์จ์ 0.01 ~ 0.2 ์ ๋๋ก ๋ฎ์ถ๋ ์ด์ ?
TraceID, SpanID ๋ฅผ 100% ์์งํ๋ฉด ๋ถํ๊ฐ ์ปค์ง๊ธฐ ๋๋ฌธ
๋ถ์ฐ ํธ๋ ์ด์ฑ์ ๋ชจ๋ ์์ฒญ์ ์คํ ๊ฒฝ๋ก๋ฅผ Span ๋จ์๋ก ๊ธฐ๋กํ๊ณ ์ ์กํ๊ณ , ์ด ๋ฐ์ดํฐ๋ Zipkin, Tempo, Jaeger ๊ฐ์ ์ ์ฅ์์ ์์. ํธ๋ํฝ์ด ๋ง์ผ๋ฉด ๋ชจ๋ Span์ด Zipkin ์๋ฒ๋ก ์ ์ก๋๊ณ ๋์คํฌ/๋คํธ์ํฌ/CPU๊ฐ ๊ธ๊ฒฉํ ์ฌ์ฉ๋์ด ์ฌ๋ผ๊ฐ !
๋ฐ๋ผ์ Trace ๋ฐ์ดํฐ๋ฅผ ์ ๋ถ ์์งํ์ง ์๊ณ ์ผ๋ถ ์์ฒญ๋ง ์ถ์ ํ๋๋ก ํ๋ฅ ์ ์ผ๋ก ์ ํํ๋ ๊ฒ์ด ์ํ๋ง์ผ๋ก ๋ชจ๋ ์์ฒญ์ ๊ธฐ๋กํ ํ์๋ ์๊ณ , ์ผ์ ๋น์จ๋ง์ผ๋ก๋ ์ถฉ๋ถํ ํจํด๊ณผ ๋ณ๋ชฉ์ ํ์ ํ ์ ์์.
โก๏ธ ํ๋ง๋๋ก ์ํ๋ง ๋น์จ์ ๋ฎ์ถ๋ ์ด์ ๋ ํธ๋ ์ด์ค ๋ฐ์ดํฐ๊ฐ ๋ฐฉ๋ํ๊ฒ ์์ฌ ์์คํ ๊ณผ ์ ์ฅ์์ ๋ถ๋ด์ ์ฃผ๊ธฐ ๋๋ฌธ์ด๋ฉฐ, ์ด์ ํ๊ฒฝ์์๋ ์ผ๋ถ ์์ฒญ๋ง ์ถ์ ํด๋ ์ถฉ๋ถํ ์ฑ๋ฅ ๋ณ๋ชฉ์ ์ง๋จํ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
Zipkin Exporter ์ค์
management:
zipkin:
tracing:
endpoint: <http://localhost:9411/api/v2/spans>
- Micrometer Tracing์ด ์์งํ Span ์ Zipkin ์๋ฒ๋ก ์ ์กํ ์ฃผ์
- Zipkin์ ๊ธฐ๋ณธ ํฌํธ๋ 9411
Actuator ๋ ธ์ถ (์ ํ)
management:
endpoints:
web:
exposure:
include: health,info
- Actuator์์ ์น์ผ๋ก ๋ ธ์ถํ ์๋ํฌ์ธํธ๋ฅผ ์ง์ ํจ
- ์ต์ํ health, info ์ ๋๋ง ์ด์ด๋๋ฉด ๊ธฐ๋ณธ ์ํ ์ ๊ฒ์ ์ถฉ๋ถํจ
- ์ด์์์ ๋ ธ์ถ ๋ฒ์๋ฅผ ์ขํ๊ณ ๋ณด์ ์ค์ ์ ํจ๊ป ๊ณ ๋ คํด์ผ ํจ
logging ์ค์
logging:
pattern:
console: '[%d{yyyy-MM-dd HH:mm:ss}] [%X{traceId},%X{spanId}] [%thread] %highlight(%-5level) %logger %msg%n'
file: '[%d{yyyy-MM-dd HH:mm:ss}] [%X{traceId},%X{spanId}] [%thread] %-5level %logger %msg%n'
file:
path: /var/www/log/tracing-study
level:
root: info
docker-compose.yml
services:
zipkin:
image: openzipkin/zipkin
container_name: zipkin
ports:
- "9411:9411"
ObservationConfig
@Configuration
public class ObservationConfig {
@Bean
public ObservedAspect observedAspect(ObservationRegistry registry) {
return new ObservedAspect(registry);
}
}
- Micrometer Tracing ์ด @Observed ์ด๋ ธํ ์ด์ ์ธ์ํ๊ณ ๋์ํ๊ฒ ๋ง๋๋ ์ค์
- ObservationRegistry: Micrometer ์ Observation ์ ๋ฑ๋ก, ๊ด๋ฆฌํ๋ ์ค์ ํ๋ธ ๊ฐ์ฒด
- @Observed๊ฐ ๋ฌ๋ฆฐ ๋ฉ์๋๋ฅผ ์คํํ ๋, Micrometer๊ฐ ๋ด๋ถ์ ์ผ๋ก ObservationRegistry๋ฅผ ์ฌ์ฉํด ์ Observation(Span) ์ ์์ฑํ๊ณ ์คํ ์๊ฐ, ์์ธ, ํ๊ทธ, ์ํ ๋ฑ์ ๊ธฐ๋กํฉ๋๋ค.
- ObservedAspect : Spring AOP ๊ธฐ๋ฐ์ผ๋ก ์๋ํ๋ Aspect
๐งฉ ์คํ ํ๋ฆ ์์
1๏ธโฃ Controller → DemoService.work() ํธ์ถ
2๏ธโฃ AOP๊ฐ @Observed ๊ฐ์ง
3๏ธโฃ ObservedAspect๊ฐ ObservationRegistry์ ์ Observation ๋ฑ๋ก
4๏ธโฃ ์คํ ์ ํ๋ก ๋ก๊ทธ + ํธ๋ ์ด์ค ์์ง
5๏ธโฃ Exporter(Zipkin ๋ฑ)์ Span ์ ์ก
์ค์ต ์ฝ๋ (Ctl, Service…)
TracingController
package com.example.spring_tracing_quickstart.ctl;
import com.example.spring_tracing_quickstart.service.DemoService;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@Slf4j
@RestController
public class TracingController {
private final Tracer tracer;
private final DemoService demoService;
public TracingController(Tracer tracer, DemoService demoService) {
this.tracer = tracer;
this.demoService = demoService;
}
@GetMapping("/trace-test")
public Map<String, String> test() {
Span span = tracer.currentSpan();
return Map.of(
"traceId", span.context().traceId(),
"spanId", span.context().spanId()
);
}
@GetMapping("/trace")
public Map<String, String> trace() throws InterruptedException {
Span span = tracer.currentSpan();
log.info("Controller - handling /trace");
demoService.work();
demoService.callDownstreamSimulation();
return Map.of("Ok", span.context().traceId());
}
}
DemoService
package com.example.spring_tracing_quickstart.service;
import io.micrometer.observation.annotation.Observed;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.concurrent.ThreadLocalRandom;
@Slf4j
@Service
public class DemoService {
@Observed(name = "service.work", contextualName = "service#work")
public void work() throws InterruptedException {
log.info("Service - work()");
Thread.sleep(50 + ThreadLocalRandom.current().nextInt(50));
log.info("Service - work() done");
}
@Observed(name = "service.downstream", contextualName = "service#downstream")
public void callDownstreamSimulation() throws InterruptedException {
log.info("Service - callDownstreamSimulation() start");
// Imagine an external API / DB call here
Thread.sleep(100 + ThreadLocalRandom.current().nextInt(150));
log.info("Service - callDownstreamSimulation() end");
}
}
@Observed
๋ฉ์๋ ์คํ์ ์๋์ผ๋ก ํธ๋ ์ด์ค(Span) + ๋ฉํธ๋ฆญ(Observation) ์ผ๋ก ๊ธฐ๋กํ๋ ์ด๋ ธํ ์ด์
- name : ๋ฉํธ๋ฆญ ์ด๋ฆ์ผ๋ก ์ฌ์ฉ๋จ
- contextualName : ํธ๋ ์ด์ค์ ํ์๋ ์ด๋ฆ
์คํ
/trace ํธ์ถ
- http://localhost:8080/trace
์คํ ๋ก๊ทธ
[2025-10-28 13:51:01] [749f01246a3dc717d386e80b295c4464,91313b104f8e24da] [http-nio-8085-exec-1] INFO com.example.spring_tracing_quickstart.ctl.TracingController Controller - handling /trace
[2025-10-28 13:51:01] [749f01246a3dc717d386e80b295c4464,4df6a2ffd774bc93] [http-nio-8085-exec-1] INFO com.example.spring_tracing_quickstart.service.DemoService Service - work()
[2025-10-28 13:51:01] [749f01246a3dc717d386e80b295c4464,4df6a2ffd774bc93] [http-nio-8085-exec-1] INFO com.example.spring_tracing_quickstart.service.DemoService Service - work() done
[2025-10-28 13:51:01] [749f01246a3dc717d386e80b295c4464,af04e80269c1aea9] [http-nio-8085-exec-1] INFO com.example.spring_tracing_quickstart.service.DemoService Service - callDownstreamSimulation() start
[2025-10-28 13:51:01] [749f01246a3dc717d386e80b295c4464,af04e80269c1aea9] [http-nio-8085-exec-1] INFO com.example.spring_tracing_quickstart.service.DemoService Service - callDownstreamSimulation() end
Trace (ํ๋์ HTTP ์์ฒญ)
โโ [Span 1] Controller (/trace)
โ โโ [Span 2] Service.work()
โ โโ [Span 3] Service.callDownstreamSimulation()
- TraceID : ์์ฒญ ์ ์ฒด์ ์๋ณ์
- SpanID : ์์ ๋จ์๋ณ ์๋ณ์
- ParentID : ์ด๋ค Span ์๋์ ์ํ๋์ง ๋ํ๋
Zipkin UI

Zipkin UI์์ ๋ถ์ํ๋ ๋ฐฉ๋ฒ
(1) Duration (์์ ์๊ฐ) ๋น๊ต
- /trace : ์ ์ฒด 314ms
- service#work : 73ms
- service#downstream : 192ms
→ ์๋น์ค ๋ด๋ถ์ ์ด๋ค ๋ก์ง์ด ์๋์ ์ผ๋ก ์ค๋ ๊ฑธ๋ฆฌ๋์ง ํ์ ๊ฐ๋ฅํจ (์ฑ๋ฅ ๋ณ๋ชฉ ๊ตฌ๊ฐ ๋ถ์์ฉ์ผ๋ก ์์ฃผ ์ฌ์ฉํจ)
(2) Time Overlap (๊ฒน์นจ ์ฌ๋ถ)
- ๋ Span ์ด ๊ฒน์ณ ์์ผ๋ฉด ๋น๋๊ธฐ ํธ์ถ, ๋จ์ด์ ธ ์์ผ๋ฉด ๋๊ธฐ ํธ์ถ
(3) ์๋น์ค ๊ฐ ํธ์ถ ํธ๋ ์ด์ค
- spring-tracing-quickstart ์ธ์ ๋ค๋ฅธ ์๋น์ค๋ช ์ด ๋ณด์ด๋ฉด HTTP ์์ฒญ ์ traceparent ํค๋๊ฐ ์ ์ ํ๋ ๊ฒ
→ ๋ถ์ฐ ํธ๋ ์ด์ฑ ํ์ธ์ฉ
๋ฑ.. ์ค๋ฌด์์ ํ๋ Trace ๋ถ์ ๋ฃจํด
๋ถ์ ๋ชฉํ ๋ณด๋ ์์น ํด์ ํฌ์ธํธ
| ๋๋ฆฐ ์์ฒญ ์ฐพ๊ธฐ | Trace ๋ฆฌ์คํธ Duration ์ ๋ ฌ | ์์ 10๊ฐ Trace ์ด์ด์ ๋ณ๋ชฉ Span ํ์ธ |
| ์ธ๋ถ API ๋๋ฆผ ๋ถ์ | ๊ฐ Span์ Duration ๋น๊ต | ํน์ ์ธ๋ถ ํธ์ถ (ex. payment API)์ด ๋๋ถ๋ถ ์ง์ฐ์ธ์ง ํ์ธ |
| ๋ด๋ถ ํธ์ถ ๋ณ๋ชฉ ํ์ธ | Controller → Service → Repository ์ | Service, Repository ์ค ์ด๋์ ์๊ฐ์ด ๋ง์์ง |
| ์ฅ์ ์ TraceID ๊ธฐ๋ฐ ์ญ์ถ์ | ๋ก๊ทธ traceId๋ก Zipkin ๊ฒ์ | ์์ฒญ ํ๋ฆ ์ ์ฒด ์ฌํ ๊ฐ๋ฅ |
โ ๏ธ Zipkin ์ด์ ํ๊ฒฝ ์ฃผ์์ฌํญ
1๏ธโฃ ์ํ๋ง ๋น์จ ์กฐ์ (Sampling Rate)
- ๊ฐ๋ฐ ํ๊ฒฝ์ 1.0 (100%) ๋ก ๋๊ณ , ์ด์ ํ๊ฒฝ์ 0.01 ~ 0.2 ์ ๋๋ก ์ค์ฌ์ผ ํจ
- ํธ๋ํฝ์ด ๋ง์ผ๋ฉด Span ๋ฐ์ดํฐํญ์ฆํด์ Zipkin ์๋ฒ๊ฐ ๋ฒํฐ์ง ๋ชปํ๊ธฐ ๋๋ฌธ์ ๋ํ ์ํ๋ง ์์งํด์ ๊ฒฝํฅ์ฑ ํ์ธ ํ์
2๏ธโฃ Zipkin ์๋ฒ ์ ์ฅ์(Storage) ๋ถํ ๊ด๋ฆฌ
- ๊ธฐ๋ณธ Zipkin ์๋ฒ๋ in-memory storage ๋ก ๋จ๊ธฐ ๋๋ฌธ์ ์ฌ์์ ์ ๋ฐ์ดํฐ ์ฌ๋ผ์ง
- ๋ฐ๋ผ์ ์ด์์์๋ Elasticsearch, MySQL, Cassandra ์ค ํ๋๋ก ๊ต์ฒดํ๋ค๊ณ ํจ..!
→ ์ค๋๋ Trace ๋ ๋ก๊ทธ ๋ถ์์ด๋ APM ์ผ๋ก ๋๊ธฐ๊ณ , Zipkin ์ ๋จ๊ธฐ ํธ๋ฌ๋ธ์ํ ์ฉ์ผ๋ก ์ฌ์ฉํ๋๊ฒ ์ผ๋ฐ์ ์
3๏ธโฃ ๋ณด์ ๋ฐ ๊ฐ์ธ์ ๋ณด ๋ ธ์ถ ์ฃผ์
- Trace/Span ๋ฐ์ดํฐ ์์๋ URI, ์์ฒญ ํ๋ผ๋ฏธํฐ, ํค๋ ๊ฐ์ด ๊ทธ๋๋ก ๋ค์ด๊ฐ ์ ์๊ธฐ ๋๋ฌธ์ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ ๋ก๊ทธ ์ ์ก ์ ๋ง์คํน or ์ ์ธ ํํฐ ์ ์ฉ ํด์ผ ํจ
4๏ธโฃ TraceID ์ฐ๋ ๋ก๊น ์ผ๊ด์ฑ ์ ์ง
- Zipkin์ Tracer → MDC(TraceId/ SpanID) → Logback → ๋ก๊ทธ ์์ง๊ธฐ (ELK) ๋ก ์ด์ด์ง๋ฏ๋ก ๋ก๊ทธ ํฌ๋งท์ด ์ผ์ ํ์ง ์์ผ๋ฉด TraceID๋ก ๊ฒ์์ด ๋ถ๊ฐ๋ฅํจ !
- ์ค๋ฌด์์๋ Zipkin ๋ณด๋ค ELK ์์ traceID๋ก ๋ก๊ทธ ํํฐ๋ง์ ๋ ์์ฃผ ํจ.
(์ถ๊ฐ) ๋น๋๊ธฐ ์ฒ๋ฆฌ์์์ ์ ํ
ClonedTaskDecorator
์ ์ค๋ ๋์์๋ traceId๊ฐ ๋์ผํ๊ฒ ์ ์ง๋๊ฒ ํ๋ ๋ํผ
public class ClonedTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
ContextSnapshot snapshot = ContextSnapshot.captureAll();
return () -> snapshot.wrap(runnable).run();
}
}
- Spring ์ @Async ๋ ์ ์ค๋ ๋์์ ๋ฉ์๋๋ฅผ ์คํํ๋๋ฐ traceID, SpanID, MDC ๊ฐ์ ์ ๋ณด๋ ThreadLocal ์ ์ ์ฅ๋๊ธฐ ๋๋ฌธ์ ์ ์ค๋ ๋๋ก ๋์ด๊ฐ๋ฉด ์๋์ผ๋ก ์ฌ๋ผ์ง๋ค.
- ๋ฐ๋ผ์ ํ์ ์ค๋ ๋(Controller) ์์ ๊ฐ์ง๊ณ ์๋ ๊ด์ธก ์ปจํ ์คํธ(Trace + MDC) ๋ฅผ ๋น๋๊ธฐ ์ค๋ ๋(Service) ๋ก ๋ณต์ ํด์ฃผ๋ ์ญํ ์ด ํ์ํจ
- ContextSnapshot.captureAll(); : Micrometer์์ ์ ๊ณตํ๋ API๋ก ํ์ฌ ์ค๋ ๋์ ThreadLocal ๊ธฐ๋ฐ ์ปจํ ์คํธ(MDC, Trace, Observation) ๋ฅผ ํ ๋ฒ์ ์ฌ์ง ์ฐ๋ฏ ๋ณต์ฌํจ → snapshot ์ ๊ทธ ์์ ์ traceId, spanId๋ฅผ ๋ชจ๋ ๋ด๊ณ ์์
- snapshot.wrap(runnable) : Mircometer ๊ฐ ์ ๊ณตํ๋ ๋ํ ํจ์๋ก ์๋ ์คํํ Runnable ๋ฅผ ๊ฐ์ธ์ ์ ์ค๋ ๋์์ ์คํ๋ ๋ ์ค๋ ์ท์ผ๋ก ์ฐ์ด๋ ์ปจํ ์คํธ๋ฅผ ๋ณต์ํด์ค
- ๋ณต์๋ ์ํ์์ runnable.run()์ด ์คํ๋๊ธฐ ๋๋ฌธ์ ๋ก๊ทธ์ ํธ๋ ์ด์ค๊ฐ ๋ชจ๋ ๋ถ๋ชจ TraceID๋ก ์ฐ๊ฒฐ๋จ
AsyncConfig ์ค์
@Aysnc๊ฐ ์ฌ์ฉํ๋ ๋น๋๊ธฐ ์ค๋ ๋ ํ์ ์ง์ ๊ตฌ์ฑํ๋ฉด์ TaskDecorator ๋ก Trace, MDC ๋ฑ ThreadLocal ์ปจํ ์คํธ๋ฅผ ์๋ ๋ณต์ ํด์ฃผ๋ ์ค์
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(4);
taskExecutor.setMaxPoolSize(40);
taskExecutor.setQueueCapacity(160);
taskExecutor.setThreadNamePrefix("springTask-");
taskExecutor.setTaskDecorator(taskDecorator());
taskExecutor.initialize();
return taskExecutor;
}
@Bean
public TaskDecorator taskDecorator() {
return new ClonedTaskDecorator();
}
}
- ์ค๋ ๋ ํ ์ธ๋ถ ์ค์ ์ค์ ์๋ฏธ ์ค๋ช
→ ๋์์ 4๊ฐ์ @Async ์์ ์ด ๋๊ณ , ํ์ํ๋ฉด ์ต๋ 40๊ฐ๊น์ง ํ์ฅ๋๋ฉฐ ๊ทธ ์ด์์ 160๊ฐ๊น์ง๋ง ํ์ ๋๊ธฐcorePoolSize ์ต์ ์ค๋ ๋ ๊ฐ์ ๊ธฐ๋ณธ์ผ๋ก ํญ์ ์ ์ง๋๋ ์์ ์ค๋ ๋ ์ maxPoolSize ์ต๋ ์ค๋ ๋ ๊ฐ์ ์์ฒญ์ด ๋ชฐ๋ฆด ๋ ๋์ด๋ ์ ์๋ ์ต๋ ์ค๋ ๋ ์ queueCapacity ์์ ๋๊ธฐ ํ ํฌ๊ธฐ ์คํ ์ค์ธ ์ค๋ ๋๊ฐ ๊ฝ ์ฐผ์ ๋ ๋๊ธฐํ ์์ ์ threadNamePrefix ์ค๋ ๋ ์ด๋ฆ ์ ๋์ด ๋๋ฒ๊น ์ ๋ก๊ทธ์ [springTask-1] ์ด๋ฐ ์์ผ๋ก ํ์๋จ - taskExecutor.setTaskDecorator(taskDecorator()); : TaskDecorator๋ ๊ฐ Runnable ์คํ ์ ์ ํน์ ๋ก์ง์ ๊ฐ์ธ์ฃผ๋ ํ (Hook)
- taskExecutor.initialize(); : ์ค๋ ๋ ํ ์ด๊ธฐํํ๊ณ Spring Bean์ผ๋ก ๋ฑ๋ก, ์ดํ @Async ๋ฉ์๋๋ค์ ์ด Executor๋ฅผ ํตํด ์คํ๋จ
DemoService
@Async
@Observed(name = "service.async", contextualName = "service#async")
public void workAsync() throws InterruptedException {
Thread.sleep(100 + ThreadLocalRandom.current().nextInt(5000));
log.info("Service - workAsync() start");
Thread.sleep(100 + ThreadLocalRandom.current().nextInt(100));
log.info("Service - workAsync() end");
}
TracingController
@GetMapping("/trace/async")
public Map<String, String> async() throws InterruptedException {
log.info("Controller - handling /trace/async");
demoService.work();
demoService.workAsync();
return Map.of("Ok", tracer.currentSpan().context().traceId());
}
์คํ ๋ก๊ทธ
[2025-10-28 15:20:45] [c1ac96986375abf055733ffde0f927dd,33e39c74ab39a193] [http-nio-8085-exec-1] INFO com.example.spring_tracing_quickstart.ctl.TracingController Controller - handling /trace/async
[2025-10-28 15:20:45] [c1ac96986375abf055733ffde0f927dd,5c6a1a727850edf8] [http-nio-8085-exec-1] INFO com.example.spring_tracing_quickstart.service.DemoService Service - work()
[2025-10-28 15:20:45] [c1ac96986375abf055733ffde0f927dd,5c6a1a727850edf8] [http-nio-8085-exec-1] INFO com.example.spring_tracing_quickstart.service.DemoService Service - work() done
[2025-10-28 15:20:49] [c1ac96986375abf055733ffde0f927dd,e8d9e126ad2339a2] [springTask-1] INFO com.example.spring_tracing_quickstart.service.DemoService Service - workAsync() start
[2025-10-28 15:20:49] [c1ac96986375abf055733ffde0f927dd,e8d9e126ad2339a2] [springTask-1] INFO com.example.spring_tracing_quickstart.service.DemoService Service - workAsync() end
๐ ์ ์ฒด ์คํ ํ๋ฆ ์ ๋ฆฌ
1. Controller Thread
↓
Micrometer Tracing์ด TraceID, SpanID๋ฅผ MDC/ThreadLocal์ ์ ์ฅ
↓
2. @Async ๋ฉ์๋ ํธ์ถ
↓
ThreadPoolTaskExecutor๊ฐ ์๋ก์ด Worker Thread๋ฅผ ๊บผ๋
↓
TaskDecorator (ClonedTaskDecorator)๊ฐ ์คํ๋จ
- ContextSnapshot.captureAll() → ํ์ฌ ThreadLocal ๋ณต์ฌ
- snapshot.wrap(runnable).run() → ์ ์ค๋ ๋์์ ๋ณต์
↓
3. Service Thread
↓
traceId, spanId๊ฐ ๋ณต์๋ ์ํ๋ก ์คํ
↓
Zipkin / ๋ก๊ทธ ๋ชจ๋ ๋์ผ traceId๋ก ์ฐ๊ฒฐ
3๏ธโฃ์์ฝ
Trace vs Span
| ๋จ์ | ์ ์ฒด ์์ฒญ | ์ธ๋ถ ์์ |
| ์๋ณ์ | TraceID | SpanID |
| ๊ด๊ณ | Span๋ค์ ์์ ์ปจํ ์ด๋ | Trace ๋ด์ ๊ฐ๋ณ ๋จ์ |
| ์ ํ ๋ฐฉ์ | HTTP ํค๋, Context | Parent-Child ๊ตฌ์กฐ |
| ๋ชฉ์ | ์์ฒญ ์ ์ฒด ์ถ์ | ๊ฐ ๊ตฌ๊ฐ ์ฑ๋ฅ ์ธก์ |
'Spring > [2025] Spring Boot' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| Spring Batch ๊ฐ๋ ๋ฐ ์ฃผ์ ๊ตฌ์ฑ์์, ๊ฐ๋จํ ์์ ๊ตฌํ (0) | 2025.02.18 |
|---|