분산 트레이싱 마스터하기: 복잡한 시스템의 흐름을 꿰뚫어 보는 눈

안녕하세요! 10년 차 소프트웨어 엔지니어이자 기술 교육자로서, 오늘 여러분과 함께 다뤄볼 주제는 현대 소프트웨어 개발에서 그 중요성이 날마다 커지고 있는 '분산 트레이싱(Distributed Tracing)'입니다. 특히 마이크로서비스 아키텍처가 대세가 된 지금, 분산 트레이싱은 시스템의 건강 상태를 파악하고 문제를 해결하는 데 없어서는 안 될 필수 도구가 되었습니다.
1. 개념 소개: 정의, 탄생 배경, 왜 중요한지

정의
분산 트레이싱은 분산 시스템 내에서 단일 요청(request)이 여러 서비스와 컴포넌트를 거치며 어떻게 처리되는지 그 전체 흐름을 추적하고 시각화하는 기술입니다. 각 서비스에서 요청이 처리되는 시간, 호출 관계, 오류 발생 여부 등의 상세 정보를 기록하여, 복잡하게 얽힌 시스템의 내부 동작을 명확하게 보여줍니다.
탄생 배경
과거 모놀리식(Monolithic) 아키텍처에서는 하나의 애플리케이션 안에서 모든 로직이 실행되었기 때문에, 요청의 흐름을 추적하거나 특정 기능의 성능 문제를 진단하는 것이 상대적으로 쉬웠습니다. 하지만 2010년대 이후 클라우드 컴퓨팅과 마이크로서비스 아키텍처의 확산으로, 하나의 요청을 처리하기 위해 수십, 수백 개의 서비스가 서로 통신하는 복잡한 분산 시스템이 보편화되었습니다.
이러한 환경에서는 단순히 특정 서비스의 로그만으로는 전체 요청의 맥락을 이해하기 어렵고, 어디에서 지연이 발생했는지, 어떤 서비스가 오류의 근원인지 파악하는 것이 거의 불가능해졌습니다. 이러한 문제를 해결하기 위해 Google Dapper 논문을 시작으로 분산 트레이싱 기술이 발전하기 시작했으며, 이제는 분산 시스템의 '관측 가능성(Observability)'을 확보하기 위한 핵심 요소로 자리매김했습니다.
왜 중요한가?
분산 트레이싱이 중요한 이유는 다음과 같습니다.
- 성능 병목 지점 식별: 특정 요청이 느려질 때, 어떤 서비스 또는 어떤 코드 블록에서 가장 많은 시간이 소요되는지 정확히 파악하여 최적화할 수 있습니다.
- 오류 원인 분석: 요청 처리 중 오류가 발생했을 때, 어떤 서비스에서부터 오류가 시작되었고 어떻게 전파되었는지 시각적으로 추적하여 빠르고 정확하게 문제를 해결할 수 있습니다.
- 시스템 가시성 확보: 눈에 보이지 않던 서비스 간의 복잡한 호출 관계를 명확하게 보여주어, 개발자가 시스템의 전체적인 동작 방식을 이해하는 데 도움을 줍니다. 이는 새로운 기능을 개발하거나 기존 시스템을 개선할 때 매우 유용합니다.
- 사용자 경험 개선: 성능 저하의 원인을 신속하게 찾아 해결함으로써 최종 사용자에게 더 빠르고 안정적인 서비스를 제공할 수 있습니다.
2. 핵심 원리 설명 (비유와 다이어그램 활용)

분산 트레이싱의 핵심은 '트레이스(Trace)', '스팬(Span)', 그리고 '컨텍스트 전파(Context Propagation)' 세 가지 개념으로 설명할 수 있습니다.
-
트레이스 (Trace): 단일 요청이 시스템에 들어와 완전히 처리되기까지의 전체 여정을 나타냅니다. 예를 들어, 웹사이트에서 '상품 주문' 버튼을 눌렀을 때, 이 요청이 인증 서비스, 상품 서비스, 결제 서비스, 재고 서비스 등 여러 서비스를 거쳐 최종적으로 완료되는 모든 과정을 하나의 트레이스라고 볼 수 있습니다.
-
스팬 (Span): 트레이스를 구성하는 가장 기본적인 단위로, 요청의 한 부분 또는 특정 서비스 내에서의 단일 작업을 나타냅니다. 각 스팬은 시작 시간, 종료 시간, 작업 이름, 서비스 이름, 그리고 부모 스팬과의 관계(부모-자식 관계) 등을 포함합니다. 예를 들어, '결제 서비스' 내에서 '카드 승인'이라는 작업이 하나의 스팬이 될 수 있습니다.
- Trace ID: 트레이스를 고유하게 식별하는 ID입니다. 모든 스팬은 동일한 Trace ID를 공유합니다.
- Span ID: 각 스팬을 고유하게 식별하는 ID입니다.
- Parent Span ID: 현재 스팬을 호출한 부모 스팬의 ID입니다. 이를 통해 스팬들의 계층 구조를 형성합니다.
비유: 국제 특송 서비스의 여정 추적
분산 트레이싱을 이해하기 위해 국제 특송 서비스에 비유해볼 수 있습니다.
-
택배 송장 번호 (Trace ID): 해외에 있는 친구에게 소포를 보냈다고 가정해봅시다. 이 소포에는 고유한 송장 번호가 붙어있습니다. 이 송장 번호가 바로 'Trace ID'입니다. 소포가 어디를 거치든 이 번호는 변하지 않습니다.
-
각 물류 센터에서의 처리 과정 (Span):
- 소포가 국내 물류 센터 A에 도착하여 '접수' 처리됩니다 (스팬 1).
- 물류 센터 B로 '이동'합니다 (스팬 2).
- 물류 센터 B에서 '해외 발송 준비'가 됩니다 (스팬 3).
- 해외 물류 센터 C에 '도착'합니다 (스팬 4).
- 해외 물류 센터 D로 '이동'합니다 (스팬 5).
- 최종적으로 친구에게 '배송 완료'됩니다 (스팬 6).
각 물류 센터에서의 '접수', '이동', '준비', '도착', '배송 완료' 같은 개별 작업들이 바로 '스팬'입니다. 각 스팬은 언제 시작해서 언제 끝났는지, 어떤 물류 센터(서비스)에서 처리되었는지 등의 정보를 가지고 있습니다.
-
물류 시스템의 내부 기록 (Span ID, Parent Span ID): 각 물류 센터는 소포가 들어오고 나갈 때마다 기록을 남깁니다. '이동' 스팬은 '접수' 스팬의 다음 단계이므로, '이동' 스팬은 '접수' 스팬을 '부모 스팬'으로 가집니다. 이렇게 스팬들이 부모-자식 관계를 통해 전체 소포의 여정(트레이스)을 형성하게 됩니다.
-
송장 번호 전달 (Context Propagation): 소포가 물류 센터를 거칠 때마다 송장 번호(Trace ID)는 항상 소포와 함께 전달됩니다. 이 송장 번호가 없으면 물류 센터는 이 소포가 어떤 전체 여정의 일부인지 알 수 없습니다. 이처럼 Trace ID와 현재 Span ID 같은 트레이싱 컨텍스트 정보가 서비스 호출 시 함께 전달되는 것이 '컨텍스트 전파'입니다. 주로 HTTP 헤더와 같은 메타데이터를 통해 전달됩니다.
다이어그램: 마이크로서비스 환경에서의 트레이싱
사용자의 요청이 Gateway를 거쳐 User Service, Order Service, Payment Service로 전달되는 과정을 다이어그램으로 표현하면 다음과 같습니다. 각 화살표는 트레이싱 컨텍스트(Trace ID, Span ID)가 전달되는 것을 의미합니다.
+----------------+ +----------------+ +-----------------+ +-----------------+
| User Request | ----> | Gateway | ----> | User Service | ----> | Order Service |
+----------------+ +----------------+ +-----------------+ +-----------------+
^ |
| v
| +-----------------+
| | Payment Service |
| +-----------------+
|
+-------------------------------------------------------------+
(Trace ID, Span ID 전파)
이 요청은 하나의 Trace를 형성하며, 각 서비스에서의 작업은 Span으로 기록됩니다.
Gateway에서 요청을 받으면 새로운 Trace가 시작되고 초기 Span이 생성됩니다.Gateway가User Service를 호출할 때, Trace ID와GatewaySpan의 ID를User Service로 전달합니다.User Service는 전달받은 Trace ID를 사용하여 새로운 Span을 생성하고,GatewaySpan을 부모 Span으로 설정합니다.- 마찬가지로
User Service가Order Service를 호출할 때, Trace ID와User ServiceSpan의 ID를Order Service로 전달합니다. Order Service는 다시Payment Service를 호출하며 컨텍스트를 전파합니다.
모든 스팬이 완료되면, 트레이싱 백엔드(예: Jaeger, Zipkin, OpenTelemetry Collector)로 전송되어 Trace ID를 기준으로 연결되고 시각화됩니다.
3. 코드 예제 (Python)
분산 트레이싱 구현을 위해 OpenTelemetry를 사용하겠습니다. OpenTelemetry는 클라우드 네이티브 컴퓨팅 재단(CNCF) 프로젝트로, 벤더 중립적인 옵저버빌리티(Observability) 데이터(트레이스, 메트릭, 로그)를 수집하고 내보내는 표준을 제공합니다.
먼저 필요한 라이브러리를 설치합니다:
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-flask opentelemetry-instrumentation-requests
예제 1: 두 개의 Flask 서비스 간 트레이싱
1. trace_config.py (공통 트레이싱 설정 파일)
# trace_config.py
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
def configure_tracer(service_name):
# 리소스 설정: 서비스 이름과 같은 메타데이터
resource = Resource.create({
"service.name": service_name,
"environment": "development",
})
# 트레이서 프로바이더 설정
provider = TracerProvider(resource=resource)
trace.set_tracer_provider(provider)
# OTLP 익스포터 설정 (Jaeger나 Zipkin 등 OTLP를 지원하는 백엔드로 전송)
# 기본적으로 localhost:4317 (gRPC) 또는 localhost:4318 (HTTP/protobuf) 으로 전송됩니다.
span_exporter = OTLPSpanExporter()
# 배치 스팬 프로세서: 스팬을 모아서 한 번에 전송하여 오버헤드 감소
span_processor = BatchSpanProcessor(span_exporter)
provider.add_span_processor(span_processor)
# Flask 및 requests 라이브러리 자동 계측 (미들웨어처럼 동작)
FlaskInstrumentor().instrument()
RequestsInstrumentor().instrument()
# 트레이서 객체 반환
return trace.get_tracer(__name__)
2. service_a.py (첫 번째 서비스)
# service_a.py
from flask import Flask, jsonify
import requests
from trace_config import configure_tracer
# 서비스 A의 트레이서 설정
tracer = configure_tracer("service-a")
app = Flask(__name__)
@app.route('/hello')
def hello_from_a():
with tracer.start_as_current_span("hello-from-a-endpoint"):
app.logger.info("Service A received request.")
# Service B 호출 (트레이싱 컨텍스트가 자동으로 전파됨)
response = requests.get("http://127.0.0.1:5001/world")
data = response.json()
app.logger.info(f"Service A received response: {data}")
return jsonify({"message": "Hello from Service A", "data_from_b": data})
if __name__ == '__main__':
# Jaeger UI를 보려면 OpenTelemetry Collector와 Jaeger를 실행해야 합니다.
# docker run -p 4317:4317 -p 16686:16686 jaegertracing/all-in-one:latest
app.run(port=5000, debug=True)
3. service_b.py (두 번째 서비스)
# service_b.py
from flask import Flask, jsonify
from trace_config import configure_tracer
import time
# 서비스 B의 트레이서 설정
tracer = configure_tracer("service-b")
app = Flask(__name__)
@app.route('/world')
def world_from_b():
with tracer.start_as_current_span("world-from-b-endpoint"):
app.logger.info("Service B received request.")
# 의도적인 지연 추가
time.sleep(0.05)
return jsonify({"message": "World from Service B", "timestamp": time.time()})
if __name__ == '__main__':
app.run(port=5001, debug=True)
실행 방법:
- Jaeger All-in-One Docker 컨테이너 실행:
docker run -p 4317:4317 -p 16686:16686 jaegertracing/all-in-one:latest service_b.py실행:python service_b.pyservice_a.py실행:python service_a.py- 웹 브라우저에서
http://127.0.0.1:5000/hello에 접속합니다. http://localhost:16686(Jaeger UI)에 접속하여 'service-a' 또는 'service-b'를 선택하고 'Find Traces'를 클릭하면 트레이스 데이터를 시각적으로 확인할 수 있습니다.
이 예제에서는 FlaskInstrumentor와 RequestsInstrumentor가 자동으로 HTTP 요청/응답에서 트레이싱 컨텍스트를 추출하고 주입해주므로, 개발자가 직접 헤더를 조작할 필요가 없습니다.
예제 2: 사용자 정의 스팬과 태그 추가
이번에는 특정 로직에 대한 사용자 정의 스팬을 생성하고, 유용한 정보를 태그로 추가하는 방법을 보여줍니다.
# service_b_with_custom_span.py (service_b.py를 수정)
from flask import Flask, jsonify
from trace_config import configure_tracer
import time
import random
# 서비스 B의 트레이서 설정
tracer = configure_tracer("service-b")
app = Flask(__name__)
@app.route('/world')
def world_from_b():
# 현재 활성화된 스팬을 부모로 하는 새로운 스팬 생성
with tracer.start_as_current_span("world-from-b-endpoint") as parent_span:
app.logger.info("Service B received request.")
parent_span.set_attribute("http.method", "GET")
parent_span.set_attribute("http.target", "/world")
# 비즈니스 로직에 대한 커스텀 스팬 생성
with tracer.start_as_current_span("process_data") as data_processing_span:
data_processing_span.set_attribute("data.size", 100)
data_processing_span.set_attribute("data.type", "json")
time.sleep(0.03) # 데이터 처리 시간
result = {"message": "World from Service B", "timestamp": time.time(), "processed_items": 10}
data_processing_span.set_attribute("processing.result", "success")
# 외부 API 호출 시뮬레이션
with tracer.start_as_current_span("call_external_api") as api_span:
api_span.set_attribute("api.name", "random_generator")
# 10% 확률로 에러 발생 시뮬레이션
if random.randint(1, 10) == 1:
api_span.set_attribute("error", True)
api_span.record_exception(ValueError("Simulated external API error"))
return jsonify({"error": "External API failed"}), 500
time.sleep(0.02) # API 호출 시간
api_span.set_attribute("api.status", "200 OK")
return jsonify(result)
if __name__ == '__main__':
app.run(port=5001, debug=True)
이 예제에서는 tracer.start_as_current_span()을 사용하여 특정 코드 블록을 새로운 스팬으로 묶고, span.set_attribute()를 통해 스팬에 추가적인 키-값 형태의 메타데이터(태그)를 추가했습니다. 또한 span.record_exception()을 사용하여 스팬 내에서 발생한 예외를 기록하는 방법을 보여줍니다. 이렇게 추가된 정보는 Jaeger UI에서 스팬을 클릭했을 때 상세 정보로 나타나, 디버깅에 큰 도움을 줍니다.
4. 실무 적용 사례
- 마이크로서비스 간 지연 원인 분석: 사용자가 특정 페이지 로드가 느리다고 불평할 때, 분산 트레이싱 툴을 통해 해당 요청의 트레이스를 확인하면 어떤 서비스에서 가장 많은 시간이 소요되었는지 시각적으로 쉽게 파악할 수 있습니다. 예를 들어,
Product Service의 데이터베이스 쿼리가 예상보다 오래 걸렸다는 것을 발견하여 쿼리 최적화를 진행할 수 있습니다. - 오류 발생 시 근본 원인 파악: 특정 API 호출이 500 에러를 반환했을 때, 트레이스를 통해 에러가 처음 발생한 서비스와 그 원인(예:
Payment Service에서 외부 결제 게이트웨이 호출 실패)을 찾아낼 수 있습니다. 관련 로그와 함께 보면 문제 해결 시간을 획기적으로 단축할 수 있습니다. - 새로운 기능의 성능 영향도 분석: 새로운 기능을 배포한 후, 해당 기능과 관련된 트레이스를 모니터링하여 전체 시스템에 예상치 못한 성능 저하를 일으키는지 확인하고, 문제가 있다면 어떤 서비스의 어떤 지점에서 발생하는지 빠르게 파악하여 롤백하거나 수정할 수 있습니다.
- A/B 테스트 결과 분석: A/B 테스트 시 두 가지 버전의 기능이 시스템 전체 성능에 미치는 영향을 비교 분석하여, 더 효율적인 버전이 무엇인지 객관적인 데이터를 기반으로 결정할 수 있습니다.
- SLA(서비스 수준 협약) 모니터링: 특정 비즈니스 트랜잭션의 엔드-투-엔드(End-to-End) 응답 시간을 트레이스 데이터를 통해 측정하고, SLA 위반 여부를 자동으로 감지하여 알림을 받을 수 있습니다.
5. 자주 하는 실수와 해결법
1. 컨텍스트 전파 누락
문제: 서비스 간 호출 시 Trace ID나 Span ID와 같은 트레이싱 컨텍스트가 제대로 전달되지 않아 트레이스가 중간에 끊어지는 현상. 해결법:
- 자동 계측(Auto-Instrumentation) 활용: OpenTelemetry와 같은 표준은 웹 프레임워크(Flask, Django, Spring 등)나 HTTP 클라이언트 라이브러리(requests, httpx 등)에 대한 자동 계측 기능을 제공합니다. 이를 활용하면 대부분의 컨텍스트 전파를 개발자가 직접 구현할 필요 없이 자동으로 처리해줍니다.
- 미들웨어/인터셉터 사용: 수동으로 구현해야 하는 경우, 각 서비스의 API 게이트웨이나 마이크로서비스 간 통신 레이어에 HTTP 헤더를 통해 컨텍스트를 주입/추출하는 미들웨어나 인터셉터를 구현합니다.
2. 샘플링 전략 부재 또는 부적절한 설정
문제: 모든 요청에 대해 트레이스 데이터를 수집하면, 특히 고처리량 시스템에서 엄청난 양의 데이터가 생성되어 스토리지 비용과 트레이싱 시스템의 부하가 커집니다. 해결법:
- 샘플링(Sampling) 전략 적용:
- 확률 기반 샘플링(Probabilistic Sampling): 전체 요청 중 일부(예: 1%)만 트레이스합니다. 가장 보편적인 방법입니다.
- 헤드 기반 샘플링(Head-based Sampling): 요청이 들어오는 시점에 트레이스 여부를 결정합니다. 빠르지만 중요한 트레이스를 놓칠 수 있습니다.
- 테일 기반 샘플링(Tail-based Sampling): 트레이스의 모든 스팬이 수집된 후, 트레이스 전체를 분석하여 (예: 오류가 발생했거나 특정 임계값 이상으로 느린 트레이스만) 유지할지 버릴지 결정합니다. 가장 정확하지만, 모든 스팬을 일단 수집해야 하므로 오버헤드가 더 큽니다.
- 오류 기반 샘플링(Error-based Sampling): 오류가 발생한 트레이스는 무조건 수집하고, 정상적인 트레이스는 확률적으로 수집합니다.
- 중요한 트랜잭션 우선: 핵심 비즈니스 로직이나 특정 사용자 그룹의 요청에 대해서는 더 높은 샘플링 비율을 적용하는 등 비즈니스 중요도에 따라 샘플링을 조절합니다.
3. Span 데이터 과부하
문제: 스팬에 너무 많은 정보를 태그나 로그로 추가하여 데이터 크기가 커지고, 이로 인해 트레이싱 시스템의 성능 저하 및 스토리지 비용 증가를 초래합니다. 해결법:
- 필요한 정보만 기록: 스팬에 추가하는 태그는 문제 해결에 필수적인 정보(예: 사용자 ID, 주문 ID, HTTP 상태 코드, DB 쿼리 결과 개수)로 제한합니다. 민감한 정보는 기록하지 않도록 주의합니다.
- 의미 있는 이름 지정: 스팬 이름은 해당 작업의 목적을 명확하게 나타내도록 간결하게 작성합니다 (예:
user_authentication,process_order_payment등). - 로그와 트레이스의 역할 분리: 상세한 애플리케이션 로그는 별도의 로깅 시스템(Elasticsearch, Loki 등)에 저장하고, 트레이스는 요청의 흐름과 성능 지표에 집중합니다. 필요할 경우 스팬에 로그 시스템으로 연결되는
log_id등을 태그로 추가하여 연관성을 높일 수 있습니다.
4. 트레이싱 시스템 자체의 성능 문제
문제: 트레이스 데이터를 수집하고 전송하는 과정이 애플리케이션 자체의 성능에 영향을 주거나, 트레이싱 백엔드가 부하를 견디지 못하는 경우. 해결법:
- 비동기 전송: 스팬 데이터를 트레이싱 백엔드로 전송할 때 블로킹(Blocking) 방식이 아닌 비동기 방식으로 처리하여 애플리케이션의 응답 시간에 영향을 주지 않도록 합니다. OpenTelemetry의
BatchSpanProcessor가 이 역할을 수행합니다. - 에이전트/컬렉터 사용: 애플리케이션에서 직접 백엔드로 전송하는 대신, 경량화된 에이전트(예: OpenTelemetry Collector)를 사용하여 스팬을 버퍼링하고, 필터링하며, 배치로 전송하도록 설정합니다. 이는 애플리케이션의 부하를 줄이고 트레이싱 시스템의 유연성을 높입니다.
- 리소스 최적화: 트레이싱 백엔드(Jaeger, Zipkin)의 데이터베이스(Cassandra, Elasticsearch 등) 및 인프라 리소스를 충분히 확보하고, 성능 모니터링을 통해 병목 지점을 해결합니다.
6. 더 공부할 리소스 추천
분산 트레이싱은 한 번에 모든 것을 마스터하기 어렵지만, 꾸준히 학습하고 실제 시스템에 적용해보면서 그 진가를 깨달을 수 있습니다.
- OpenTelemetry 공식 문서:
- OpenTelemetry Website
- OpenTelemetry Python Documentation
- 분산 트레이싱뿐만 아니라 메트릭, 로깅까지 관측 가능성 전반을 아우르는 표준이므로 꼭 살펴보시길 권장합니다.
- Jaeger 프로젝트:
- Jaeger Website
- 분산 트레이싱 데이터를 시각화하고 분석하는 데 널리 사용되는 오픈소스 툴입니다. 공식 문서의 "Getting Started" 부분을 통해 직접 설치하고 데이터를 확인해보는 것이 좋습니다.
- Zipkin 프로젝트:
- Zipkin Website
- Jaeger와 함께 대표적인 오픈소스 분산 트레이싱 툴입니다. Spring Cloud Sleuth 등과 연동이 잘 되어 있어 Java 생태계에서 많이 사용됩니다.
- Google Dapper 논문:
- Dapper, a Large-Scale Distributed Systems Tracing Infrastructure
- 분산 트레이싱의 개념과 설계 철학을 이해하는 데 매우 유용한 원본 논문입니다. 다소 기술적일 수 있지만, 깊이 있는 이해를 돕습니다.
- 클라우드 벤더별 트레이싱 서비스:
- AWS X-Ray, Google Cloud Trace, Azure Monitor Application Insights와 같은 클라우드 고유의 트레이싱 서비스를 사용한다면, 해당 서비스의 공식 문서를 통해 연동 및 활용법을 익히는 것이 좋습니다. 이들은 OpenTelemetry와도 연동이 가능합니다.
분산 트레이싱은 단순히 문제를 해결하는 도구를 넘어, 시스템의 동작 방식을 이해하고 더 나은 아키텍처를 설계하는 데 통찰력을 제공하는 강력한 기술입니다. 여러분의 서비스가 복잡해질수록 그 중요성을 더 크게 느끼실 겁니다. 꾸준히 학습하고 적용하며, 복잡한 시스템의 미로를 헤쳐나가는 '눈'을 키우시길 바랍니다!
