사용자가 "주문이 느려요"라고 할 때, 10개 서비스 중 어디가 병목인지 어떻게 찾을 수 있을까요?

마이크로서비스 환경에서 하나의 사용자 요청은 여러 서비스를 거칩니다. 각 서비스의 로그만 보면 전체 흐름을 파악하기 어렵습니다. 분산 추적은 하나의 요청이 서비스를 거치는 전체 경로를 추적하고, 어디서 지연이 발생하는지 가시화합니다.

분산 추적의 핵심 개념

Trace

하나의 사용자 요청이 시작되어 최종 응답까지 이어지는 전체 흐름 입니다. 고유한 Trace ID로 식별됩니다.

Span

Trace 내에서 하나의 서비스가 수행하는 개별 작업 단위 입니다. 고유한 Span ID를 가지며, 부모-자식 관계를 형성합니다.

PLAINTEXT
Trace (ID: abc123)
├── Span A: API Gateway (20ms)
│   └── Span B: Order Service (150ms)
│       ├── Span C: User Service (30ms)
│       └── Span D: Payment Service (100ms)
│           └── Span E: DB Query (50ms)

이 구조를 보면 "전체 요청 200ms 중 결제 서비스가 100ms를 차지한다"는 것을 바로 알 수 있습니다.

Spring Boot 3 + Micrometer Tracing

Spring Boot 3부터 Spring Cloud Sleuth가 Micrometer Tracing으로 대체되었습니다.

의존성

XML
<!-- Micrometer Tracing + Brave 브릿지 -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>

<!-- Zipkin 리포터 -->
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
</dependency>

또는 OpenTelemetry 브릿지를 사용할 수도 있습니다.

XML
<!-- OpenTelemetry 브릿지 (Brave 대신) -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency>

설정

YAML
management:
  tracing:
    sampling:
      probability: 1.0  # 100% 샘플링 (운영에서는 0.1 등으로 조절)
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans

logging:
  pattern:
    level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"

이 설정만으로 Spring Boot가 자동으로:

  • 들어오는 요청에 Trace ID/Span ID 생성 (또는 기존 것 이어받기)
  • 나가는 요청(RestTemplate, WebClient, Feign)에 Trace 헤더 전파
  • 로그에 Trace ID/Span ID 포함
  • Zipkin에 Trace 데이터 전송

로그에서 Trace ID 확인

PLAINTEXT
2026-03-19 INFO [order-service,abc123def456,span789012] - 주문 생성 시작
2026-03-19 INFO [order-service,abc123def456,span789012] - 결제 서비스 호출
2026-03-19 INFO [payment-service,abc123def456,span345678] - 결제 처리 중
2026-03-19 INFO [order-service,abc123def456,span789012] - 주문 생성 완료

abc123def456가 Trace ID입니다. 이 ID로 로그를 grep하면 여러 서비스에 걸친 전체 요청 흐름 을 한눈에 볼 수 있습니다.

Trace 전파 방식

HTTP 헤더를 통한 전파

서비스 간 HTTP 호출 시 Trace 정보가 헤더로 전파됩니다.

W3C Trace Context (표준):

PLAINTEXT
traceparent: 00-abc123def456-span789012-01

B3 Propagation (Zipkin 기본):

PLAINTEXT
X-B3-TraceId: abc123def456
X-B3-SpanId: span789012
X-B3-ParentSpanId: parentspan123
X-B3-Sampled: 1

Spring Boot는 RestTemplate, WebClient, OpenFeign 등에서 자동으로 이 헤더를 추가하고 파싱합니다.

JAVA
@Service
public class OrderService {

    private final WebClient.Builder webClientBuilder;

    public UserInfo getUser(Long userId) {
        // WebClient 호출 시 자동으로 Trace 헤더가 추가됨
        return webClientBuilder.build()
                .get()
                .uri("http://user-service/api/users/{id}", userId)
                .retrieve()
                .bodyToMono(UserInfo.class)
                .block();
    }
}

메시지 브로커를 통한 전파

Kafka, RabbitMQ 등 메시지 브로커에도 Trace 정보를 전파할 수 있습니다.

JAVA
// Kafka 프로듀서 — 메시지 헤더에 Trace 정보 자동 추가
kafkaTemplate.send("order-events", orderEvent);

// Kafka 컨슈머 — 메시지 헤더에서 Trace 정보 자동 파싱
@KafkaListener(topics = "order-events")
public void handleOrderEvent(OrderEvent event) {
    // 이 메서드 내에서 로그를 찍으면 프로듀서와 같은 Trace ID가 출력됨
    log.info("주문 이벤트 처리: {}", event.getOrderId());
}

Zipkin으로 시각화

Zipkin은 Trace 데이터를 수집하고 시각화하는 서버입니다.

Zipkin 실행

BASH
# Docker로 간단 실행
docker run -d -p 9411:9411 openzipkin/zipkin

Zipkin UI

http://localhost:9411에서 웹 UI를 통해 확인할 수 있는 것:

  • **Trace 검색 **: 서비스, 시간, 지연 시간으로 Trace 검색
  • ** 타임라인 뷰 **: Span들의 시작/종료 시점과 소요 시간을 타임라인으로 시각화
  • ** 서비스 의존 관계 **: 서비스 간 호출 관계를 그래프로 표시
  • ** 에러 추적 **: 에러가 발생한 Span 하이라이트

Jaeger — Zipkin 대안

Jaeger는 CNCF 프로젝트로, Zipkin과 유사하지만 더 풍부한 기능을 제공합니다.

BASH
docker run -d --name jaeger \
  -p 16686:16686 \
  -p 4317:4317 \
  jaegertracing/all-in-one:latest

Jaeger의 추가 기능:

  • 적응형 샘플링
  • 서비스 성능 모니터링 (SPM)
  • 비교 분석 (두 Trace 비교)

커스텀 Span 생성

자동 Span 외에 직접 Span을 생성할 수도 있습니다.

JAVA
@Service
public class OrderService {

    private final Tracer tracer;

    public Order createOrder(OrderCreateRequest request) {
        // 커스텀 Span 생성
        Span customSpan = tracer.nextSpan().name("validate-order").start();

        try (Tracer.SpanInScope ws = tracer.withSpan(customSpan)) {
            validateOrder(request);
        } finally {
            customSpan.end();
        }

        // 나머지 로직...
    }
}

또는 어노테이션으로 간단하게:

JAVA
@Observed(name = "order.validation",
          contextualName = "validate-order")
public void validateOrder(OrderCreateRequest request) {
    // 이 메서드 실행이 자동으로 Span으로 기록됨
}

샘플링 전략

운영 환경에서 모든 요청을 추적하면 성능 오버헤드와 저장 비용이 커집니다.

YAML
management:
  tracing:
    sampling:
      probability: 0.1  # 10%만 샘플링
샘플링 비율용도
1.0 (100%)개발/스테이징 환경
0.1 (10%)일반적인 운영 환경
0.01 (1%)대규모 트래픽 운영 환경

에러가 발생한 요청은 샘플링 비율과 관계없이 항상 추적하는 것이 좋습니다.

실무 팁

  • ** 로그에 Trace ID를 포함 **하면 로그 검색 시 전체 요청 흐름을 추적할 수 있습니다
  • 운영에서 ** 샘플링 비율을 적절히** 조절하세요 (너무 높으면 성능 저하, 너무 낮으면 추적 누락)
  • Trace 데이터와 ** 메트릭을 연계 **하면 "느린 API의 어떤 Span이 병목인지"를 정밀하게 분석할 수 있습니다
  • ELK Stack과 연동하여 ** 로그 + Trace를 통합** 검색하는 환경을 구축하세요

주의할 점

1. 샘플링 비율을 100%로 설정하면 운영 환경에서 성능과 저장 비용이 급증한다

모든 요청을 추적하면 Trace 데이터의 양이 폭발적으로 늘어나 네트워크 대역폭과 Zipkin/Jaeger의 저장소 비용이 크게 증가합니다. 동시에 각 요청마다 Span 생성 오버헤드가 추가되어 응답 시간이 미세하게 늘어날 수 있습니다. 운영 환경에서는 0.1(10%) 정도로 시작하여 트래픽에 맞게 조절하세요.

2. 비동기 처리(@Async, CompletableFuture)에서는 Trace 컨텍스트가 자동 전파되지 않을 수 있다

새 스레드에서 실행되는 비동기 코드에서는 MDC에 저장된 Trace ID가 전파되지 않아 로그에서 추적이 끊길 수 있습니다. @Async를 사용할 때 Micrometer의 LazyTraceExecutor나 적절한 TaskDecorator를 설정해야 Trace 컨텍스트가 비동기 스레드까지 이어집니다.

3. Spring Cloud Sleuth에서 Micrometer Tracing으로 마이그레이션할 때 설정 키가 완전히 바뀐다

Spring Boot 3으로 업그레이드하면서 Sleuth의 spring.sleuth.* 설정이 모두 management.tracing.*으로 변경되었습니다. 기존 설정을 그대로 두면 샘플링이 동작하지 않거나 Trace 데이터가 전송되지 않는 문제가 발생합니다. 마이그레이션 시 설정 키를 꼼꼼히 변환해야 합니다.

정리

  • Trace ID 는 전체 요청 흐름을, Span ID 는 개별 작업 단위를 식별합니다
  • Micrometer Tracing은 자동으로 HTTP/메시지를 통해 Trace 정보를 전파합니다
  • Zipkin/Jaeger 로 서비스 간 호출 관계와 지연 시간을 시각화할 수 있습니다
  • 운영에서는 샘플링 비율 조절 로 성능과 가시성의 균형을 잡으세요
댓글 로딩 중...