
개요
마이크로서비스 환경에서 장애가 발생했을 때 "어느 서비스에서 병목이 생겼는가?"를 파악하는 데 수십 분을 소비한 경험이 있으신가요? 서비스 수가 늘어날수록 로그만으로 문제의 근본 원인을 추적하기는 점점 어려워집니다. OpenTelemetry(OTel)는 이 문제를 해결하기 위해 등장한 클라우드 네이티브 컴퓨팅 재단(CNCF, Cloud Native Computing Foundation)에서 만든 오픈소스 표준으로, 분산 시스템에서 트레이스(Trace), 메트릭(Metric), 로그(Log)를 단일 표준으로 수집하고 내보낼 수 있게 해줍니다.
이 글에서는 OpenTelemetry의 핵심 개념을 이해하고, Spring Boot 애플리케이션에 계측(Instrumentation)을 적용하여 Jaeger 백엔드로 분산 트레이싱 데이터를 시각화하는 과정을 단계별로 정리했습니다.
OpenTelemetry 핵심 개념과 구성 요소
OpenTelemetry를 효과적으로 활용하려면 세 가지 핵심 구성 요소를 이해해야 합니다.
- Trace: 단일 요청이 여러 서비스를 거치는 전체 경로를 나타냅니다. 하나의 Trace는 여러 Span으로 구성되며, 각 Span은 특정 작업의 시작·종료 시간과 메타데이터를 담습니다.
- Metric: 시스템 성능을 반영하는 시간 경과에 따른 수치 측정값으로 CPU 사용률, 요청 수, 응답 지연 등 시계열 수치 데이터입니다.
- Log: 특정 시점에 발생한 개별 이벤트의 텍스트 기록한 것으로 기존의 구조화된 로그 데이터를 Trace와 연결(Correlation)할 수 있도록 표준화합니다.
OpenTelemetry의 데이터 파이프라인은 다음과 같이 동작합니다.
[애플리케이션] → SDK/Agent → [OTel Collector] → [Jaeger / Prometheus / Datadog 등]OTel Collector는 수집·변환·내보내기를 담당하는 중간 컴포넌트로, 백엔드 벤더를 변경해도 애플리케이션 코드를 수정할 필요가 없다는 것이 최대 장점입니다. 이 벤더 중립성이 OpenTelemetry가 업계 표준으로 자리 잡은 이유입니다.
Spring Boot 애플리케이션에 OpenTelemetry 적용하기
Spring Boot 3.x 환경에서는 opentelemetry-spring-boot-starter를 활용하면 최소한의 코드로 자동 계측을 적용할 수 있습니다. 먼저 build.gradle에 의존성을 추가합니다.
// build.gradle
dependencies {
// OpenTelemetry Spring Boot 자동 계측 스타터
implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:2.10.0'
}다음으로 application.yml에서 서비스 이름과 OTLP 엔드포인트를 설정합니다.
# application.yml
spring:
application:
name: order-service # Trace에서 서비스 식별자로 사용됨
otel:
exporter:
otlp:
endpoint: http://localhost:4317 # OTel Collector gRPC 엔드포인트
resource:
attributes:
deployment.environment: production
traces:
sampler: parentbased_traceidratio # 샘플링 전략 설정
sampler.arg: 0.5 # 50% 샘플링자동 계측만으로 부족한 경우, 비즈니스 로직 내부에 커스텀 Span을 추가할 수 있습니다.
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final Tracer tracer;
public OrderService(Tracer tracer) {
this.tracer = tracer;
}
public Order processOrder(String orderId) {
// 커스텀 Span 시작 — 결제 검증 단계를 별도로 추적
Span span = tracer.spanBuilder("validatePayment")
.setAttribute("order.id", orderId)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 결제 검증 로직 수행
return validateAndProcess(orderId);
} catch (Exception e) {
// 예외 정보를 Span에 기록
span.recordException(e);
throw e;
} finally {
span.end(); // 반드시 Span을 종료해야 데이터가 내보내집니다
}
}
}실행 결과: Jaeger UI에서 order-service > validatePayment 계층 구조의 Span이 생성되며, order.id 속성으로 특정 주문의 처리 흐름을 추적할 수 있습니다.
OTel Collector와 Jaeger 연동하기
OTel Collector와 Jaeger를 Docker Compose로 로컬 환경에 빠르게 구성할 수 있습니다. Jaeger는 Uber가 개발하여 CNCF에서 관리하는 오픈 소스 분산 추적(Distributed Tracing) 시스템으로, 마이크로서비스 아키텍처의 트랜잭션 흐름을 시각화하고 성능 병목 지점을 진단합니다.
# docker-compose.yml
version: '3.8'
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.115.0
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
command: ["--config=/etc/otel-collector-config.yaml"]
ports:
- "4317:4317" # gRPC OTLP 수신 포트
depends_on:
- jaeger
jaeger:
image: jaegertracing/all-in-one:1.63
ports:
- "16686:16686" # Jaeger UI
- "4317" # Collector 내부 통신용# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317 # 애플리케이션으로부터 수신
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true # 로컬 환경에서만 사용
service:
pipelines:
traces:
receivers: [otlp]
exporters: [otlp/jaeger]docker compose up -d 실행 후 http://localhost:16686에 접속하면 Jaeger UI에서 서비스 간 요청 흐름, 지연 시간, 오류 발생 위치를 시각적으로 확인할 수 있습니다.
운영 환경 적용 시 고려 사항
현업 프로젝트에 OpenTelemetry를 도입할 때 주의해야 할 사항이 있습니다.
첫째, 도입 목적을 명확히 정의해야 합니다.
트레이싱, 메트릭, 로그 중 무엇을 통해 어떤 문제를 해결하려는지 사전에 합의해야 합니다. 목적 없이 도입하면 데이터는 수집되지만 장애 분석이나 성능 개선에 활용되지 않는 상태가 됩니다.
둘째, 메트릭과 스팬의 카디널리티를 관리해야 합니다.
userId, orderId, requestId와 같은 값들을 무분별하게 attribute나 label로 사용하면 저장소 부하와 비용이 급격히 증가합니다. 집계가 필요한 값과 단순 추적용 값을 명확히 구분해야 합니다.
셋째, 성능 오버헤드를 고려해야 합니다.
자동 계측은 편리하지만 모든 요청에 대해 스팬 생성, 컨텍스트 전파, exporter 전송 비용이 발생합니다. 고트래픽 서비스에서는 샘플링 전략을 사전에 설계해야 하며, 필요 시 일부 계측을 비활성화해야 합니다.
넷째, 샘플링 전략을 초기에 결정해야 합니다.
Head-based sampling과 tail-based sampling은 수집 비용과 분석 가능성에 차이가 있습니다. 장애 분석 정확도와 시스템 부하 간의 균형을 고려해 선택해야 합니다.
다섯째, 컨텍스트 전파가 끊기는 지점을 점검해야 합니다.
비동기 처리, 메시지 큐, 배치 작업, 외부 SDK 사용 구간에서는 trace context가 손실되기 쉽습니다. 이러한 구간은 명시적으로 전파 설정을 확인해야 합니다.
여섯째, 벤더 종속성을 최소화해야 합니다.
OpenTelemetry 자체는 벤더 중립적이지만, exporter 설정, 대시보드 구성, 커스텀 attribute 설계에 따라 특정 벤더에 의존하게 될 수 있습니다. 가능하면 표준 semantic convention을 따르는 것이 바람직합니다.
일곱째, 운영 프로세스와 연계해야 합니다.
트레이스와 메트릭이 수집되더라도 이를 언제, 누가, 어떤 기준으로 확인하는지가 정해져 있지 않으면 실효성이 떨어집니다. 장애 대응 및 성능 분석 흐름에 포함시켜야 합니다.
샘플링 전략 선택: 모든 트레이스를 수집하면 높은 트래픽 환경에서 스토리지 비용이 급증할 수 있습니다. parentbased_traceidratio로 비율 샘플링을 적용하거나, 오류가 포함된 트레이스를 우선 수집하는 Tail-based Sampling을 OTel Collector 레벨에서 구성하는 방법을 권장합니다.
Trace와 Log 연결(Correlation): MDC(Mapped Diagnostic Context)에 traceId와 spanId를 자동으로 삽입하면, 기존 로그 분석 도구(ELK 스택 등)와 OpenTelemetry 트레이싱을 연계하여 더 풍부한 디버깅 맥락을 확보할 수 있습니다. Spring Boot + Logback 환경에서는 opentelemetry-logback-appender가 이 작업을 자동으로 처리해 줍니다.
백프레셔(Backpressure) 관리: OTel Collector의 batch 프로세서를 설정하여 데이터를 일괄 전송하면 네트워크 오버헤드를 줄이고 백엔드 부하를 조절할 수 있습니다.
맺음말
OpenTelemetry는 특정 벤더에 종속되지 않고 분산 시스템의 관측 가능성(Observability)을 확보할 수 있는 가장 현실적인 표준입니다. Spring Boot 자동 계측으로 빠르게 시작한 뒤, 커스텀 Span과 속성으로 비즈니스 맥락을 추가하고, OTel Collector를 통해 백엔드를 유연하게 교체할 수 있다는 점에서 장기적으로 유지보수 비용을 크게 절감할 수 있습니다.
다음 단계로는 Prometheus와 Grafana를 연동한 메트릭 대시보드 구성이나 OpenTelemetry Operator for Kubernetes를 활용한 사이드카 자동 주입 방식을 탐색해 보시길 권장합니다. 관측 가능성 파이프라인을 코드로 정의하고 표준화하는 과정이 곧 안정적인 운영 환경을 만드는 첫걸음입니다.
'프로그래밍 PROGRAMMING > 자바 JAVA AND FRAMEWORKS' 카테고리의 다른 글
| GraalVM Native Image로 Spring Boot 시작 시간 줄이기 (0) | 2026.03.16 |
|---|---|
| HikariCP 커넥션 풀 최적화 설정법 (0) | 2026.03.14 |
| [JAVA] JVM GC 튜닝으로 자연시간 줄이기 (0) | 2026.03.09 |
| Spring Boot 테스트 전략: @SpringBootTest vs 슬라이스 테스트 선택 (0) | 2026.03.09 |
| Java HashMap 성능 최적화 초기 용량과 로드 팩터를 제대로 설정하는 법 (0) | 2026.03.07 |