Spring Boot 관측 가능성: 로그를 더 쌓기 전에 연결할 것들
메트릭·로그·트레이스를 하나의 요청 맥락으로 연결하고, 결제 API의 SLO에서 출발해 실제 장애 대응 시간을 줄인 방법을 소개합니다.
장애 대응이 느린 팀에는 로그가 부족한 경우보다 로그가 서로 연결되지 않은 경우가 많다. API 오류율 알람은 울리지만 어떤 요청이 실패했는지, 그 요청이 어떤 Kafka 이벤트와 외부 결제 호출로 이어졌는지 찾는 데 시간이 걸린다.
관측 가능성을 “로그를 많이 남기는 일”이 아니라 사용자의 실패에서 원인 후보까지 이동하는 경로를 짧게 만드는 일로 정의하고 구조를 바꿨다.
도구보다 먼저 SLO를 정한다
첫 대시보드는 JVM 메모리와 CPU로 가득했지만 “사용자가 결제를 완료할 수 있는가?”에는 바로 답하지 못했다. 결제 승인 API에 다음 SLI를 정했다.
- 성공률: 시스템 오류 없이 최종 결과를 반환한 요청 비율
- 지연 시간: 승인 API의 p50, p95, p99
- 결과 최신성: 승인 후 정산 준비 이벤트가 생성되기까지 걸린 시간
- 불명확 결과: 타임아웃 뒤 성공·실패를 즉시 확정하지 못한 요청 수
시스템 메트릭은 원인 분석에 필요하지만 알람의 출발점은 사용자 경험이어야 한다. 예를 들어 CPU 90% 자체보다 “승인 p99이 2초를 넘고 오류 예산 소진 속도가 6배”라는 신호가 대응 우선순위를 더 명확히 만든다.
한 요청의 식별자를 경계 끝까지 전달한다
Spring Boot 3의 Micrometer Tracing을 사용해 HTTP 요청에 trace ID와 span ID를 만들고 로그 패턴에 포함했다.
management:
tracing:
sampling:
probability: 0.1
logging:
pattern:
level: "%5p [traceId=%X{traceId:-},spanId=%X{spanId:-}]"
샘플링 비율이 10%여도 오류 요청은 별도 정책으로 보존했다. 개인정보와 카드 정보는 span attribute에 넣지 않았다. 대신 조사에 필요한 안전한 식별자만 제한적으로 기록했다.
Observation observation = Observation.createNotStarted(
"payment.approve",
observationRegistry
).lowCardinalityKeyValue("provider", providerCode)
.lowCardinalityKeyValue("result", resultCode);
여기서 paymentId를 low-cardinality tag로 넣지 않는다. 고유값이 많은 태그는 시계열 수를 폭발시킨다. 결제 ID처럼 개별 요청을 찾기 위한 값은 로그나 trace attribute에 두고, 메트릭 태그는 결제사·결과 코드·API처럼 제한된 집합만 사용한다.
비동기 경계에서도 맥락을 끊지 않는다
HTTP 요청에서 만든 추적 맥락은 Kafka로 넘어갈 때 자동으로 이어지는지 확인해야 한다. 프로듀서가 trace context를 메시지 헤더에 주입하고 컨슈머가 추출하도록 계측 라이브러리를 맞췄다. 비즈니스 사건을 추적하는 eventId와 기술적인 traceId는 역할이 다르므로 둘 다 유지했다.
traceId: 한 번의 실행 경로를 추적한다. 재시도하면 새 trace가 생길 수 있다.eventId: 같은 비즈니스 사건을 식별한다. 재전달돼도 유지된다.
컨슈머 로그에는 두 값을 함께 남겼다.
{
"message": "settlement event consumed",
"traceId": "8d7e4b2a...",
"eventId": "evt_01J...",
"topic": "payment-approved",
"partition": 7,
"offset": 184201
}
이 구조 덕분에 한 실행의 지연은 trace에서, 같은 이벤트의 반복 처리는 event ID 검색에서 확인할 수 있었다.
로그에는 사실과 다음 행동을 남긴다
다음과 같은 로그는 실제 대응에 거의 도움이 되지 않았다.
ERROR payment failed
오류 로그를 설계할 때 최소한 네 가지를 포함했다.
- 어떤 작업이 실패했는가
- 재시도 가능한가
- 외부 부작용이 발생했을 가능성이 있는가
- 안전하게 식별할 수 있는 비즈니스 키는 무엇인가
예외 스택은 원인을 보여 주지만 현재 상태와 복구 방법까지 알려주지는 않는다. 그래서 outcome=UNKNOWN, retryable=false, reconciliationRequired=true처럼 다음 행동을 결정하는 필드를 구조화해 남겼다.
알람에서 trace까지 한 번에 이동한다
최종 대시보드는 다음 순서로 읽게 만들었다.
- SLO와 오류 예산 소진 속도로 영향도를 확인한다.
- 실패한 API·결제사·결과 코드로 범위를 좁힌다.
- exemplar가 연결한 trace에서 가장 긴 span과 오류를 본다.
- trace ID로 구조화 로그를 검색해 비즈니스 상태를 확인한다.
알람 메시지에도 같은 경로를 넣었다. 담당자는 채널에서 대시보드, 대표 trace, 대응 문서로 바로 이동할 수 있었다. 도구를 추가한 것보다 관찰 화면 사이의 이동 비용을 줄인 것이 평균 확인 시간을 더 크게 줄였다.
비용과 개인정보도 설계 대상이다
모든 요청을 영구 보관할 수는 없다. 정상 trace는 낮은 비율로 샘플링하고 오류와 고지연 trace는 tail-based sampling으로 더 많이 보존했다. 로그 보존 기간은 용도별로 나누고, 운영 검색에 필요 없는 body와 헤더는 수집 단계에서 제거했다.
관측 데이터는 디버깅 편의를 위한 복사본이 아니라 또 하나의 데이터 시스템이다. 접근 권한, 마스킹, 보존 기간, 비용 상한이 애플리케이션 설계와 함께 정해져야 한다.
마치며
관측 가능성의 목표는 데이터의 양이 아니다. 질문에 답하는 데 필요한 연결이 핵심이다.
- SLO는 사용자 실패에서 조사를 시작하게 한다.
- trace ID는 동기·비동기 실행 경로를 연결한다.
- event ID는 재시도와 재전달 사이에서도 사건을 연결한다.
- 구조화 로그는 현재 상태와 다음 행동을 설명한다.
- 메트릭 태그는 집계에 필요한 낮은 카디널리티만 가진다.
로그 한 줄을 더 남기기 전에, 그 로그가 어떤 알람과 trace에서 도달 가능한지 먼저 확인하는 습관이 장애 대응 시간을 바꿨다.