← 블로그 목록

Developer Knowledge

개발자가 알아야 할 지식: Trace Context, 요청 하나를 여러 서비스에서 따라가는 법

W3C Trace Context와 OpenTelemetry 관점에서 traceparent, trace id, span id, 샘플링, 로그 상관관계를 이해하고 분산 시스템 디버깅에 적용하는 방법을 정리한다.

World Wide Web Consortium
  • 개발자가 알아야 할 지식
  • Software Engineering
  • Observability
  • Distributed Systems
  • OpenTelemetry

왜 개발자가 알아야 하나

서비스가 하나의 서버에서만 동작할 때는 로그 파일 하나만 봐도 많은 문제가 보였다. 하지만 지금의 애플리케이션은 프론트엔드, API 서버, 인증 서비스, 결제 서비스, 메시지 큐, 데이터베이스, 외부 SaaS를 지나간다. 사용자는 “버튼을 눌렀더니 느렸다”고 말하지만, 개발자는 그 요청이 어느 서비스에서 시간을 썼는지, 어디서 재시도됐는지, 어떤 하위 호출이 실패했는지 찾아야 한다. 이때 로그만으로는 요청의 여행 경로를 복원하기 어렵다.

분산 추적은 이 문제를 풀기 위한 관측성 도구다. 한 사용자 요청이 여러 시스템을 지나갈 때 같은 추적 식별자를 붙이고, 각 단계의 작업을 span으로 기록한다. 그러면 “이 HTTP 요청이 API 게이트웨이에서 30ms, 사용자 서비스에서 80ms, 결제 서비스에서 1.8초, 외부 카드사 호출에서 1.6초를 썼다”처럼 전체 흐름을 볼 수 있다. 장애 분석뿐 아니라 성능 개선, 의존성 파악, 배포 후 회귀 탐지에도 직접 도움이 된다.

여기서 중요한 표준이 W3C Trace Context다. 여러 벤더와 라이브러리가 각자 다른 헤더를 쓰면 서비스 경계를 지나는 순간 추적이 끊긴다. Trace Context는 traceparenttracestate라는 HTTP 헤더를 통해 trace id, span id, trace flags 같은 핵심 정보를 전달하는 방식을 정의한다. 덕분에 OpenTelemetry, 클라우드 APM, 프록시, 서버 프레임워크가 같은 요청을 같은 추적으로 이어 붙일 수 있다.

개발자가 이 개념을 알아야 하는 이유는 추적이 운영팀만의 일이 아니기 때문이다. API 핸들러에서 하위 호출을 만들 때 컨텍스트를 전달하지 않으면 추적은 거기서 끊긴다. 비동기 작업을 큐에 넣을 때 trace id를 메시지에 싣지 않으면 원래 요청과 작업 결과를 연결하기 어렵다. 로그에 trace id를 남기지 않으면 장애 화면에서 로그 검색으로 넘어가는 흐름이 끊긴다. 좋은 관측성은 도구 구매보다 코드 경계마다 컨텍스트를 잃지 않는 습관에서 시작한다.

핵심 개념

Trace는 하나의 논리적 작업 전체를 뜻한다. 사용자가 /checkout을 호출하고, 그 요청이 재고 확인, 쿠폰 계산, 결제 승인, 알림 발송을 거친다면 이 전체가 하나의 trace가 된다. Trace id는 이 전체 작업을 식별하는 값이다. 같은 trace id를 가진 기록들은 “같은 사용자 요청에서 파생된 일”로 묶인다.

Span은 trace 안의 개별 작업 단위다. API 게이트웨이가 받은 요청, 주문 서비스의 핸들러 실행, 결제 서비스로 보낸 HTTP 호출, 데이터베이스 쿼리 하나가 각각 span이 될 수 있다. Span id는 그 작업 하나를 식별하고, parent span id는 이 작업을 호출한 상위 작업을 가리킨다. 이 부모-자식 관계가 모이면 트리처럼 요청의 실행 경로가 보인다.

W3C Trace Context에서 가장 눈에 띄는 헤더는 traceparent다. 형식은 버전, trace id, parent id, trace flags를 하이픈으로 나눈 값이다. 예를 들면 다음처럼 보인다.

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

여기서 가운데 긴 값이 trace id이고, 그 다음 값이 현재 부모 span id다. 마지막 flags의 샘플링 비트는 이 trace가 기록 대상으로 선택됐는지 알려준다. 개발자가 직접 이 문자열을 조립할 일은 많지 않다. 보통 OpenTelemetry SDK나 프레임워크 미들웨어가 생성하고 전파한다. 그래도 의미를 알고 있으면 프록시 설정, 로그, HTTP 디버깅에서 추적이 왜 끊겼는지 훨씬 빨리 찾을 수 있다.

tracestate는 벤더별 추가 정보를 담기 위한 헤더다. 표준 공통 정보는 traceparent에 넣고, 특정 관측성 시스템이 필요한 세부 상태는 tracestate에 보관한다. 이 분리는 중요하다. 모든 시스템이 반드시 이해해야 하는 최소 정보와, 특정 도구가 쓰는 확장 정보를 구분해야 서로 다른 도구가 섞인 환경에서도 추적이 이어진다.

OpenTelemetry는 이 표준을 실제 코드와 운영 환경에서 쓰기 쉽게 만든 생태계다. SDK는 span 생성, 컨텍스트 전파, exporter, 샘플링 설정을 제공한다. Collector는 애플리케이션에서 나온 trace, metric, log 데이터를 받아 여러 백엔드로 전달한다. 핵심은 애플리케이션 코드가 특정 APM 업체의 형식에 갇히지 않고 표준 데이터 모델과 전파 방식을 사용할 수 있다는 점이다.

작은 예시 또는 체크리스트

예를 들어 Node.js API 서버가 주문 요청을 받고 결제 서비스로 HTTP 호출을 보낸다고 하자. 이상적인 흐름은 이렇다.

  • 들어온 요청에서 traceparent를 읽어 현재 trace 컨텍스트를 복원한다.
  • 주문 처리 span을 시작하고, 필요한 속성에는 http.route, user.plan, order.region처럼 디버깅에 도움이 되는 낮은 카디널리티 값을 붙인다.
  • 결제 서비스 호출을 만들 때 현재 컨텍스트를 HTTP 헤더에 주입한다.
  • 결제 서비스는 그 헤더를 읽고 같은 trace id 아래에 새 span을 만든다.
  • 로그에는 trace_idspan_id를 같이 남겨 추적 화면과 로그 검색을 오갈 수 있게 한다.
  • 오류가 발생하면 span status와 예외 정보를 기록하되, 비밀번호나 토큰 같은 민감값은 절대 넣지 않는다.

직접 점검할 때는 다음 질문이 유용하다.

  • 모든 인바운드 HTTP 요청에서 trace 컨텍스트를 추출하는가?
  • 모든 아웃바운드 HTTP, gRPC, 메시지 큐 호출에 컨텍스트를 주입하는가?
  • 비동기 작업, 배치 작업, 이벤트 핸들러에서도 원래 trace와 연결할 방법이 있는가?
  • 로그에 trace id가 포함되어 추적 화면에서 바로 검색할 수 있는가?
  • 샘플링 정책이 너무 낮아 중요한 장애 trace가 사라지지 않는가?
  • span 속성에 사용자 개인정보나 높은 카디널리티 값이 무분별하게 들어가지 않는가?

실무에서 자주 생기는 오해

  • 추적 도구를 설치하면 자동으로 모든 문제가 보인다고 생각한다. 자동 계측은 시작점일 뿐이다. 비즈니스적으로 중요한 작업, 큐 메시지, 커스텀 클라이언트, 내부 SDK는 수동 계측이 필요할 수 있다.

  • 모든 요청을 100% 추적해야 한다고 믿는다. 트래픽이 적으면 가능하지만, 대규모 서비스에서는 비용과 저장 용량이 문제가 된다. 샘플링은 필요하다. 대신 오류, 느린 요청, VIP 경로처럼 중요한 이벤트를 더 잘 보존하는 정책이 필요하다.

  • Span 이름에 너무 많은 값을 넣는다. GET /users/12345처럼 사용자별 값이 이름에 들어가면 검색과 집계가 어려워진다. GET /users/{id}처럼 낮은 카디널리티 이름을 쓰고, 필요한 식별자는 신중하게 속성으로 분리한다.

  • Trace id만 있으면 로그가 필요 없다고 생각한다. Trace는 구조와 시간 흐름을 보여주고, 로그는 세부 상태와 의사결정 흔적을 보여준다. 둘은 대체재가 아니라 서로 연결되어야 하는 데이터다.

  • 프론트엔드에서 백엔드로 trace를 넘기는 일을 가볍게 본다. 브라우저, CDN, API 게이트웨이를 거치며 헤더가 빠지면 사용자 경험과 서버 처리 시간을 한 화면에서 보기 어렵다. CORS와 보안 정책을 고려해 허용할 전파 헤더를 명확히 해야 한다.

  • 민감정보를 span 속성에 넣어도 내부 도구라 괜찮다고 여긴다. 관측성 데이터는 여러 시스템으로 복제되고 오래 남을 수 있다. 이메일, 전화번호, 토큰, 원문 요청 바디는 기본적으로 넣지 않는 쪽이 안전하다.

오늘 바로 적용해보기

먼저 서비스에서 가장 자주 장애를 분석하는 사용자 흐름 하나를 고르자. 로그인, 결제, 검색, 파일 업로드, AI 작업 실행처럼 여러 시스템을 지나가는 흐름이 좋다. 그 흐름의 시작점에서 trace가 만들어지는지, 하위 HTTP 호출과 큐 메시지까지 같은 trace id가 이어지는지 확인한다.

둘째, 로그 포맷을 점검하자. 장애가 났을 때 APM 화면에서 trace id를 복사해 로그 검색에 넣을 수 있어야 한다. 반대로 로그에서 특정 오류를 봤을 때 trace 화면으로 이동할 수도 있어야 한다. 이 연결이 없으면 추적과 로그가 각각 따로 노는 화면이 된다.

셋째, 샘플링 정책을 제품 현실에 맞게 조정하자. 모든 요청을 남기기 어렵다면 기본 샘플링률을 낮추되, 오류 응답과 긴 지연 시간 요청은 보존하는 tail sampling을 검토한다. 초기에는 과하게 복잡한 정책보다 “중요한 장애를 놓치지 않는가”에 집중하는 편이 낫다.

넷째, 코드 리뷰에 컨텍스트 전파 질문을 추가하자. “이 함수가 다른 서비스나 큐를 호출할 때 trace context를 넘기는가?”, “이 비동기 작업은 원래 요청과 연결되는가?”, “로그에 trace id가 남는가?” 같은 질문은 장애 대응 시간을 실제로 줄인다.

다섯째, 민감정보 규칙을 문서화하자. 어떤 span 속성은 허용되고 어떤 값은 금지되는지 팀 기준이 있어야 한다. 관측성은 더 많이 보는 기술이지만, 아무 데이터나 더 많이 저장하는 기술은 아니다.

더 알아보기

오늘의 takeaway

분산 시스템에서 요청을 끝까지 따라가려면 로그를 더 많이 남기는 것보다, 서비스 경계마다 trace context를 잃지 않는 설계가 먼저다.