분산 서비스 끝까지 하나의 트레이스 아이디 전달하기
민감한 정보를 노출하거나 저장소를 로그로 넘치게 하지 않으면서 HTTP 호출과 비동기 이벤트를 하나의 요청 맥락으로 추적합니다.
출처 및 AI 안내: 이 글은 제미니의 개발실무 유튜브를 기반으로 작성되었습니다.
gpt-5.6-sol모델을 사용해 생성·편집했습니다.
로깅이라는 질문은 금방 너무 큰 주제가 됩니다. 저는 운영에서 바로 확인할 수 있는 작은 기준부터 봅니다. 중요한 비즈니스 동작을 찾을 수 있는가, 그리고 하나의 요청을 처리한 모든 서비스를 끝까지 따라갈 수 있는가.
운영자가 알아봐야 할 동작에는 의미가 드러나는 로그를 넣습니다. 개발할 때만 필요한 세부사항과 운영 기록도 분리합니다. 로컬에서는 편한 로그가 운영의 공용 로깅 시스템에 개인정보를 노출할 수 있습니다. 그런 내용은 적절한 레벨에 두고 로컬과 개발 환경에서만 보이도록 설정해야 합니다.
로그의 양도 비용입니다. 애플리케이션 실행 시간, 네트워크, 인덱싱 작업과 저장 공간을 사용합니다. 지나치게 시끄러운 경로가 운영 가치에 비해 너무 많은 데이터를 쌓고 있지 않은지 주기적으로 확인합니다. 줄 수가 많다고 장애 원인을 더 잘 찾는 것은 아닙니다.
여기까지는 기본적인 로깅 습관입니다. 애플리케이션을 여러 서비스로 나누기 시작하면 분산 추적이 답의 중심으로 들어옵니다.
사용자 아이디는 요청 아이디가 아니다
사용자가 쿠폰을 적용하려고 했다고 해보겠습니다. 요청은 사용자와 맞닿은 서비스를 지나 쿠폰 서비스로 가고, 다시 다른 내부 서비스를 호출합니다. 애플리케이션마다 자기 로그를 남깁니다.
사용자가 쿠폰 적용에 실패했다고 문의했습니다. 공통 필드가 user_id뿐이면 조사하기 어렵습니다. 같은 사용자가 다섯 번 시도했을 수 있습니다. 한 번의 시도마다 여러 서비스에서 여러 줄이 생깁니다. 사용자로 검색하면 관련 로그는 많이 나오지만 어느 줄이 실패한 한 번의 요청에 속하는지는 알기 어렵습니다.
하나의 진입 요청에는 하나의 트레이스 식별자가 필요합니다. 요청을 받거나 시작한 서비스가 그 식별자를 다음 서비스에 전달하고, 모든 하위 호출이 계속 전파합니다. 그러면 500 오류와 함께 기록된 트레이스 아이디를 기준으로 참여한 모든 서비스의 흐름을 볼 수 있습니다.
트레이스 아이디는 분산 요청 전체를 나타냅니다. 스팬 아이디는 그 안의 개별 작업이나 호출 구간을 구분합니다. 로깅 시스템에 두 값을 일관되고 검색 가능한 형태로 남겨야 서로 다른 애플리케이션의 시간을 사람이 직접 맞추지 않고도 실패 지점까지 갈 수 있습니다.
구체적인 프레임워크 지원은 바뀔 수 있습니다. 라이브러리가 교체되고 관측 도구 연동 방식이 이전되며 B3나 W3C Trace Context 같은 전파 형식을 선택할 수 있습니다. 제가 먼저 보는 것은 우리가 운영하는 경계를 실제로 끝까지 통과하느냐입니다. 유행하는 라이브러리를 설치했지만 클라이언트 하나에서 맥락이 끊긴다면 소박해도 전파가 되는 구성보다 못합니다.
전파는 애플리케이션 경계의 책임이다
HTTP 클라이언트에는 트레이스 맥락을 자동으로 주입하고 읽는 연동 지점이 있는 경우가 많습니다. 사용한 뒤 정말 동작하는지 확인해야 합니다. 내부 전용 클라이언트, 커스텀 프로토콜이나 오래된 라이브러리가 자동 연동에 참여하지 못하면 그 경계에서는 수동 작업이 필요합니다.
트레이스 아이디는 전용 추적 저장소뿐 아니라 애플리케이션 로그에도 보여야 합니다. 운영자는 트레이스 화면보다 에러 로그나 고객 문의에서 조사를 시작하는 경우가 많습니다. 에러 한 줄에 트레이스 아이디가 있으면 로컬 증상에서 전체 요청으로 넘어가는 연결점이 됩니다.
서비스마다 새 아이디를 만들면 안 됩니다. 각 서비스는 자기 작업을 위한 하위 스팬을 만들 수 있지만 전체 요청을 잇는 부모 트레이스는 유지해야 합니다.
이벤트를 넘어갈 때도 맥락을 유지하기
동기 HTTP 호출은 첫 번째 사례일 뿐입니다. 서비스가 이벤트를 발행하고 응답을 끝낸 뒤 다른 서비스가 나중에 이벤트를 소비해 후처리할 수 있습니다. 호출 스택은 사라졌지만 운영에서 알고 싶은 것은 같습니다. 이 최초 동작 이후에 무슨 일이 벌어졌는가.
이벤트 메타데이터에 트레이스 맥락이나 명확하게 연결된 상관 식별자를 전달합니다. 컨슈머가 작업을 시작할 때 그 관계를 복원해야 발행과 소비를 같은 맥락에서 볼 수 있습니다. 그렇지 않으면 트레이스가 프로듀서에서 끝나고 중요한 비동기 동작은 별개의 수수께끼가 됩니다.
모든 이벤트 처리를 같은 동기 호출처럼 꾸미라는 뜻은 아닙니다. 프로듀서와 컨슈머는 별도 스팬과 시간을 가질 수 있습니다. 운영자가 원인 관계를 따라갈 수 있으면 됩니다.
쿠폰 사례를 다시 보면 사용자 서비스가 이벤트를 발행하고 쿠폰 서비스가 받아 쿠폰을 갱신할 수 있습니다. 갱신이 실패했을 때 컨슈머 실패에서 이벤트로, 다시 이벤트를 만든 사용자 동작으로 이동할 수 있어야 합니다. 전파된 맥락이 없으면 시간과 비즈니스 아이디를 보고 짐작해야 합니다.
장식이 아니라 운영을 위해 로그 남기기
쓸모 있는 구성은 구체적인 질문에 답해야 합니다.
- 어떤 사용자 동작 또는 비즈니스 동작을 시도했는가?
- 이번 한 번의 시도는 어떤 트레이스인가?
- 어느 서비스와 스팬에서 실패했는가?
- 클라이언트나 이벤트 경계에서 맥락이 끊기지 않았는가?
- 민감한 정보가 허용된 환경과 로그 레벨에만 남는가?
- 특정 코드 경로가 로깅 시스템을 압도하고 있지 않은가?
트레이스 아이디가 비즈니스 필드를 대신하지는 않습니다. 운영자는 주문 번호, 쿠폰 ID나 사용자 문의로 시작할 수 있으므로 중요한 식별자도 안전하게 골라 남겨야 합니다. 비즈니스 필드가 가능한 동작의 묶음을 찾는다면 트레이스 아이디는 그중 한 번의 실행을 분리합니다.
연결된 트레이스 안에 모호한 메시지만 가득하면 여전히 조사 시간이 오래 걸립니다. 중요한 상태와 판단을 알아볼 수 있게 남기되 로그 저장소에 들어가면 안 되는 개인정보나 비밀값은 제외해야 합니다.
저는 이 설계를 운영에서부터 생각합니다. 고객이 쿠폰 시도 한 번이 실패했다고 말하면 그 문의에서 하나의 트레이스로 이동하고, HTTP 호출과 이벤트를 따라가며 어디서 동작이 달라졌는지 보고 싶습니다. 서비스마다 로그는 있지만 서로 연결하지 못한다면 데이터를 모았을 뿐 조사 경로를 만든 것은 아닙니다.