구조적 로깅은 장애 대응에서 어디까지 책임져야 하나?
결론부터 적으면, 구조적 로깅 자체가 장애를 막지는 않는다. 다만 "어느 의존성에서, 몇 번째 시도에, 어떤 격리 상태에서 실패했는가"를 쿼리 가능한 필드로 남겨두면 장애 탐지와 원인 분리에 걸리는 시간이 줄어든다. 이 글은 처리량·필드 설계·재시도 추적·카디널리티 네 가지 축으로 구조적 로깅 도입을 실무 판단 기준으로 정리한다.
핵심 정리
- 구조적 로깅 도입 전 처리량·지연시간·메모리를 같은 workload에서 비교하되, 수치는 특정 버전·환경의 결과임을 전제한다.
trace_id,span_id,service_name을 로그에 넣는 목적은 멋이 아니라 단일 요청의 로그·메트릭·트레이스를 연결하는 것이다.- 재시도·서킷브레이커 상태는 문자열 메시지가 아니라
attempt,circuit_state,timeout_ms같은 별도 필드로 남겨야 장애 회고에서 쓸 수 있다. - 사용자 ID·URL 전체처럼 값이 무한히 늘어나는 필드를 메트릭 레이블로 승격하면 카디널리티 문제가 생긴다.
- 선택 축은 "트래픽 규모", "운영 조직 규모", "장애 재현 요구 수준" 세 가지로 좁혀서 판단하는 편이 낫다.
JSON 로그냐 커스텀 미들웨어냐, 처리량 기준으로 뭘 고르나?
처리량 손실을 어디까지 감수할지가 첫 번째 갈림이다. Python 3.13, FastAPI 0.120, AWS c7g.4xlarge 환경에서 LLM 프록시를 대상으로 측정한 결과, 기준 10,000 RPS에서 커스텀 미들웨어는 9,800 RPS, Loguru는 9,500 RPS, OpenTelemetry는 9,200 RPS, Structlog 단독은 8,500 RPS가 나왔다고 보고됐다FastAPI 로깅 벤치마크. 지연시간 오버헤드는 커스텀 미들웨어 1.2ms, Loguru 1.5ms, Structlog 2.1ms, OpenTelemetry 5.8ms로 보고됐고, 워커 메모리는 25MB~150MB까지 벌어졌다.
이 수치를 "OpenTelemetry가 느리다"로 단순화하면 안 된다. 이건 특정 버전·특정 필드 수·특정 수집기 구성에서 나온 내부 측정치이며, 출처 역시 독립 학술 벤치마크가 아니라고 명시돼 있다. 판단할 때는 숫자 자체보다 "같은 조건에서 우리 서비스의 로그 필드 수와 RPS로 다시 재보면 비슷한 방향이 나오는가"를 확인하는 편이 실무적이다. 10,000 RPS를 넘는 경로라면 2~6% 처리량 차이도 누적되지만, 수백 RPS 수준의 내부 API라면 이 차이는 체감되지 않는 경우가 많다.
trace_id·span_id를 로그에 넣는 게 정말 필요한가?
로그 한 줄만 보고 원인을 추정해야 하는 조직 규모라면 필요성이 커진다. OpenTelemetry는 로그·메트릭·트레이스 세 신호의 표준화를 제공하며, 2023년에 세 신호가 안정화됐다고 Grafana Labs가 정리했고, 2025년에는 프로파일링 신호를 CPU 급증 구간과 실제 코드 구간에 연결하는 방향이 강조됐다Grafana Labs 2025 정리. 실무적으로는 로그에 trace_id, span_id, service_name을 넣어 분산 트레이스와 로그를 같은 요청 기준으로 엮는 구성이 핵심으로 꼽힌다. Logz.io 릴리스 노트에도 Java Logback·Log4j2에서 OpenTelemetry 컨텍스트를 주입해 이 세 필드를 로그에 포함하는 기능이 기재돼 있다.
다만 이 구성은 공짜가 아니다. 트레이스 컨텍스트 전파 코드가 모든 서비스 경계에 들어가야 하고, 컨텍스트가 끊기는 구간(큐 소비, 배치 작업, 서드파티 콜백)에서는 trace_id가 비어버릴 수 있다. 팀이 2~3명이고 서비스가 1~2개라면 trace_id 없이도 로그만으로 원인 추적이 가능한 경우가 많아서, 이 투자는 서비스 수가 늘어나는 시점에 다시 검토하는 편이 합리적이다.
재시도·서킷브레이커 상태를 로그 필드로 어떻게 남기나?
"DB 연결 실패"라는 문장 대신 dependency, attempt, timeout_ms, circuit_state를 별도 필드로 남겨야 재시도 증폭을 나중에 쿼리할 수 있다. 이 필요성을 보여주는 사례가 2025년 9월 29일 16:58~18:46 UTC(총 1시간 48분) 발생한 PostHog Feature Flags 장애다. 미국 리전에서 플래그 평가 요청의 약 78%가 HTTP 504로 실패했는데, 데이터베이스 연결 타임아웃을 1초에서 300ms로 줄인 변경이 writer 데이터베이스 부하가 높은 시점과 겹쳤고, 17:12 UTC부터 재시도 증폭이 발생해 writer를 더 압박했다PostHog 포스트모템.
회고에서는 지수 백오프와 서킷 브레이커가 없는 재시도 로직, 읽기·쓰기 경로의 결합, 부적절한 Kubernetes liveness/readiness 설정을 원인으로 지목했고, 크래시 루프에 빠진 파드로 최대 45분 동안 트래픽이 계속 들어간 점도 복구 지연 요인으로 기록됐다. 이 사례가 로깅 설계에 주는 교훈은 명확하다. "타임아웃을 줄이면 안전해진다"가 아니라, 타임아웃·재시도·헬스체크·자원 풀을 함께 설계하고, 그 상태를 timeout_ms, attempt, backoff_ms, circuit_state, downstream, retryable, bulkhead 같은 필드로 구조화해 남겨야 사후에 "몇 번째 시도에서, 어떤 설정값으로 실패가 증폭됐는가"를 재구성할 수 있다. 여기에 애플리케이션 버전·설정 버전·배포 ID·리전까지 같이 남기면, 설정 변경과 부하 상승이 겹친 이번 같은 복합 원인도 추적이 쉬워진다.
카디널리티 폭발, 로그 필드를 메트릭 레이블로 승격해도 되나?
로그 필드는 자유롭게 늘려도 되지만, 그 필드를 메트릭 레이블로 그대로 승격하는 건 별개 문제다. OpenTelemetry Specification 1.41.0 변경 사항에는 LowMemory라는 OTLP exporter temporality 설정이 추가됐는데, 이는 메트릭 집계에 Delta temporality를 사용해 카디널리티가 급증할 때 메모리 사용량을 줄이는 목적으로 도입됐다OTel Specification CHANGELOG. 이 변경이 나왔다는 것 자체가, 사용자 ID나 URL 전체, 오류 메시지 원문처럼 값의 종류가 무한히 늘어나는 필드를 레이블로 쓰면 실제로 운영 비용 문제가 생긴다는 방증이다.
실무적으로는 역할을 분리하는 편이 안전하다. 로그에는 user_id, request_id처럼 카디널리티가 높은 필드를 자유롭게 넣되, 메트릭 레이블에는 dependency, circuit_state, status_code처럼 값의 종류가 고정된 필드만 올린다. 트레이스 조회가 필요하면 trace_id로 로그 저장소를 검색하고, 집계·알림은 낮은 카디널리티 메트릭으로 처리하는 분업이 메모리와 쿼리 비용을 동시에 관리하는 현실적인 절충안이다.
이 선택에서 자주 놓치는 함정은?
가장 흔한 함정은 "JSON으로 바꾸면 문제가 해결된다"는 가정이다. JSON 로그는 검색·집계·상관관계 분석에 유리하지만, 문자열 로그보다 직렬화와 전송 비용이 커질 수 있고, 고트래픽 경로에서 동기 파일 쓰기를 그대로 쓰면 오히려 p99 지연시간이 늘어난다. 또 OpenTelemetry 전체 계측(로그+메트릭+트레이스)은 로그만 쓰는 구성보다 메모리·CPU 비용이 커질 수 있는데, 앞서 본 150MB라는 수치는 특정 환경의 결과이지 일반값이 아니다.
두 번째 함정은 벤치마크 재현 조건을 기록하지 않는 것이다. 런타임·버전, CPU 아키텍처·인스턴스 타입, RPS·동시성·로그 필드 수, 출력 경로(stdout, 로컬 파일, Fluent Bit, OTLP Collector), 배치 크기·flush 간격·샘플링률을 같이 적지 않으면, "우리 팀에서 측정한 결과"와 "남이 측정한 결과"를 나란히 놓고 비교할 수 없다. "구조적 로그만", "로그+트레이스", "로그+메트릭+트레이스"를 같은 조건에서 각각 따로 측정해야 계측 비용을 분리해서 판단할 수 있다는 점도 자주 생략된다.
세 번째 함정은 정상 요청까지 전부 높은 보존율로 남기는 것이다. 오류와 지연이 발생한 요청은 높은 보존율로 남기고, 정상 요청은 샘플링하는 방식이 비용과 진단 가능성 사이의 현실적인 절충안이다. 이 분리를 하지 않으면 로그 저장 비용이 트래픽 증가와 선형으로 커진다.
그래서 언제 무엇을 고르나?
2026년 기준으로 구조적 로깅 도입을 판단할 때는 다음 세 축으로 좁혀서 결정하는 편이 낫다.
- 트래픽 규모: 수백 RPS 내부 서비스라면 Loguru나 자체 JSON 포맷터 수준으로 충분한 경우가 많고, 수천~만 RPS급 경로라면 OpenTelemetry 전체 계측 도입 전에 메모리·CPU 오버헤드를 자체 workload로 재측정해볼 가치가 있다.
- 팀·서비스 규모: 서비스가 1~2개이고 담당자가 소수라면 trace_id 전파 투자보다 로그 필드 표준화(의존성명, 타임아웃, 시도 횟수)가 우선순위가 높다. 서비스가 늘어나 분산 호출이 많아지면 trace_id·span_id 전파의 효용이 커진다.
- 장애 재현 요구 수준: PostHog 사례처럼 설정 변경과 부하 상승이 겹치는 복합 장애를 다뤄야 한다면, 로그에 애플리케이션 버전·설정 버전·배포 ID·circuit_state까지 남겨야 사후 재구성이 가능해진다. 단순 가용성 모니터링 수준이라면 메트릭과 알림만으로도 충분할 수 있다.