| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 분산시스템
- docker
- gradle
- Java 8
- 아키텍처
- 도커
- SpringCloud
- dockercompose
- 백엔드면접준비
- 트러블슈팅
- java
- 인프라
- CS
- 멀티모듈
- 마이크로서비스
- Database
- 컨테이너
- 마이그레이션
- springboot
- Flyway
- 자바
- 마이크로서비스아키텍처
- github actions
- PostgreSQL
- GCP
- ci/cd
- MSA
- 백엔드
- GitHub Packages
- 공통모듈
- Today
- Total
NYO_O
흩어진 로그를 한 번에: 분산 추적(Distributed Tracing) 아키텍처 본문
마이크로서비스 환경에서의 디버깅
모놀리식(Monolithic) 애플리케이션에서는 에러 추적이 비교적 단순합니다. 클라이언트의 요청 하나는 서버 내부의 단일 스레드(Thread)에 할당되어 처리되며, 예외가 발생하더라도 해당 스레드가 남긴 단일 로그 파일의 스택 트레이스(Stack Trace)만 위에서부터 차례대로 읽어 내려가면 원인을 파악할 수 있습니다.
하지만 마이크로서비스 아키텍처(MSA)에서는 상황이 완전히 다릅니다. 사용자가 API 게이트웨이로 보낸 '상품 결제' 요청 하나가 내부적으로 주문 서비스 → 재고 서비스 → 결제 서비스 → 배송 서비스 순서로 4번의 네트워크 홉(Hop)을 거친다고 가정해 보겠습니다.
만약 3번째 단계인 결제 서비스에서 NullPointerException이 발생해 클라이언트에게 HTTP 500 에러가 반환되었다면 어떨까요? 트래픽이 몰리는 운영 환경에서는 4대의 서로 다른 서버에 초당 수천 줄의 로그가 각자의 파일에 동시다발적으로 쌓이게 됩니다. 수천만 줄의 로그 더미 속에서, 오직 이 실패한 '특정 사용자의 결제 요청'이 4개의 서버를 관통하며 남긴 로그들만 정확히 발라내어 연결하는 것은 사실상 불가능에 가깝습니다.
이러한 분산 환경의 옵저버빌리티(Observability, 관측 가능성) 부재를 해결하기 위해 도입된 아키텍처 패턴이 바로 분산 추적(Distributed Tracing)입니다.
분산 추적(Distributed Tracing)의 등장과 Trace ID
분산 추적의 핵심 원리는 생각보다 아주 단순합니다. 여러 서비스에 흩어진 로그들을 하나로 묶어줄 수 있는 '고유한 꼬리표(Correlation ID)'를 붙여주는 것입니다. 이를 분산 추적 생태계에서는 Trace ID라고 부릅니다.
클라이언트의 최초 요청이 시스템의 단일 진입점(API 게이트웨이 등)에 도달하는 순간, 시스템은 무작위 식별자인 Trace ID를 생성합니다. 그리고 이 요청이 내부의 다른 마이크로서비스(주문, 재고, 결제 등)로 넘어갈 때마다 HTTP 헤더(Header)에 이 Trace ID를 담아서 함께 전달(Propagation)합니다.
결과적으로 이 요청에 관여한 모든 마이크로서비스들은 자신의 로컬 로그 파일에 로그를 출력할 때, 전달받은 동일한 Trace ID를 로그 문구 앞에 공통으로 찍게 됩니다. 장애가 발생했을 때 중앙 집중형 로그 시스템(ELK 스택 등)에서 이 Trace ID 하나만 검색하면, 4개의 서버에 흩어져 있던 로그들이 하나의 흐름으로 완벽하게 필터링되어 나타납니다.
작업의 구간과 인과관계를 정의하는 Span ID
Trace ID가 숲 전체를 보여준다면, 나무의 세부적인 연결 관계를 파악하기 위해서는 Span ID라는 또 다른 식별자가 필요합니다.
Trace ID는 전체 트랜잭션의 시작부터 끝까지 동일하게 유지되는 ID입니다. 반면 Span은 개별 마이크로서비스가 수행한 '논리적인 작업 구간'을 의미합니다. 주문 서비스가 요청을 받아 처리하는 구간이 하나의 Span이 되고, 재고 서비스가 처리하는 구간이 또 다른 Span이 되며, 각 Span은 고유한 Span ID를 발급받습니다.
단순히 식별자만 발급받는 것이 아니라, 서비스가 다른 서비스를 호출할 때 자신의 Span ID를 'Parent Span ID'로 엮어서 전달합니다. 이를 통해 중앙 모니터링 시스템은 단순히 "이 로그들이 같은 요청에서 발생했다(Trace ID)"를 넘어서, "API 게이트웨이가 주문 서비스를 불렀고, 주문 서비스가 재고 서비스를 불렀다"라는 명확한 인과관계의 트리(Tree) 구조를 그려낼 수 있게 됩니다.
시각화와 병목 추적: Zipkin과 Jaeger의 역할
Trace ID와 Span ID가 로그 텍스트에 출력되는 것만으로도 디버깅은 훨씬 수월해지지만, 대규모 분산 시스템에서는 이러한 추적 데이터를 시각화하여 병목(Bottleneck) 구간을 찾아내는 것이 더욱 중요합니다.
이러한 추적 데이터를 수집하고 시각화해 주는 대표적인 오픈소스 분산 추적 시스템이 바로 Zipkin과 Jaeger입니다.
각 마이크로서비스들은 로그를 파일에 남기는 것과 동시에, 백그라운드 스레드를 통해 자신의 추적 데이터(Trace ID, Span ID, 처리 소요 시간 등)를 Zipkin이나 Jaeger 서버로 비동기 전송합니다. 시각화 대시보드에 접속해 특정 Trace ID를 조회하면, 폭포수(Waterfall) 형태의 간트 차트(Gantt Chart)가 나타납니다.
이 차트를 통해 개발자는 "전체 요청 5초 중 주문 서비스에서 0.5초, 결제 서비스에서 무려 4.5초의 지연이 발생했다"는 사실을 직관적으로 파악할 수 있으며, 시스템의 성능 최적화 포인트를 정확히 타겟팅할 수 있게 됩니다.
Spring Boot 3 생태계와 Micrometer Tracing
자바 기반의 Spring Boot 생태계에서 이러한 분산 추적을 구현하는 방법은 최근 큰 패러다임 변화를 겪었습니다. 과거 Spring Boot 2.x 환경에서는 Spring Cloud Sleuth가 사실상의 표준이었으나, Spring Boot 3.x 버전부터는 Sleuth가 지원 종료(Deprecated)되고 그 역할이 Micrometer Tracing으로 완벽하게 이관되었습니다.
Spring Boot 3.x 환경에 Micrometer Tracing 의존성을 추가하기만 하면 마법 같은 일이 벌어집니다. 개발자가 비즈니스 코드에 어떠한 로직도 추가할 필요 없이, 프레임워크가 알아서 들어오는 HTTP 요청을 가로채어 Trace ID와 Span ID를 생성합니다. 그리고 이를 쓰레드 로컬(ThreadLocal) 기반의 로그 컨텍스트(MDC, Mapped Diagnostic Context)에 주입하여 모든 Logback 로그에 추적 ID가 출력되도록 만듭니다.
뿐만 아니라, RestTemplate, WebClient, OpenFeign 등을 이용해 다른 마이크로서비스로 외부 API를 호출할 때 자동으로 HTTP 헤더(예: b3 헤더 또는 traceparent 헤더)에 추적 ID를 심어서 전파해 줍니다. 개발자는 인프라적인 추적 로직에 신경 쓸 필요 없이 비즈니스 로직에만 집중할 수 있습니다.
마무리
오늘은 분산 환경에서 발생하는 옵저버빌리티의 한계를 극복하고, 여러 서비스에 걸친 요청의 흐름과 인과관계를 투명하게 추적할 수 있도록 돕는 분산 추적(Distributed Tracing) 아키텍처에 대해 알아보았습니다. 이제 우리는 장애가 발생하거나 시스템이 느려질 때, 헤매지 않고 정확히 어느 서비스의 어느 구간이 문제인지 집어낼 수 있는 강력한 무기를 얻었습니다.
지금까지 우리가 다룬 서비스 디스커버리, 로드 밸런싱, 서킷 브레이커, API 게이트웨이는 모두 '동기적인 HTTP 통신'을 전제로 시스템을 단단하게 만드는 기술들이었습니다. 하지만 아무리 이 통신망을 견고하게 만든다 한들, "주문이 완료되면 배송 서비스도 즉각적으로 호출되어야 한다"는 시간적 강결합(Temporal Coupling) 자체는 끊어낼 수 없습니다. 배송 서버가 배포 중이어서 잠시 내려가 있다면 주문도 함께 실패해야만 할까요?
다음 포스팅에서는 마이크로서비스 간의 통신 패러다임을 동기식에서 비동기식으로 완전히 뒤바꾸고, 시스템의 결합도를 극단적으로 낮추어 진정한 독립성을 부여하는 핵심 아키텍처, '이벤트 드리븐(Event-Driven) 아키텍처'에 대해 상세히 다루어 보겠습니다.
'Architecture > MSA' 카테고리의 다른 글
| MSA 환경에서 공통 모듈이 필요한 이유 (0) | 2026.05.27 |
|---|---|
| 시스템의 결합도를 낮추는 비동기 통신: 이벤트 드리븐(Event-Driven) 아키텍처 (0) | 2026.05.27 |
| 분산 시스템의 단일 진입점과 공통 관심사 통제: API 게이트웨이(API Gateway) 아키텍처 (0) | 2026.05.27 |
| 파편화된 설정 정보의 중앙 집중화: Spring Cloud Config 아키텍처 (0) | 2026.05.27 |
| 연쇄 장애 방지: 서킷 브레이커(Circuit Breaker) 아키텍처 (0) | 2026.05.26 |