웹서비스의 API가 정상적으로 응답한다고 해서 서비스의 모든 기능이 정상적으로 작동하고 있다는 뜻은 아니다.
HTTP 상태 코드가 200으로 반환되더라도 실제 응답을 받기까지 5초 이상 걸릴 수 있다.
개발자는 서버 로그를 확인한다. 오류 메시지는 없다. CPU 사용률도 정상이고 데이터베이스 연결도 유지되고 있다.
그런데 사용자는 여전히 서비스가 느리다고 말한다.
이런 상황에서는 단순한 로그 분석이나 서버 자원 모니터링만으로 원인을 찾기 어렵다.
특히 API Gateway, 여러 백엔드 서비스, 데이터베이스, 외부 API가 연결된 환경에서는 하나의 요청이 여러 시스템을 통과한다.
이번 글에서는 OpenTelemetry를 이용해 이러한 요청 흐름을 추적하고, 어느 단계에서 응답 지연이 발생하는지 확인할 수 있는 분산 추적(Distributed Tracing) 시스템의 설계 방법을 살펴본다.
1. API가 성공해도 서비스가 느릴 수 있는 이유
먼저 여러 서비스로 구성된 웹 애플리케이션을 생각해 보자.
사용자가 웹사이트에서 특정 정보를 검색하면 다음과 같은 과정이 실행된다.
[사용자 검색 요청] | v [API Gateway] | v [Search API] | +------> [Database] | +------> [External API] | v [Response] 각 시스템은 정상적으로 응답할 수 있다.
하지만 전체 요청 시간은 각 작업이 어떻게 실행되는지에 따라 달라진다.
예를 들어 다음과 같은 응답 시간이 측정되었다고 가정해 보자.
| 처리 단계 | 소요 시간 |
|---|---|
| API Gateway 처리 | 20ms |
| Search API 내부 처리 | 40ms |
| Database 조회 | 80ms |
| External API 호출 | 1,800ms |
| 기타 처리 | 60ms |
모든 작업이 순차 실행되고 측정 구간이 서로 겹치지 않는다고 가정하면 총 응답 시간은 약 2초다.
이 경우 핵심 문제는 External API 호출이다.
하지만 API 응답 결과만 확인하면 다음과 같이 표시될 수 있다.
HTTP Status: 200 Response Time: 2000ms 요청이 성공했다는 사실은 확인할 수 있지만 어느 단계에서 시간이 많이 사용됐는지는 알 수 없다.
이 문제를 해결하려면 개별 서버가 아니라 요청 전체의 흐름을 관찰해야 한다.
2. 로그, 메트릭, 트레이스는 무엇이 다를까?
시스템 관측성(Observability)을 구성하는 대표적인 데이터에는 로그(Logs), 메트릭(Metrics), 트레이스(Traces)가 있다.
세 가지는 비슷해 보이지만 서로 다른 질문에 답한다.
| 구분 | 주요 목적 | 확인할 수 있는 정보 |
|---|---|---|
| Logs | 특정 사건 기록 | 오류 메시지, 작업 결과 |
| Metrics | 시스템 상태 수치화 | 요청 수, 오류율, 지연 시간 |
| Traces | 요청 실행 경로 추적 | 서비스 간 호출 순서와 소요 시간 |
예를 들어 API 응답 지연이 증가했다고 가정해 보자.
Metrics는 평균 응답 시간과 P95 지연 시간이 증가했다는 사실을 보여줄 수 있다.
Logs는 특정 외부 API 호출에서 오류가 발생했다는 기록을 제공할 수 있다.
Traces는 하나의 요청이 어떤 서비스를 통과했고, 어느 작업에서 시간이 많이 사용됐는지 확인하는 데 도움을 준다.
세 가지는 서로 대체하는 기술이 아니다.
서로 연결해서 사용할 때 더 정확한 장애 분석이 가능하다.
3. Trace와 Span의 기본 구조
분산 추적을 이해하려면 Trace와 Span의 차이를 알아야 한다.
Trace는 하나의 요청과 관련된 전체 실행 흐름을 표현한다.
Span은 그 흐름 안에서 실행되는 개별 작업을 나타낸다.
예를 들어 다음 구조를 생각해 보자.
Trace ID: abc123 [HTTP GET /search] 2000ms | +-- [Search API Processing] 1980ms | +-- [Database Query] 80ms | +-- [External API Call] 1800ms | +-- [Result Formatting] 60ms 위 수치는 설명을 위한 가상 사례다.
실제 Trace에서는 Span의 시작·종료 시점과 부모·자식 관계를 함께 기록하므로, 각 Span의 소요 시간을 단순히 더해서 전체 요청 시간을 계산해서는 안 된다.
Trace ID
하나의 요청 흐름을 식별하는 ID다.
서로 다른 서비스에서 생성된 Span도 같은 Trace ID를 공유하면 하나의 요청 흐름으로 연결할 수 있다.
Span ID
개별 작업을 식별하는 ID다.
각 Span은 고유한 Span ID를 가지며 부모 Span과 연결될 수 있다.
Span Attributes
작업에 대한 추가 정보를 제공한다.
예를 들어 다음과 같은 데이터를 기록할 수 있다.
{ "http.request.method": "GET", "http.route": "/search", "http.response.status_code": 200 } Attribute는 요청 분석을 돕지만, 개인정보나 인증 토큰처럼 민감한 정보를 무분별하게 저장해서는 안 된다.
4. Context Propagation이 중요한 이유
분산 시스템에서는 요청이 여러 서비스로 이동한다.
하지만 각 서비스에서 새로운 Trace를 독립적으로 생성한다면 전체 실행 흐름을 연결하기 어렵다.
이를 해결하는 기술이 Context Propagation이다.
Context Propagation은 서비스 사이에서 추적 문맥을 전달하는 방식이다.
OpenTelemetry는 W3C Trace Context와 호환되는 추적 문맥을 지원한다.
HTTP 환경에서는 대표적으로 traceparent 헤더를 이용할 수 있다.
예시
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 이 값에는 다음 정보가 포함된다.
- Trace ID
- Parent Span ID
- Trace Flags
예를 들어 API Gateway에서 Search API를 호출할 때 추적 문맥을 전달하면 Search API는 같은 Trace에 연결되는 새로운 Span을 생성할 수 있다.
[API Gateway] Trace ID: A | | traceparent 전달 v [Search API] Trace ID: A | | traceparent 전달 v [External API] Trace ID: A 단, 외부 API가 전달받은 추적 문맥을 지원하고 Span을 실제로 생성해야 해당 서비스 내부 작업까지 추적할 수 있다.
지원하지 않는 외부 서비스라면 호출하는 애플리케이션에서 생성한 클라이언트 Span을 통해 요청에 걸린 시간을 관찰할 수 있다.
5. OpenTelemetry를 이용한 Python API 추적 예시
간단한 FastAPI 애플리케이션에 OpenTelemetry를 적용해 보자.
아래 예제는 콘솔에 Span을 출력하는 최소 구성이다.
프로덕션 환경에서는 OpenTelemetry Collector와 별도의 관측성 저장소를 연결하는 구조를 사용할 수 있다.
필요한 라이브러리
pip install fastapi uvicorn opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-fastapi FastAPI 예제
import time from fastapi import FastAPI from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) from opentelemetry.instrumentation.fastapi import ( FastAPIInstrumentor, ) app = FastAPI() resource = Resource.create({ "service.name": "search-api" }) provider = TracerProvider(resource=resource) provider.add_span_processor( BatchSpanProcessor(ConsoleSpanExporter()) ) trace.set_tracer_provider(provider) tracer = trace.get_tracer(__name__) FastAPIInstrumentor.instrument_app(app) @app.get("/search") def search(): with tracer.start_as_current_span( "database.query" ): time.sleep(0.08) with tracer.start_as_current_span( "external_api.call" ): time.sleep(1.8) with tracer.start_as_current_span( "response.format" ): time.sleep(0.06) return { "status": "success", "results": [] } 서버를 실행한다.
uvicorn main:app --reload 그리고 다음 주소로 요청을 보낸다.
http://127.0.0.1:8000/search 예제에서는 의도적으로 처리 시간을 지연시켰다.
실제로 데이터베이스나 외부 API를 호출하는 코드는 아니다.
하지만 OpenTelemetry가 개별 작업의 시작과 종료 시점을 기록하는 방식을 이해하는 데 사용할 수 있다.
예상되는 추적 구조
GET /search | +-- database.query | 80ms | +-- external_api.call | 1800ms | +-- response.format 60ms 이 예제를 통해 어떤 작업이 상대적으로 많은 시간을 사용하는지 확인할 수 있다.
실제 애플리케이션에서는 HTTP 클라이언트나 데이터베이스 라이브러리에 대한 자동 계측(Auto-Instrumentation)을 추가하는 방법도 있다.
6. OpenTelemetry Collector를 도입하는 이유
개별 애플리케이션이 추적 데이터를 관측성 저장소에 직접 전송하도록 만들 수도 있다.
하지만 서비스가 늘어나면 추적 데이터의 전송, 가공, 필터링을 각 애플리케이션에서 관리하기 어려워질 수 있다.
OpenTelemetry Collector는 이러한 텔레메트리 데이터를 수집하고 처리한 뒤 다른 시스템으로 전달하는 역할을 한다.
기본 아키텍처
[API Gateway] | v [Search API] --------+ | [Worker Service] ----+ | v [OTel Collector] | +---------+---------+ | | v v [Trace Backend] [Metrics Backend] Collector는 일반적으로 다음 구성 요소를 이용한다.
- Receiver: 데이터를 수신
- Processor: 데이터를 가공하거나 필터링
- Exporter: 데이터를 외부 시스템에 전달
- Pipeline: 위 구성 요소를 연결
예를 들어 애플리케이션은 OTLP를 통해 Collector에 데이터를 전송하고, Collector는 이를 추적 저장소로 전달하도록 구성할 수 있다.
이 구조는 애플리케이션의 비즈니스 로직과 텔레메트리 전달 구성을 분리하는 데 도움이 된다.
다만 Collector를 도입하면 해당 구성 요소의 가용성, 처리 용량, 백프레셔와 운영 비용도 함께 관리해야 한다.
7. 모든 요청을 저장해야 할까? Sampling 설계
서비스 트래픽이 증가하면 모든 요청의 Trace를 장기간 저장하는 것은 비용 부담이 될 수 있다.
이를 해결하기 위한 기술이 Sampling이다.
Sampling은 전체 Trace 중 일부를 선택해 기록하거나 저장하는 방식이다.
대표적으로 Head Sampling과 Tail Sampling이 있다.
Head Sampling
요청의 초기 단계에서 추적 데이터를 수집할지 결정한다.
장점은 초기에 데이터 양을 줄일 수 있다는 것이다.
단점은 요청의 최종 결과를 알기 전에 결정하므로 나중에 발생한 오류나 느린 요청을 놓칠 수 있다는 점이다.
Tail Sampling
요청에 속한 Span들이 수집된 뒤 Trace 전체 또는 상당 부분의 정보를 참고해 저장 여부를 결정한다.
예를 들어 다음과 같은 기준을 적용할 수 있다.
- 오류가 발생한 요청
- 처리 시간이 긴 요청
- 특정 서비스에서 발생한 요청
- 일부 정상 요청의 무작위 표본
Tail Sampling은 장애 분석에 유용하지만 더 많은 메모리와 처리 시간이 필요하다.
여러 Collector가 분산되어 있는 경우 같은 Trace의 Span이 동일한 샘플링 결정 지점으로 모이도록 설계하는 문제도 고려해야 한다.
Sampling 선택 기준
| 상황 | 검토할 방식 |
|---|---|
| 작은 서비스, 낮은 트래픽 | 전체 수집 또는 단순 Sampling |
| 높은 트래픽, 저장 비용 민감 | Head Sampling |
| 오류 및 느린 요청 분석 중심 | Tail Sampling |
| 다양한 요구 조건 혼합 | 계층적 수집 및 Sampling 정책 |
중요한 것은 Sampling 비율 자체가 아니다.
시스템에서 어떤 요청을 분석할 필요가 있는지 정의하고 그에 맞게 정책을 만드는 것이다.
8. Trace 데이터를 수집할 때 주의해야 할 보안 문제
관측성 시스템에는 운영 정보가 집중된다.
API 경로, 서비스 이름, 요청 특성, 오류 메시지 등이 추적 데이터에 기록될 수 있다.
따라서 Trace가 많을수록 무조건 좋다는 접근은 위험하다.
특히 다음 정보는 수집과 전달을 신중하게 검토해야 한다.
- Access Token
- API Key
- 비밀번호
- 세션 정보
- 개인정보
- 민감한 요청 본문
OpenTelemetry의 Baggage 기능을 사용할 때도 주의해야 한다.
Baggage는 서비스 간 요청 문맥에 추가 정보를 전달할 수 있는 기능이다.
그러나 HTTP 헤더를 통해 외부 시스템으로 전달될 가능성이 있으므로 민감한 값을 넣지 않는 것이 중요하다.
또한 외부에서 전달된 Baggage 값은 신뢰할 수 없는 입력으로 취급해야 한다.
운영 기준
- 민감한 요청 데이터를 기본적으로 수집하지 않는다.
- 필요한 Attribute만 명시적으로 기록한다.
- 수집 단계에서 민감한 값을 제거하거나 마스킹한다.
- 추적 데이터에 대한 접근 권한을 제한한다.
- 저장 기간과 보존 정책을 정의한다.
관측성을 높이기 위해 보안 수준을 낮추는 구조는 피해야 한다.
9. 실제 운영 장애 시나리오
웹사이트 검색 API의 응답 시간이 갑자기 증가했다고 가정해 보자.
기존에는 평균 200ms 정도였지만, 최근 일부 요청이 3초 이상 걸리고 있다.
그러나 HTTP 500 오류는 발생하지 않는다.
1단계: Metrics 확인
API의 P95 응답 시간이 증가했다는 사실을 확인한다.
하지만 어떤 내부 작업이 원인인지는 알 수 없다.
2단계: Trace 확인
느린 요청의 Trace를 조회한다.
GET /search Total: 3200ms | +-- Database Query: 90ms | +-- External API: 2950ms | +-- Processing: 160ms 외부 API 호출에서 대부분의 시간이 사용됐다는 사실을 확인한다.
3단계: Logs 연결
해당 Trace ID를 기준으로 애플리케이션 로그를 조회한다.
외부 API 호출 과정의 추가적인 시간 정보와 응답 상태를 확인한다.
4단계: 원인 분석
외부 서비스의 응답 지연, 네트워크 상태, 연결 풀, 호출 횟수 등을 조사한다.
분산 추적은 문제의 위치를 좁히는 데 도움을 주지만 근본 원인을 자동으로 확정해 주지는 않는다.
5단계: 개선 결과 검증
문제를 수정한 뒤 이전 배포와 새로운 배포의 응답 시간 및 Trace를 비교한다.
단순히 HTTP 오류가 사라졌는지만 보는 것이 아니라 실제 요청 처리 시간이 개선됐는지 확인한다.
10. 프로덕션 적용 전 체크리스트
추적 구조
- [ ] 주요 서비스에 Trace 계측이 적용되어 있는가?
- [ ] 서비스 간 Trace Context가 전달되는가?
- [ ] 핵심 작업이 의미 있는 Span으로 구분되는가?
- [ ] 데이터베이스 및 외부 API 호출을 관찰할 수 있는가?
- [ ] 로그와 Trace ID를 연결할 수 있는가?
데이터 수집
- [ ] Collector의 처리 용량을 고려했는가?
- [ ] 적절한 Sampling 전략을 선택했는가?
- [ ] 추적 데이터 저장 기간을 정의했는가?
- [ ] 수집 및 내보내기 실패를 감지할 수 있는가?
- [ ] 관측성 시스템 자체의 장애를 모니터링하는가?
보안
- [ ] API Key와 인증 토큰이 기록되지 않는가?
- [ ] 개인정보가 불필요하게 포함되지 않는가?
- [ ] Baggage에 민감한 값이 저장되지 않는가?
- [ ] 추적 데이터 접근 권한을 제한했는가?
- [ ] 운영 데이터의 보존 정책이 있는가?
장애 대응
- [ ] 느린 요청을 식별할 수 있는가?
- [ ] 오류 발생 요청을 추적할 수 있는가?
- [ ] 서비스 간 호출 관계를 확인할 수 있는가?
- [ ] 문제 발생 전후의 지연 시간을 비교할 수 있는가?
- [ ] Trace 데이터가 부족할 때 활용할 Metrics와 Logs가 있는가?
11. 모든 웹서비스에 분산 추적이 필요할까?
반드시 그렇지는 않다.
하나의 서버에서 실행되는 작은 웹사이트라면 구조화된 로그와 기본적인 성능 모니터링만으로도 충분할 수 있다.
그러나 다음과 같은 환경에서는 분산 추적을 고려할 가치가 있다.
- 여러 백엔드 서비스가 연결되어 있다.
- 외부 API 호출이 빈번하다.
- 백그라운드 작업과 비동기 처리가 많다.
- 특정 요청의 처리 시간이 간헐적으로 증가한다.
- 서비스 간 장애 원인을 추적하기 어렵다.
- 배포 후 성능 저하 원인을 빠르게 확인해야 한다.
OpenTelemetry를 도입하는 목적은 더 많은 데이터를 수집하는 것이 아니다.
문제가 발생했을 때 필요한 정보를 확인할 수 있도록 시스템을 설계하는 것이다.
결론
API 요청이 정상적으로 완료되었다는 사실과 서비스가 빠르게 작동한다는 사실은 다르다.
여러 서비스가 연결된 시스템에서는 하나의 요청이 어느 단계를 거쳤고 어떤 작업에서 시간이 사용됐는지 확인하는 것이 중요하다.
OpenTelemetry는 Trace와 Span을 이용해 요청 흐름을 관찰할 수 있는 표준화된 방법을 제공한다.
이를 통해 개발자는 개별 서버 로그만 확인하는 방식에서 벗어나 서비스 전체의 실행 경로를 분석할 수 있다.
다만 분산 추적만으로 모든 장애가 해결되는 것은 아니다.
Metrics, Logs, Sampling, Collector 운영, 보안 정책을 함께 설계해야 프로덕션 환경에서 활용할 수 있는 관측성 시스템을 구축할 수 있다.
좋은 관측성 설계의 목표는 모든 데이터를 기록하는 것이 아니라, 문제가 발생했을 때 어느 부분을 조사해야 하는지 빠르게 알 수 있도록 만드는 것이다.
웹사이트 디렉터리 서비스와 관측성
레이더 링크모음과 같은 주소모음안내사이트는 다양한 외부 웹사이트의 주소와 카테고리 정보를 정리해 제공하는 서비스 유형의 사례다.
이러한 디렉터리 유형의 서비스에서 검색 API, 데이터 조회, 외부 주소 상태 점검 등의 기능을 별도 시스템으로 운영한다면, 분산 추적을 통해 각 작업의 실행 경로와 응답 지연을 분석하는 구조를 고려할 수 있다.
실제 적용 범위는 서비스의 기술 구성과 운영 환경에 따라 달라진다.


