본문으로 건너뛰기
중급약 35분

로그를 결함 증거로 — 상관 ID·타임스탬프·스택 트레이스로 즉시 재현시키기

로그를 읽는 것과 로그로 개발자를 움직이는 것은 다르다. 상관 ID(W3C traceparent·x-request-id)를 찾아내 리포트에 박고, 시간대를 명시한 타임스탬프 범위를 적고, 스택 트레이스를 어디까지 어떻게 붙일지 정한다. 증거 여섯 묶음으로 된 결함 패키지를 만들고, 상관 ID 자체가 없을 때 개발팀에 무엇을 요구할지까지 다룬다.

  • #결함리포트
  • #상관ID
  • #traceparent
  • #스택트레이스
  • #증거

개념

좋은 증거의 기준은 하나다 — 개발자가 검색할 수 있는가

결함 리포트에 로그를 붙이는 목적은 "열심히 조사했음을 보이는 것"이 아니다. 개발자가 자기 자리에서 그 요청을 다시 찾아낼 수 있게 하는 것 이다. 기준은 이 한 문장으로 충분하다.

이 리포트만 보고, 우리 로그 시스템에서 해당 요청을 검색어 하나로 찾아낼 수 있는가.

찾을 수 있으면 좋은 증거다. 못 찾으면 아무리 길게 써도 개발자는 결국 "재현 부탁드립니다"라고 답한다. 그리고 재현이 어려운 결함일수록 그 답은 곧 방치를 뜻한다.

검색을 가능하게 만드는 것은 세 가지다. 무엇으로 찾을 것인가(상관 ID), 언제인가(타임스탬프), 무엇이 터졌는가(스택 트레이스). 이 레슨은 이 셋을 다룬다. 모듈 6(레슨 2)에서 배운 재현 절차·기대 결과·실제 결과 위에 이 셋을 얹는다고 생각하면 된다.

상관 ID — 요청 하나를 끝까지 따라가는 열쇠

요즘 시스템은 요청 하나가 여러 서비스를 거친다. 주문 API 가 회원 서비스와 쿠폰 서비스와 결제 게이트웨이를 부른다. 로그도 그만큼 흩어진다. 상관 ID(correlation ID) 는 그 흩어진 로그를 하나로 묶는 값이다. 분산 추적에서는 보통 trace ID 라고 부른다.

표준은 W3C Trace Context 다. HTTP 요청에 traceparent 헤더로 실려 다닌다.

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             │  │                                │                │
             │  trace-id (32자리 16진수)          │                trace-flags
             version                              parent-id(span-id, 16자리)

QA 가 리포트에 적어야 하는 값은 가운데 trace-id 다. 요청이 어느 서비스를 거치든 이 값은 유지된다. tracestate 라는 짝 헤더도 있는데 벤더별 정보를 나르는 용도라 QA 가 쓸 일은 거의 없다.

현장에서는 표준 헤더 대신 회사 규칙을 쓰는 곳도 많다. 이름만 다를 뿐 역할은 같다.

이름어디서 보이나
traceparentW3C 표준. OpenTelemetry 계열이면 대부분 이것
x-request-idNginx·많은 프레임워크의 관행적 이름
x-correlation-id기업 내부 게이트웨이에서 흔함
x-amzn-trace-idAWS ALB·API Gateway
응답 본문의 errorId·referenceId오류 응답에 참조 코드를 담는 설계

찾는 순서는 이렇다.

  1. 개발자 도구 네트워크 탭에서 실패한 요청을 클릭한다
  2. 응답 헤더 를 먼저 본다. 서버가 되돌려 주는 경우가 가장 편하다
  3. 없으면 요청 헤더 를 본다. 프런트엔드나 브라우저 확장이 심었을 수 있다
  4. 그래도 없으면 응답 본문 을 본다. 오류 응답 JSON 에 참조 코드가 들어 있는 설계가 흔하다
  5. 오류 화면에 "오류 코드 A-2031" 같은 문구가 있으면 그것도 후보다

상관 ID 가 아예 없다면

없는 시스템이 실제로 많다. 그럴 때 할 일이 두 가지다.

당장은 대체 식별자를 쓴다. 로그를 좁힐 수 있는 조합이면 된다.

  • 업무 키 — 주문번호·결제 승인번호·회원 ID. 가장 강력한 대체재 다
  • 계정 식별자 + 정확한 시각 범위 + 엔드포인트 경로
  • 세션 ID·기기 ID (앱이라면)

중기적으로는 요구한다. 이것은 테스트 가능성(testability) 개선 요구이고, QA 가 제기하기에 가장 정당한 종류의 요구다(모듈 9 레슨 9). 요구 문장은 구체적일수록 받아들여진다.

  • 모든 API 응답에 x-request-id 를 헤더로 내려 달라
  • 오류 응답 본문에 그 값을 referenceId 로 포함해 달라
  • 사용자에게 보이는 오류 화면에 그 값의 앞 8자리를 표시해 달라 — 고객센터 문의에도 그대로 쓰인다

세 번째 항목은 QA 만의 이익이 아니라서 설득이 잘 된다. CS 팀이 "고객이 말한 오류 코드로 로그를 찾는" 능력을 갖게 된다.

타임스탬프 — 시간대 없는 시각은 증거가 아니다

"14시 3분쯤"은 증거가 아니다. 로그 시스템에서 검색 범위를 만들 수 없기 때문이다. 세 가지를 지킨다.

  1. 시간대를 명시한다. 2026-09-24 14:03:11 KST (+09:00) 또는 ISO 8601 로 2026-09-24T14:03:11+09:00. 서버 로그가 UTC 면 05:03:11Z 와 같은 시각이다
  2. 범위로 적는다. 초 단위까지 정확한 값을 알기 어려우므로 14:03:00 ~ 14:03:40 KST 처럼 앞뒤 여유를 준다
  3. 기기 시계를 확인한다. 테스트 단말의 시계가 몇 분 틀어져 있으면 검색 범위가 통째로 빗나간다. 특히 에뮬레이터·오래 켜 둔 테스트 기기에서 흔하다

시각을 정확히 남기는 가장 쉬운 방법은 네트워크 탭에서 그 요청의 Date 응답 헤더를 복사하는 것 이다. 서버가 찍은 시각이므로 기기 시계와 무관하다.

스택 트레이스 — 어디까지 붙이나

전부 붙이면 리포트가 읽히지 않고, 첫 줄만 붙이면 정보가 없다. 기준은 이렇다.

  • 예외 타입과 메시지 전체 (첫 줄)
  • 우리 코드 프레임 전부 — 회사 패키지로 시작하는 줄들
  • 최종 Caused by 블록 전체
  • 프레임워크·JDK 내부 프레임은 ... 로 줄인다
  • 접기(details)나 첨부파일로 원문 전체도 같이 남긴다. 요약이 잘못 잘렸을 때를 대비한다
java.lang.NullPointerException: Cannot invoke "java.time.LocalDate.isBefore(...)"
        because "coupon.validTo" is null
    at com.example.shop.coupon.CouponService.validate(CouponService.java:87)
    at com.example.shop.coupon.CouponService.apply(CouponService.java:54)
    at com.example.shop.order.OrderService.createOrder(OrderService.java:212)
    at com.example.shop.order.OrderController.create(OrderController.java:63)
    ... (framework frames omitted)

붙이기 전에 마스킹 한다. 로그 한 줄에 회원 이메일·전화번호·카드번호가 섞여 있는 경우가 많다. 리포트는 사내 여러 사람이 보고 첨부는 오래 남는다. 값의 앞뒤만 남기고 가운데를 가린다 — hong****@example.com, 5412-****-****-1234. 마스킹이 필요했다는 사실 자체도 별도 결함으로 등록 한다(레슨 3).

증거 여섯 묶음

좋은 결함 리포트의 증거는 대개 이 여섯으로 구성된다. 전부 있을 필요는 없지만, 무엇이 없는지는 알고 있어야 한다.

#증거무엇을 증명하나
1상관 ID(또는 대체 식별자)개발자가 로그를 찾을 수 있다
2시간대 포함 타임스탬프 범위검색 범위를 만들 수 있다
3요청·응답 원문(경로·본문·상태 코드)무엇을 보냈고 무엇을 받았는가
4로그 발췌(스택 트레이스 포함)서버 내부에서 무엇이 터졌는가
5화면 캡처 또는 짧은 영상사용자에게 어떻게 보였는가
6환경 정보 — 빌드 버전·환경(stg/prod)·계정·기기어디서 재현되는가

여기에 이 모듈의 특기를 하나 더 얹을 수 있다. 정합성 조회 결과(레슨 2) 다. "이 증상이 발생한 주문이 지난 7일간 34건"은 우선순위를 바꾼다.

나쁜 리포트와 좋은 리포트

같은 결함을 두 가지로 써 본다.

[나쁨]
제목: 결제가 안 됨
내용: 쿠폰 넣고 결제하니까 오류납니다. 확인 부탁드립니다.
첨부: 스크린샷1.png
[좋음]
제목: [stg] 종료일이 없는 쿠폰 적용 시 주문 생성 500 — NPE (CouponService.validate:87)
 
환경: stg / 빌드 2026.9.24-rc3 / Chrome 140 / 계정 qa-buyer-07 (memberId=10023)
발생 시각: 2026-09-24 14:03:00~14:03:40 KST (+09:00) / 서버 로그 UTC 기준 05:03
상관 ID: traceparent trace-id = 4bf92f3577b34da6a3ce929d0e0e4736
         (응답 헤더 x-request-id 에도 동일 값)
 
재현 절차:
 1. qa-buyer-07 로 로그인
 2. 상품 2건을 장바구니에 담는다 (합계 52,000원)
 3. 결제 화면에서 쿠폰 WELCOME10 적용
 4. [결제하기] 클릭
 
기대 결과: 주문이 생성되고 주문 완료 화면으로 이동
실제 결과: "일시적인 오류" 토스트, 주문 미생성
           POST /api/v1/orders → 500, 응답시간 2.4s
 
서버 로그 (발췌):
 java.lang.NullPointerException: ... because "coupon.validTo" is null
   at com.example.shop.coupon.CouponService.validate(CouponService.java:87)
   at com.example.shop.order.OrderService.createOrder(OrderService.java:212)
 
데이터 확인:
 SELECT coupon_id, code, valid_from, valid_to FROM coupons WHERE valid_to IS NULL;
 → 12건. 그중 3건이 현재 발급 중인 쿠폰
 
재현율: 3/3 (종료일이 NULL 인 쿠폰에서만. 종료일이 있는 쿠폰은 정상)
첨부: 네트워크탭_응답.png, 화면녹화.mp4, 로그원문.txt

두 번째 리포트에서 개발자가 해야 할 일은 코드를 여는 것뿐 이다. 재현도, 로그 검색도, 원인 가설 수립도 이미 끝나 있다. 더 중요한 것은 마지막 두 줄이다 — 재현 조건이 valid_to IS NULL 로 좁혀져 있고, 같은 조건의 쿠폰이 12건 더 있다는 영향 범위까지 나와 있다.

로그에 접근할 수 없을 때

QA 에게 서버 로그 권한이 없는 조직도 있다. 그래도 할 수 있는 것이 남아 있다.

  • 클라이언트 쪽 증거를 최대한 모은다 — 네트워크 탭의 요청·응답 원문(HAR 파일로 저장 가능), 콘솔 로그, 응답 헤더의 식별자
  • 읽기 권한을 요청한다. 로그 시스템의 읽기 전용 계정 요구는 DB 와 같은 논리다. "결함 리포트의 왕복 횟수를 줄이기 위해"라는 근거를 붙인다
  • 없으면 없다고 적는다. "서버 로그 미확인(권한 없음)"이라고 명시하면 개발자가 그 부분을 자기가 채워야 한다는 것을 안다. 비워 두면 "이미 봤는데 없더라"로 오해된다

HAR 파일을 쓸 때는 주의한다. HAR 에는 인증 토큰과 쿠키가 그대로 들어 있다. 공유 전에 민감 헤더를 지우거나, 저장 시 제공되는 "민감 데이터 제외" 옵션을 쓴다.

실무에서 왜 필요한가

결함 하나가 고쳐지기까지 걸리는 시간은 코드 수정 시간이 아니라 왕복 대기 시간 이 지배한다. 리포트 등록 → 개발자 확인 → 정보 요청 → QA 응답 → 재확인. 왕복 한 번이 보통 반나절에서 하루다. 증거가 갖춰진 리포트는 이 왕복을 0 으로 만든다. 릴리스 직전 하루가 급한 상황에서 이 차이는 결정적이다.

반려율도 달라진다. "재현 불가"로 돌아오는 결함의 대부분은 재현 조건이 리포트에 없는 것이지 진짜로 재현이 안 되는 것이 아니다. 위 예시에서 valid_to IS NULL 이라는 조건이 없으면 개발자는 아무 쿠폰으로나 시도하고 정상 동작을 보고 닫아 버린다. 조건을 좁히는 능력이 반려를 막는다.

조직 차원의 효과도 있다. 상관 ID 를 리포트에 꾸준히 적다 보면 그 값이 없는 곳이 드러난다. 어느 서비스는 헤더를 전파하지 않고, 어느 오류 화면은 참조 코드를 안 보여 준다. 이 목록이 쌓이면 관측성 개선 과제가 되고, 그것을 주도하는 QA 는 팀에서 다른 위치에 선다.

실습 과제

과제 1 — 상관 ID 사냥 (25분)

자주 쓰는 웹 서비스 3개(자기 제품 포함)에서 아무 요청이나 하나씩 잡아 다음 표를 채운다.

서비스요청 헤더에 있는 ID응답 헤더에 있는 ID오류 응답 본문의 참조 코드오류 화면에 노출되나

세 서비스 중 몇 개가 네 칸을 다 채우는지 본다. 대부분 한두 칸만 찬다. 자기 제품이 비어 있는 칸을 골라, 앞의 "중기적으로는 요구한다" 문장 형식으로 개선 요청 한 문단을 써 본다.

과제 2 — 나쁜 리포트 고쳐 쓰기 (30분)

아래 리포트를 이 레슨의 증거 여섯 묶음 기준으로 다시 쓴다. 정보가 없는 칸은 "어떻게 구할 것인가"를 적는다(예: "상관 ID — 네트워크 탭 응답 헤더에서 확인 필요").

제목: 포인트 적립 오류
내용: 어제 주문했는데 포인트가 안 들어왔어요. 다른 계정은 되는 것 같습니다.
      급합니다. 확인 부탁드립니다.

다시 쓴 리포트에는 최소한 다음이 들어가야 한다 — 시간대 포함 시각 범위, 영향 범위를 확인할 조회문(레슨 2 형식), 재현 조건 가설 하나, 비어 있는 증거 항목의 목록.

과제 3 — 마스킹 규칙 만들기 (20분)

담당 제품의 로그 샘플 20줄을 확보한다(권한이 없으면 공개된 오픈소스 프로젝트의 이슈에 첨부된 로그도 된다). 다음을 수행한다.

  1. 민감정보로 분류될 값을 전부 표시한다 — 이메일·전화번호·주소·카드번호·토큰·주민번호
  2. 각각에 마스킹 규칙을 정한다(앞 2자 + ****, 뒤 4자리만 등)
  3. 원본에서 실제로 마스킹된 로그를 만든다
  4. 마스킹이 필요했던 항목 중 애초에 로그에 남으면 안 되는 것 을 골라 결함 초안을 쓴다

4번에서 하나도 안 나온다면 좋은 시스템이다. 대개는 한두 개가 나온다.

자가 체크리스트

  • 좋은 증거의 기준을 "개발자가 검색으로 그 요청을 찾을 수 있는가"로 설명할 수 있다
  • traceparent 헤더에서 trace-id 위치를 찾아 리포트에 옮길 수 있다
  • 응답 헤더·요청 헤더·오류 응답 본문·오류 화면 순으로 상관 ID 를 찾을 수 있다
  • 상관 ID 가 없을 때 업무 키·계정·시각 범위로 대체 식별자를 구성할 수 있다
  • 시간대를 명시한 타임스탬프 범위를 적고, 서버 UTC 시각으로 환산할 수 있다
  • 스택 트레이스를 요약해 붙이면서 원문도 함께 남길 수 있다
  • 결함 리포트에 증거 여섯 묶음 중 무엇이 있고 무엇이 없는지 말할 수 있다
  • 로그·HAR 에 포함된 민감정보를 마스킹한 뒤 공유한다
  • 로그 접근 권한이 없을 때 "미확인"임을 리포트에 명시한다
  • 관측성이 부족한 지점을 모아 개선 요청으로 만들 수 있다

흔한 실수

  • 상관 ID 를 찾아 놓고 리포트에 안 적는다. 조사한 내용이 머릿속에만 남으면 왕복이 그대로 발생한다.
  • 시간대 없이 시각을 적는다. "14시쯤"은 검색 범위가 되지 못한다. +09:00 을 붙이고 범위로 적는다.
  • 스택 트레이스를 첫 줄만 붙인다. 우리 코드 프레임과 최종 Caused by 가 빠지면 정보의 대부분이 사라진다.
  • 반대로 200줄을 통째로 붙인다. 읽히지 않는다. 요약 + 원문 첨부가 정답이다.
  • HAR 파일을 그대로 공유한다. 인증 토큰과 쿠키가 들어 있다. 민감 데이터를 제외하고 저장한다.
  • 재현 조건을 좁히지 않고 등록한다. "쿠폰 쓰면 오류"와 "종료일이 비어 있는 쿠폰에서만 오류"는 반려율이 다르다.
  • 확인하지 않은 것을 빈칸으로 둔다. "권한 없어 미확인"이라고 적어야 다음 사람이 그 자리를 채운다.

참고 자료

  • W3C — Trace Context — traceparent 헤더의 필드 구성(version·trace-id·parent-id·trace-flags)과 tracestate 의 용도
  • OpenTelemetry — Context Propagation — 서비스 사이로 trace 컨텍스트가 전파되는 방식
  • MDN — HTTP 헤더 — Date 등 응답 헤더의 표준 정의
  • Chrome DevTools — 네트워크 요청 분석 — 요청·응답 확인과 HAR 내보내기
  • 레슨 s2-m06-l02 — 좋은 결함 리포트 쓰기(재현 절차·기대/실제 결과의 기본형)
  • 레슨 s3-m61-l02 — 정합성 조회(영향 범위를 숫자로 붙이는 방법)
  • 레슨 s3-m61-l03 — 로그 읽기(여기서 읽은 것을 증거로 바꾼다)
  • 레슨 s3-m09-l09 — 테스트 가능성 판단하기(상관 ID 노출을 요구하는 근거)

낯선 말 풀이

이 레슨에 나온 약어입니다. 본문에서도 처음 나올 때 점선으로 표시해 두었습니다 (제목과 링크 글자는 그대로 둡니다).

ID — 식별자
무엇 하나를 다른 것과 구별하기 위해 붙인 이름이나 번호.
API — 응용 프로그램 인터페이스
프로그램끼리 주고받는 창구. 사람이 화면 버튼을 누르는 대신, 프로그램이 정해진 주소로 요청을 보내 답을 받는다.
HTTP — 하이퍼텍스트 전송 규약
웹에서 요청과 응답을 주고받는 약속. "무엇을 달라"고 보내면 "여기 있다" 또는 "그런 건 없다"가 돌아온다.
QA — 품질보증
만든 것이 쓸 만한지 확인하고, 애초에 덜 망가지게 만드는 방법을 정하는 일. "테스트"는 그 일의 일부다.
AWS — 아마존 웹 서비스
서버와 저장소를 빌려 쓰는 클라우드 서비스 중 가장 큰 것.
JSON — 제이슨
프로그램끼리 값을 주고받을 때 쓰는 글자 형식. 괄호와 따옴표로 이름과 값을 짝지어 적는다.
CS — 고객 지원 · 칩 선택 신호
이 커리큘럼에서 두 가지로 쓰인다. ① 고객 문의와 불만을 직접 받는 조직 ② 여러 부품이 한 선을 나눠 쓸 때 "지금은 네 차례" 라고 하나를 고르는 신호. 앞뒤에 문의·고객이 있으면 ①, 통신·배선 이야기면 ②다.
ISO — 국제표준화기구
여러 나라가 모여 "이건 이렇게 하자"를 문서로 정하는 단체. 그 문서 자체를 번호로 부른다.
UTC — 협정 세계시
세계 공통 기준 시각. 한국 시간보다 9시간 느리다.
JDK — 자바 개발 도구 모음
자바로 프로그램을 만들고 돌리는 데 필요한 도구 꾸러미.
HAR — 통신 기록 파일
브라우저가 주고받은 요청과 응답을 전부 담아 저장한 파일.
DB — 데이터베이스
데이터를 넣어 두고 꺼내 쓰는 저장소. 보통 표 여러 개가 서로 연결된 형태다.

용어집 전체 보기

이 레슨의 실습

실습은 별도 저장소에서 진행합니다. 새 탭으로 열립니다.