이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Request ID, Correlation ID, Trace ID는 각각 어떤 문제를 해결하는가?
- 여러 계층 로그를 한 요청으로 묶으려면 어떤 헤더와 로그 필드가 필요한가?
- 로그와 트레이스에 남기면 안 되는 정보는 무엇인가?
개요
분산 환경에서 한 요청은 여러 로그에 조각으로 남는다. CDN access log, Load Balancer log, NGINX log, 애플리케이션 access log, 애플리케이션 error log, DB slow query, 외부 API dependency trace가 모두 같은 요청의 일부일 수 있다.
Trace ID와 Request ID는 이 조각을 한 줄로 꿰는 키다. 이 키가 없으면 장애 대응은 “비슷한 시간대 로그를 눈으로 찾는 작업”이 된다. 반대로 요청 ID가 잘 전파되면 사용자 제보 하나를 정확한 로그와 트레이스로 연결할 수 있다.
왜 백엔드 개발자가 알아야 하는가
백엔드 개발자는 로그를 많이 남기는 것보다 “찾을 수 있는 로그”를 남겨야 한다. 장애 상황에서 필요한 것은 수천 줄의 로그가 아니라 특정 요청 하나가 어느 계층에서 실패했는지 보여 주는 연결성이다.
- 사용자가 보낸 screenshot에 request id가 있으면 운영자가 바로 로그를 찾을 수 있다.
- proxy와 app의 request id가 다르면 같은 요청인지 확인하기 어렵다.
- 외부 API 호출 span이 trace에 없으면 first byte 지연 원인을 앱 내부에서만 찾게 된다.
- 민감 정보가 trace attribute에 들어가면 장애 분석 도구가 보안 사고 지점이 된다.
요청 흐름에서의 위치
Trace ID는 요청이 처음 들어오는 edge나 gateway에서 생성될 수도 있고, 애플리케이션에서 생성될 수도 있다. 중요한 것은 생성 위치보다 전파 일관성이다.
Client
→ CDN / WAF
→ Load Balancer
→ Reverse Proxy
→ Application
→ DB / Cache / External API이 경로에서 같은 ID가 계속 이어져야 한다. 중간 계층이 새 ID를 만들더라도 원래 ID를 버리지 않고 별도 필드로 보존하는 편이 좋다.
원리
현장에서 자주 쓰는 ID는 용도가 조금씩 다르다.
| 이름 | 주된 용도 | 흔한 위치 |
|---|---|---|
| Request ID | HTTP 요청 한 번을 식별 | X-Request-ID, access log |
| Correlation ID | 여러 요청을 하나의 업무 흐름으로 묶음 | 비동기 메시지, batch, saga |
| Trace ID | 분산 trace 전체를 식별 | W3C traceparent, APM |
| Span ID | trace 안의 개별 작업을 식별 | DB query, external API call |
HTTP 표준과 도구 생태계에서는 W3C Trace Context의 traceparent가 널리 쓰인다. 기존 시스템에서는 X-Request-ID나 X-Correlation-ID도 많이 사용한다. 둘 중 무엇을 쓰든 팀 안에서 “어느 헤더를 생성하고, 어느 헤더를 신뢰하고, 어느 로그 필드에 남기는가”가 정해져 있어야 한다.
실무에서 자주 만나는 문제
- 프록시가 request id를 새로 만들고 앱이 또 새로 만든다.
- 두 로그를 연결할 수 없다.
- proxy에서 받은 ID를 app MDC에 넣거나, app 생성 ID를 response header로 내려야 한다.
- 로그에는 request id가 있는데 response header에는 없다.
- 사용자 제보와 서버 로그를 연결하기 어렵다.
- 에러 응답에도 request id를 포함한다.
- 외부 API 호출에 trace context가 전파되지 않는다.
- 우리 서비스 내부 trace는 보이지만 downstream 지연이 검은 상자로 남는다.
- HTTP client interceptor나 OpenTelemetry instrumentation을 확인한다.
- 민감 정보가 trace tag에 들어간다.
- token, cookie, 주민번호, 결제 정보가 APM에 저장될 수 있다.
- 로그/trace attribute allowlist를 둔다.
디버깅 방법
장애 제보가 들어오면 먼저 request id를 찾는다.
curl -i https://example.com/api/orders/123응답에서 다음 헤더를 확인한다.
X-Request-ID: 7f0f0d6a7c1a4b87
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01그 다음 같은 ID로 계층별 로그를 검색한다.
# NGINX
grep '7f0f0d6a7c1a4b87' /var/log/nginx/access.log
# Application
grep '7f0f0d6a7c1a4b87' app.log
# Kubernetes
kubectl logs deploy/order-api | grep '7f0f0d6a7c1a4b87'검색 결과에서 봐야 할 것은 단순히 “있다/없다”가 아니다.
- 어느 계층까지 같은 ID가 남았는가?
- status code가 어느 계층에서 바뀌었는가?
- request time과 upstream time 중 어느 쪽이 긴가?
- app log와 proxy log의 timestamp 차이가 큰가?
- 같은 trace 안에서 DB 또는 external API span이 긴가?
코드로 확인하기
Spring MVC에서 request id를 MDC에 넣고 응답 헤더로 돌려주는 간단한 예시는 다음과 같다.
@Component
public class RequestIdFilter extends OncePerRequestFilter {
private static final String HEADER = "X-Request-ID";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String requestId = Optional.ofNullable(request.getHeader(HEADER))
.filter(id -> id.length() <= 100)
.orElse(UUID.randomUUID().toString());
MDC.put("requestId", requestId);
response.setHeader(HEADER, requestId);
try {
filterChain.doFilter(request, response);
} finally {
MDC.remove("requestId");
}
}
}관찰 포인트는 다음이다.
- 외부에서 들어온 ID를 무제한 신뢰하지 않고 길이 제한을 둔다.
- 응답 헤더에도 같은 ID를 내려 사용자 제보와 연결한다.
finally에서 MDC를 지워 thread reuse로 인한 오염을 막는다.
로그 패턴에는 request id를 포함한다.
logging.pattern.level=%5p [requestId:%X{requestId}]주니어가 자주 하는 오해
- 로그를 많이 남기면 추적성이 좋아진다고 생각한다.
- ID 없이 많은 로그는 검색 비용만 늘린다.
- request id를 클라이언트가 보내면 그대로 믿어도 된다고 생각한다.
- 외부 입력이므로 길이, 문자, 신뢰 경계를 제한해야 한다.
- trace id와 business id를 섞어 쓴다.
- 주문번호와 trace id는 용도가 다르다. 둘 다 필요할 수 있지만 서로 대체하지 않는다.
- 에러 응답에는 request id가 없어도 된다고 생각한다.
- 장애 제보에서 가장 필요한 것이 실패 응답의 request id다.
시니어의 설계 판단 기준
- request id 생성 책임을 edge, gateway, app 중 어디에 둘지 정한다.
- W3C
traceparent와 기존X-Request-ID를 어떻게 함께 운용할지 정한다. - 로그 필드는 사람이 읽는 메시지보다 검색 가능한 구조화 필드를 우선한다.
- 개인 정보와 인증 토큰은 로그와 trace의 기본 수집 대상에서 제외한다.
- 비동기 메시지, queue, batch에서도 correlation id를 이어 간다.
인프라 협업 포인트
- CDN/LB/proxy가 request id를 생성한다면 헤더 이름과 로그 필드명을 공유해야 한다.
- proxy가 들어온 request id를 덮어쓰는지, 별도 upstream header로 넘기는지 확인한다.
- WAF 차단 로그에도 request id가 남는지 확인한다.
- 중앙 로그 시스템에서 request id로 CDN, proxy, app 로그를 동시에 검색할 수 있어야 한다.
실전 팁
- 장애 화면이나 API 에러 body에 request id를 포함하면 사용자 제보 품질이 크게 좋아진다.
- trace sampling을 하더라도 error request는 샘플링에서 제외하지 않는 정책을 검토한다.
- 로그와 trace timestamp는 같은 timezone으로 정렬한다.
- request id는 보안 토큰이 아니다. 인증이나 권한 판단에 사용하지 않는다.
위험 신호!
- proxy log와 app log의 request id가 서로 다르다.
- 에러 응답에는 request id가 없다.
- Authorization, Cookie, 개인정보가 trace attribute에 남는다.
- batch나 queue로 넘어가는 순간 correlation id가 끊긴다.
- 장애 때 로그 검색 쿼리를 매번 새로 만들어야 한다.
확인 질문
확인 질문
- Request ID와 Trace ID는 어떻게 다른가?
- Request ID는 HTTP 요청 한 번을 식별하는 데 자주 쓰이고, Trace ID는 여러 span으로 구성된 분산 추적 전체를 식별한다.
- MDC를 사용할 때
finally에서 값을 지워야 하는 이유는 무엇인가?
- thread가 재사용될 때 이전 요청의 request id가 다음 요청 로그에 섞이는 것을 막기 위해서다.
- 사용자 제보와 서버 로그를 연결하려면 응답에 무엇을 포함하면 좋은가?
X-Request-ID같은 요청 식별자를 응답 헤더나 에러 body에 포함하면 좋다.