Jerry's Log

observability

contents

상용 환경(Production)에서 돌아가는 시스템을 구축하는 분들이라면 반드시 알아야 할 가장 중요한 주제 중 하나입니다. 많은 개발자들이 로그와 메트릭을 같은 것으로 취급하곤 하지만, 이 둘을 혼동하면 시스템이 느려지고, 엄청난 클라우드(AWS) 비용 폭탄을 맞으며, 늦은 밤 고통스러운 디버깅을 해야 하는 참사가 벌어집니다.

로그와 메트릭은 트레이스(Traces, 분산 추적) 와 함께 "관찰 가능성(Observability)의 세 가지 기둥" 을 구성합니다. 시스템을 사람의 몸에 비유하자면, 메트릭은 '심박수 모니터기'이고, 로그는 '상세한 진료 기록 카드'와 같습니다.

이 둘이 정확히 무엇인지, 어떻게 다른지, 그리고 어떻게 올바르게 사용해야 하는지에 대한 아주 상세한 분석입니다.


1. 메트릭 (Metrics): "대시보드와 알람 벨"

메트릭은 시간에 따라 기록된 집계된 수치 측정값(시계열 데이터, Time-Series Data) 입니다. 개별적인 이벤트를 하나하나 기록하는 것이 아니라, 특정 순간에 시스템의 상태 를 기록합니다.

메트릭의 4가지 유형 (Prometheus 표준 기준)

  1. 카운터 (Counter): 오로지 증가만 하는(또는 0으로 초기화되는) 숫자입니다.
    • 예시: HTTP 500 에러 발생 총횟수, 처리된 총주문 건수.
  2. 게이지 (Gauge): 올라갈 수도 있고 내려갈 수도 있는 숫자입니다.
    • 예시: 현재 CPU 사용량, 활성화된 DB 커넥션 수, 사용 중인 JVM 힙 메모리.
  3. 히스토그램 (Histogram): 측정값을 "버킷(구간)"으로 그룹화하여 백분위수(p95, p99)를 계산합니다.
    • 예시: HTTP 응답 시간. (예: "요청의 95%가 200ms 이내에 완료됨").
  4. 서머리 (Summary): 히스토그램과 비슷하지만, 서버로 보내기 전에 클라이언트 측에서 백분위수를 미리 계산합니다.

메트릭의 장단점

표준 도구: Prometheus(프로메테우스), Grafana(그라파나), Datadog(데이터독), InfluxDB, Spring Boot Actuator(Micrometer).


2. 로그 (Logs): "블랙박스 (비행 기록 장치)"

로그는 시간에 따라 발생한 개별 이벤트에 대해 타임스탬프가 찍힌 불변의(Immutable) 기록입니다.

로깅의 진화

  1. 일반 텍스트 로그 (과거 방식):
2023-10-25 10:15:30 INFO  [UserService] User john_doe failed to login due to bad password.
  1. 구조화된 로그 (Structured Logs - 현대 표준):
{
  "timestamp": "2023-10-25T10:15:30Z",
  "level": "INFO",
  "service": "UserService",
  "event": "login_failed",
  "user_id": "john_doe",
  "reason": "bad_password"
}

로그의 장단점

표준 도구: ELK 스택 (Elasticsearch, Logstash, Kibana), Splunk, Grafana Loki, Fluentd.


3. "황금 워크플로우" (둘이 함께 작동하는 방식)

메트릭이 해야 할 일을 로그로 처리해서는 안 되며, 그 반대도 마찬가지입니다. 다음은 새벽 3시에 상용 서버 장애가 터졌을 때 시니어 엔지니어가 이 둘을 활용하는 방법입니다:

  1. 알람 (메트릭): 프로메테우스가 최근 1분 동안 http_errors_total 메트릭이 10에서 5,000으로 치솟은 것을 감지하고, 당신의 핸드폰으로 알람을 울립니다.
  2. 대시보드 확인 (메트릭): 그라파나를 엽니다. 게이지와 히스토그램을 보니 CPU도 정상이고 DB 커넥션도 정상이지만, PaymentService(결제 서비스) 의 응답 지연 시간(Latency)이 폭발한 것을 확인합니다.
  3. 원인 조사 (로그): 이제 언제(새벽 3시 2분) 어디서(PaymentService) 문제가 생겼는지 알았으니, 키바나(로그 시스템)로 들어갑니다. 새벽 3시 1분부터 3분 사이의 service="PaymentService"level="ERROR" 조건으로 필터링합니다.
  4. 근본 원인 파악 (로그): 로그 메시지를 읽습니다: Connection timed out to Stripe API. 아, 외부 결제망 API에 타임아웃이 났군요. 버그의 원인을 찾았습니다.

4. 요약 비교 테이블

특징 메트릭 (Metrics) 로그 (Logs)
무엇인가? 집계된 숫자 (시계열 데이터) 특정 이벤트에 대한 상세 기록
대답하는 질문 "고장 났나?" / "언제 고장 났나?" "왜 고장 났나?" / "무슨 일이 있었나?"
저장 비용 매우 낮음 (압축률이 매우 높음) 매우 높음 (대량의 텍스트/JSON)
최적의 용도 대시보드, 알림(Alerting), 헬스 체크 디버깅, 감사(Auditing), 원인 분석
카디널리티(Cardinality) 반드시 낮아야 함 (메트릭에 User ID를 넣으면 안 됨) 무한할 수 있음 (원하는 데이터 다 넣어도 됨)
주요 도구 Prometheus, Grafana, Micrometer Elasticsearch, Loki, Splunk

5. 피해야 할 흔한 안티 패턴 (Anti-Patterns)


옵저버빌리티(관찰 가능성)의 최종 보스에 오신 것을 환영합니다. 메트릭(Metrics)이 "대시보드"이고 로그(Logs)가 "블랙박스"라면, 분산 추적(Distributed Tracing)은 "GPS 배송 추적기"입니다.

과거 모놀리식(Monoliths) 시대에는 디버깅이 쉬웠습니다. 요청이 들어오면 하나의 애플리케이션을 거쳐 하나의 데이터베이스를 조회했죠. 실패하면 그냥 그 서버의 로그 파일만 보면 됐습니다.

하지만 현대의 마이크로서비스 아키텍처에서는 사용자가 "결제" 버튼을 한 번 누르면 API 게이트웨이, 인증 서비스, 재고 서비스, 결제 서비스, 그리고 카프카(Kafka) 큐까지 연달아 거쳐갈 수 있습니다.

이러한 마이크로서비스의 미스터리를 해결해 주는 분산 추적의 개념과 내부 작동 원리에 대한 상세 분석입니다.


1. 핵심 개념: "택배 배송 조회 (GPS Package Tracker)"

온라인에서 물건을 주문하고 '운송장 번호(Tracking Number)'를 받았다고 상상해 보세요.

택배가 3개의 다른 회사를 거쳐가더라도, 단 하나의 운송장 번호가 이 모든 여정을 하나로 묶어줍니다. 분산 추적은 여러분의 HTTP 요청에 대해 정확히 똑같은 일을 수행합니다.

2. 트레이스의 해부학: Trace ID vs. Span ID

이 시스템이 작동하려면 특정한 용어를 알아야 합니다. 트레이스(Trace)는 단순한 평면 기록이 아니라 트리(Tree) 구조로 되어 있습니다.

  1. 트레이스 (Trace - 전체 여정): 단일 사용자 요청이 모든 서비스를 거쳐가는 전체 생명주기를 나타냅니다.
  2. 트레이스 ID (Trace ID - 운송장 번호): 전역적으로 고유하게 생성된 문자열입니다(예: 4bf92f3577b34da6a3ce929d0e0e4736). 요청이 앱의 가장 첫 번째 서버(예: API 게이트웨이)에 도달하는 순간 생성되며, 이후 호출되는 모든 서비스로 계속 전달됩니다.
  3. 스팬 (Span - 단일 구간/홉): 트레이스 내에서 수행되는 하나의 논리적인 작업 단위입니다. 예를 들어 "인증 서비스에서 토큰 검증하기"가 하나의 Span이고, "DB에서 SELECT 쿼리 실행하기"가 또 다른 Span입니다.
  4. 스팬 ID (Span ID): 해당 특정 작업 단위에 부여되는 고유 ID입니다. 모든 Span은 자신의 부모(Parent) Span ID를 알고 있기 때문에, 나중에 트리 형태로 연결될 수 있습니다.

3. 마법의 원리: "바통 터치" (컨텍스트 전파, Context Propagation)

Service A는 자신이 가진 Trace ID를 Service B에게 어떻게 알려줄까요? 바로 HTTP 헤더(Headers) 를 통해서입니다.

Service AService B로 REST API 호출을 할 때, 트레이싱 라이브러리가 요청을 자동으로 가로채서 특수한 헤더를 주입합니다.

현대적 표준 (W3C Trace Context):

GET /api/inventory/item/123 HTTP/1.1
Host: inventory-service
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

만약 Kafka나 RabbitMQ 같은 메시지 큐를 사용한다면, HTTP 헤더 대신 메시지 헤더(Message Headers)에 이 트레이스 컨텍스트가 주입됩니다.


4. 트레이스 시각화: 간트 차트 (Gantt Chart)

모든 마이크로서비스가 자신들의 Span 정보를 중앙 추적 서버로 보내면, 서버는 Trace ID와 부모 Span ID를 사용하여 이 조각들을 퍼즐처럼 맞춥니다.

그 결과, 아름다운 폭포수(Waterfall) / 간트 차트가 만들어집니다.

이 차트를 보면 다음 사실들을 즉시 알 수 있습니다:


5. 표준 도구 (OpenTelemetry의 혁명)

과거에는 회사마다 각기 다른 라이브러리(Zipkin, Jaeger, New Relic 에이전트 등)를 사용하여 표준화가 악몽과도 같았습니다.

오늘날 업계는 OpenTelemetry (OTel) 라는 범용 표준에 합의했습니다.


6. 궁극의 목표: 로그와 트레이스의 연결 (MDC)

트레이싱 단독으로는 어디서 지연이 발생했는지는 잘 찾아주지만, 항상 그런 일이 생겼는지까지 알려주지는 않습니다. 궁극의 디버깅 경험을 얻으려면 여러분의 로그 파일에 Trace ID를 주입(Inject) 해야 합니다.

Spring Boot에서는 Micrometer Tracing 같은 라이브러리가 SLF4J의 MDC (Mapped Diagnostic Context) 에 Trace ID를 자동으로 밀어 넣어 줍니다.

로그 파일 출력 결과:

[INFO] [TraceId: 4bf92f35...] [ServiceA] - 사용자가 결제 버튼을 클릭함
[INFO] [TraceId: 4bf92f35...] [ServiceB] - 99번 상품의 재고 확인 중
[ERROR] [TraceId: 4bf92f35...] [ServiceC] - 외부 결제 게이트웨이 타임아웃 발생!

이제 사용자가 결제에 실패했다고 항의할 때, 짐작으로 원인을 찾을 필요가 없습니다. 해당 사용자의 Trace ID를 복사해서 로그 수집기(예: Kibana)에 검색하기만 하면, 해당 트랜잭션에 관여한 모든 마이크로서비스의 모든 로그가 완벽한 시간순으로 정렬되어 눈앞에 나타납니다.

요약: 새벽 3시의 궁극적인 디버깅 워크플로우

  1. 메트릭(Metrics) 이 당신을 깨웁니다: "CPU가 치솟고 있고, 에러율이 15%를 넘었습니다!"
  2. 트레이스(Traces) 가 어디를 봐야 할지 정확히 짚어줍니다: "간트 차트를 보세요. OrderService의 DB 쿼리 스팬(Span)이 엄청나게 길어서 응답에 5초나 걸리고 있습니다."
  3. 로그(Logs) 가 왜 그런 일이 벌어졌는지 설명해 줍니다: "그 느린 스팬의 Trace ID를 복사해서 Elasticsearch에 검색해 보니 이런 로그가 있네요: Deadlock found when trying to get lock; try restarting transaction (데드락 발생)."

당신은 DB 쿼리 데드락을 해결하고, 다시 잠자리에 들며, 다음 날 아침 개발팀의 영웅이 됩니다.

references