Spring OpenTelemetry 분산 추적

OpenTelemetry란?

OpenTelemetry(OTel)는 CNCF가 관리하는 관측 가능성(Observability) 표준으로, Traces, Metrics, Logs 세 가지 신호를 통합 수집하는 벤더 중립 프레임워크입니다. Spring Boot 3.x에서는 Micrometer Tracing을 통해 OpenTelemetry를 네이티브로 지원하며, 기존 Spring Cloud Sleuth를 완전히 대체합니다.

이 글에서는 Spring Boot + OpenTelemetry 연동, 분산 추적(Distributed Tracing) 설정, 커스텀 Span 생성, 그리고 Jaeger/Tempo 백엔드 연동까지 실전 중심으로 다루겠습니다.

Spring Cloud Sleuth → Micrometer Tracing

항목 Sleuth (Spring Boot 2.x) Micrometer Tracing (3.x)
프로젝트 Spring Cloud Sleuth Micrometer Tracing + OTel Bridge
추적 ID 전파 B3 Propagation W3C Trace Context (기본)
백엔드 Zipkin 중심 OTel Collector → 모든 백엔드
자동 계측 Spring 전용 OTel Java Agent (범용)
상태 유지보수 종료 활발한 개발 중

의존성 설정

// build.gradle.kts — Spring Boot 3.2+
dependencies {
    // Micrometer Tracing + OTel Bridge
    implementation("io.micrometer:micrometer-tracing-bridge-otel")
    
    // OTel Exporter (OTLP 프로토콜)
    implementation("io.opentelemetry:opentelemetry-exporter-otlp")
    
    // Spring Boot Actuator (메트릭 + 헬스체크)
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    
    // 자동 계측: JDBC, WebClient, RestClient 등
    implementation("io.micrometer:micrometer-tracing")
}

// 또는 Spring Boot BOM으로 버전 관리
dependencyManagement {
    imports {
        mavenBom("io.opentelemetry:opentelemetry-bom:1.35.0")
    }
}

application.yml 설정

# application.yml
management:
  tracing:
    sampling:
      probability: 1.0        # 개발: 100%, 운영: 0.1 (10%)
    propagation:
      type: w3c                # W3C Trace Context (기본)
  otlp:
    tracing:
      endpoint: http://otel-collector:4318/v1/traces
    metrics:
      endpoint: http://otel-collector:4318/v1/metrics

  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus

# 서비스 이름 설정 (추적 UI에 표시)
spring:
  application:
    name: order-service

# 로그에 traceId, spanId 자동 포함
logging:
  pattern:
    level: "%5p [${spring.application.name},%X{traceId},%X{spanId}]"

자동 계측 범위

Spring Boot + Micrometer Tracing은 아래 컴포넌트를 자동으로 계측합니다.

// 자동 계측 대상 (설정만으로 Span 자동 생성):
// ✅ Spring MVC 컨트롤러 (HTTP 요청/응답)
// ✅ RestClient / WebClient (외부 HTTP 호출)
// ✅ JDBC (SQL 쿼리)
// ✅ Spring Data JPA Repository
// ✅ @Async 비동기 메서드
// ✅ @Scheduled 스케줄러
// ✅ Spring Kafka / RabbitMQ
// ✅ Spring Security 필터

// 로그 출력 예시:
// INFO [order-service,6f3c4b2a1d8e9f0a,2c5d7e8f1a3b4c6d]
//      OrderController: Creating order for user 42
//
// traceId: 6f3c4b2a1d8e9f0a (요청 전체 추적)
// spanId:  2c5d7e8f1a3b4c6d (개별 작업 단위)

커스텀 Span 생성

import io.micrometer.observation.annotation.Observed;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.Span;

@Service
@RequiredArgsConstructor
public class OrderService {

    private final Tracer tracer;
    private final PaymentClient paymentClient;
    private final InventoryClient inventoryClient;

    // 방법 1: @Observed 어노테이션 (간단)
    @Observed(name = "order.process",
              contextualName = "process-order",
              lowCardinalityKeyValues = {"order.type", "standard"})
    public OrderResult processOrder(OrderRequest request) {
        validateOrder(request);
        PaymentResult payment = paymentClient.charge(request);
        inventoryClient.reserve(request.getItems());
        return createOrder(request, payment);
    }

    // 방법 2: Tracer API (세밀한 제어)
    public OrderResult processOrderManual(OrderRequest request) {
        Span span = tracer.nextSpan()
            .name("order.validate")
            .tag("order.id", request.getOrderId())
            .tag("user.id", String.valueOf(request.getUserId()))
            .start();

        try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
            validateOrder(request);
            span.event("validation.complete");

            // 하위 Span 자동 연결
            PaymentResult payment = paymentClient.charge(request);
            span.event("payment.charged");

            return createOrder(request, payment);
        } catch (Exception e) {
            span.error(e);  // 에러 기록
            throw e;
        } finally {
            span.end();     // 반드시 종료
        }
    }
}

서비스 간 Context 전파

// RestClient 사용 시 자동 전파 (Spring Boot 3.2+)
@Configuration
public class RestClientConfig {

    @Bean
    public RestClient restClient(RestClient.Builder builder) {
        return builder
            .baseUrl("http://payment-service:8080")
            .build();
        // Micrometer Tracing이 자동으로
        // traceparent 헤더 추가
    }
}

// HTTP 헤더로 전파되는 W3C Trace Context:
// traceparent: 00-6f3c4b2a1d8e9f0a3b4c5d6e7f8a9b0c-2c5d7e8f1a3b4c6d-01
//              버전-traceId-parentSpanId-flags

// Kafka 메시지에도 자동 전파
@Service
public class OrderEventPublisher {

    private final KafkaTemplate<String, OrderEvent> kafka;

    public void publishOrderCreated(Order order) {
        // 메시지 헤더에 traceId가 자동 포함
        kafka.send("order-events", order.getId(), 
            new OrderEvent("CREATED", order));
    }
}

// Consumer에서 자동으로 trace context 복원
@KafkaListener(topics = "order-events")
public void handleOrderEvent(OrderEvent event) {
    // 동일 traceId로 추적 가능!
    log.info("Processing event: {}", event);
}

OTel Collector 구성

# otel-collector-config.yml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024
  
  # 민감 정보 필터링
  attributes:
    actions:
      - key: db.statement
        action: hash      # SQL 해시 처리
      - key: http.request.header.authorization
        action: delete    # 인증 헤더 제거

exporters:
  # Jaeger로 내보내기
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true
  
  # Grafana Tempo로 내보내기
  otlp/tempo:
    endpoint: tempo:4317

  # Prometheus 메트릭
  prometheus:
    endpoint: 0.0.0.0:8889

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch, attributes]
      exporters: [otlp/jaeger, otlp/tempo]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]

Kubernetes 배포 구성

# OTel Collector를 DaemonSet 또는 Deployment로 배포
apiVersion: apps/v1
kind: Deployment
metadata:
  name: otel-collector
spec:
  replicas: 2
  template:
    spec:
      containers:
      - name: collector
        image: otel/opentelemetry-collector-contrib:0.95.0
        ports:
        - containerPort: 4317   # gRPC
        - containerPort: 4318   # HTTP
        - containerPort: 8889   # Prometheus
        volumeMounts:
        - name: config
          mountPath: /etc/otelcol-contrib/config.yaml
          subPath: config.yaml
      volumes:
      - name: config
        configMap:
          name: otel-collector-config
---
# 서비스 환경변수로 Collector 주소 주입
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  template:
    spec:
      containers:
      - name: app
        env:
        - name: MANAGEMENT_OTLP_TRACING_ENDPOINT
          value: "http://otel-collector:4318/v1/traces"
        - name: MANAGEMENT_TRACING_SAMPLING_PROBABILITY
          value: "0.1"   # 운영: 10% 샘플링

샘플링 전략

전략 설정 적합한 환경
전수 수집 probability: 1.0 개발/스테이징
비율 샘플링 probability: 0.1 일반 운영 (10%)
에러 우선 Tail Sampling (Collector) 에러 추적 집중
경로 기반 커스텀 Sampler /health 제외 등

마무리

Spring Boot 3.x + OpenTelemetry는 MSA 환경에서 분산 추적의 표준이 되었습니다. Micrometer Tracing Bridge를 통해 기존 Spring 생태계와 자연스럽게 통합되며, OTel Collector를 중간에 두면 Jaeger, Tempo, Datadog 등 어떤 백엔드로든 유연하게 전환할 수 있습니다. 운영 환경에서는 반드시 샘플링 비율 조정민감 정보 필터링을 설정하고, 로그의 traceId를 활용해 로그-트레이스 연계 분석을 구현하세요.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux