observability
contents
상용 환경(Production)에서 돌아가는 시스템을 구축하는 분들이라면 반드시 알아야 할 가장 중요한 주제 중 하나입니다. 많은 개발자들이 로그와 메트릭을 같은 것으로 취급하곤 하지만, 이 둘을 혼동하면 시스템이 느려지고, 엄청난 클라우드(AWS) 비용 폭탄을 맞으며, 늦은 밤 고통스러운 디버깅을 해야 하는 참사가 벌어집니다.
로그와 메트릭은 트레이스(Traces, 분산 추적) 와 함께 "관찰 가능성(Observability)의 세 가지 기둥" 을 구성합니다. 시스템을 사람의 몸에 비유하자면, 메트릭은 '심박수 모니터기'이고, 로그는 '상세한 진료 기록 카드'와 같습니다.
이 둘이 정확히 무엇인지, 어떻게 다른지, 그리고 어떻게 올바르게 사용해야 하는지에 대한 아주 상세한 분석입니다.
1. 메트릭 (Metrics): "대시보드와 알람 벨"
메트릭은 시간에 따라 기록된 집계된 수치 측정값(시계열 데이터, Time-Series Data) 입니다. 개별적인 이벤트를 하나하나 기록하는 것이 아니라, 특정 순간에 시스템의 상태 를 기록합니다.
- 비유: 자동차의 속도계. 지금 당장 시속 80km로 달리고 있다는 것을 알려줄 뿐, 어디서 출발했고 어디로 가는지는 알려주지 않습니다.
- 핵심 목적: "문제가 있는가?" 그리고 "언제 발생했는가?" 라는 질문에 답하기 위함입니다.
메트릭의 4가지 유형 (Prometheus 표준 기준)
- 카운터 (Counter): 오로지 증가만 하는(또는 0으로 초기화되는) 숫자입니다.
- 예시: HTTP 500 에러 발생 총횟수, 처리된 총주문 건수.
- 게이지 (Gauge): 올라갈 수도 있고 내려갈 수도 있는 숫자입니다.
- 예시: 현재 CPU 사용량, 활성화된 DB 커넥션 수, 사용 중인 JVM 힙 메모리.
- 히스토그램 (Histogram): 측정값을 "버킷(구간)"으로 그룹화하여 백분위수(p95, p99)를 계산합니다.
- 예시: HTTP 응답 시간. (예: "요청의 95%가 200ms 이내에 완료됨").
- 서머리 (Summary): 히스토그램과 비슷하지만, 서버로 보내기 전에 클라이언트 측에서 백분위수를 미리 계산합니다.
메트릭의 장단점
- 장점: * 매우 저렴함:
1500이라는 숫자를 저장하는 데 드는 메모리는 그것이 1건의 요청이든 1,000만 건의 요청이든 똑같습니다.- 빠름: 시계열 데이터베이스에서의 수학적 쿼리는 번개처럼 빠르기 때문에 자동화된 알림(예: 슬랙 알림, PagerDuty)을 트리거하는 데 완벽합니다.
- 단점: * 맥락(Context) 부족: 메트릭은 CPU가 100%를 찍었다고 알려주지만, 어떤 사용자나 쿼리가 원인인지는 알려주지 않습니다.
표준 도구: Prometheus(프로메테우스), Grafana(그라파나), Datadog(데이터독), InfluxDB, Spring Boot Actuator(Micrometer).
2. 로그 (Logs): "블랙박스 (비행 기록 장치)"
로그는 시간에 따라 발생한 개별 이벤트에 대해 타임스탬프가 찍힌 불변의(Immutable) 기록입니다.
- 비유: 일기장이나 상세 결제 영수증.
- 핵심 목적: "정확히 무슨 일이 있었는가?" 그리고 "왜 그런 일이 발생했는가?" 라는 질문에 답하기 위함입니다.
로깅의 진화
- 일반 텍스트 로그 (과거 방식):
2023-10-25 10:15:30 INFO [UserService] User john_doe failed to login due to bad password.
- 문제점: 사람은 읽기 편하지만, 컴퓨터는 아주 싫어합니다. 실패한 모든 로그인을 검색하려면 무겁고 깨지기 쉬운 정규표현식(Regex) 파싱을 해야 합니다.
- 구조화된 로그 (Structured Logs - 현대 표준):
{
"timestamp": "2023-10-25T10:15:30Z",
"level": "INFO",
"service": "UserService",
"event": "login_failed",
"user_id": "john_doe",
"reason": "bad_password"
}
- 장점: 로깅 데이터베이스에 즉시 쿼리를 날릴 수 있습니다:
SELECT * FROM logs WHERE event = 'login_failed'.
로그의 장단점
- 장점: * 무한한 맥락 제공: 변수, 스택 트레이스(Stack trace), 요청 페이로드, 사용자 ID 등을 마음껏 붙일 수 있습니다. 장애가 발생한 정확한 스토리를 알려줍니다.
- 단점: * 매우 비쌈: 만약 앱이 초당 10,000건의 요청을 처리하는데 초당 10,000줄의 로그를 쓴다면, 디스크 I/O가 터져나가고 Elasticsearch 저장 비용으로 엄청난 돈을 내야 합니다.
- 알림용으로는 너무 느림: 알람을 울리기 위해 50GB짜리 텍스트 로그에서 "ERROR"라는 단어의 개수를 세는 것은 극도로 비효율적입니다.
표준 도구: ELK 스택 (Elasticsearch, Logstash, Kibana), Splunk, Grafana Loki, Fluentd.
3. "황금 워크플로우" (둘이 함께 작동하는 방식)
메트릭이 해야 할 일을 로그로 처리해서는 안 되며, 그 반대도 마찬가지입니다. 다음은 새벽 3시에 상용 서버 장애가 터졌을 때 시니어 엔지니어가 이 둘을 활용하는 방법입니다:
- 알람 (메트릭): 프로메테우스가 최근 1분 동안
http_errors_total메트릭이 10에서 5,000으로 치솟은 것을 감지하고, 당신의 핸드폰으로 알람을 울립니다. - 대시보드 확인 (메트릭): 그라파나를 엽니다. 게이지와 히스토그램을 보니 CPU도 정상이고 DB 커넥션도 정상이지만, PaymentService(결제 서비스) 의 응답 지연 시간(Latency)이 폭발한 것을 확인합니다.
- 원인 조사 (로그): 이제 언제(새벽 3시 2분) 어디서(PaymentService) 문제가 생겼는지 알았으니, 키바나(로그 시스템)로 들어갑니다. 새벽 3시 1분부터 3분 사이의
service="PaymentService"및level="ERROR"조건으로 필터링합니다. - 근본 원인 파악 (로그): 로그 메시지를 읽습니다:
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)
- 안티 패턴 1: "로그 기반 메트릭."
log.info("주문 처리됨")이라고 로그를 남긴 다음, 일일 주문량을 계산하려고 로그 수집 툴에서 해당 문구가 몇 번 찍혔는지 세고 있다면, CPU와 돈을 낭비하고 있는 것입니다. 대신 메트릭 카운터를 1 증가시키세요. - 안티 패턴 2: "모든 것을 로깅하기." 트래픽이 많은 시스템에서 성공적인 HTTP 200 GET 요청을 일일이 로그로 남기지 마세요. 트래픽 볼륨은 메트릭으로 확인하고, 로그에는 경고(WARN), 에러(ERROR) 또는 중요한 비즈니스 상태 변경만 남기세요.
옵저버빌리티(관찰 가능성)의 최종 보스에 오신 것을 환영합니다. 메트릭(Metrics)이 "대시보드"이고 로그(Logs)가 "블랙박스"라면, 분산 추적(Distributed Tracing)은 "GPS 배송 추적기"입니다.
과거 모놀리식(Monoliths) 시대에는 디버깅이 쉬웠습니다. 요청이 들어오면 하나의 애플리케이션을 거쳐 하나의 데이터베이스를 조회했죠. 실패하면 그냥 그 서버의 로그 파일만 보면 됐습니다.
하지만 현대의 마이크로서비스 아키텍처에서는 사용자가 "결제" 버튼을 한 번 누르면 API 게이트웨이, 인증 서비스, 재고 서비스, 결제 서비스, 그리고 카프카(Kafka) 큐까지 연달아 거쳐갈 수 있습니다.
- 문제점: 만약 결제 과정에 10초가 걸렸다면, 이 5개의 서비스 중 도대체 어디서 지연이 발생한 걸까요? 메트릭은 "결제가 느립니다"라고 말하고, 로그는 "에러가 발생했습니다"라고 말할 뿐입니다. 둘 다 사용자 요청의 전체적인 여정(Complete Journey) 을 보여주지는 못합니다.
이러한 마이크로서비스의 미스터리를 해결해 주는 분산 추적의 개념과 내부 작동 원리에 대한 상세 분석입니다.
1. 핵심 개념: "택배 배송 조회 (GPS Package Tracker)"
온라인에서 물건을 주문하고 '운송장 번호(Tracking Number)'를 받았다고 상상해 보세요.
- 출발지 물류센터에서 바코드가 찍힙니다 (타임스탬프).
- 공항 터미널에서 바코드가 찍힙니다 (타임스탬프).
- 동네 택배 트럭에서 바코드가 찍힙니다 (타임스탬프).
택배가 3개의 다른 회사를 거쳐가더라도, 단 하나의 운송장 번호가 이 모든 여정을 하나로 묶어줍니다. 분산 추적은 여러분의 HTTP 요청에 대해 정확히 똑같은 일을 수행합니다.
2. 트레이스의 해부학: Trace ID vs. Span ID
이 시스템이 작동하려면 특정한 용어를 알아야 합니다. 트레이스(Trace)는 단순한 평면 기록이 아니라 트리(Tree) 구조로 되어 있습니다.
- 트레이스 (Trace - 전체 여정): 단일 사용자 요청이 모든 서비스를 거쳐가는 전체 생명주기를 나타냅니다.
- 트레이스 ID (Trace ID - 운송장 번호): 전역적으로 고유하게 생성된 문자열입니다(예:
4bf92f3577b34da6a3ce929d0e0e4736). 요청이 앱의 가장 첫 번째 서버(예: API 게이트웨이)에 도달하는 순간 생성되며, 이후 호출되는 모든 서비스로 계속 전달됩니다. - 스팬 (Span - 단일 구간/홉): 트레이스 내에서 수행되는 하나의 논리적인 작업 단위입니다. 예를 들어 "인증 서비스에서 토큰 검증하기"가 하나의 Span이고, "DB에서 SELECT 쿼리 실행하기"가 또 다른 Span입니다.
- 스팬 ID (Span ID): 해당 특정 작업 단위에 부여되는 고유 ID입니다. 모든 Span은 자신의 부모(Parent) Span ID를 알고 있기 때문에, 나중에 트리 형태로 연결될 수 있습니다.
3. 마법의 원리: "바통 터치" (컨텍스트 전파, Context Propagation)
Service A는 자신이 가진 Trace ID를 Service B에게 어떻게 알려줄까요? 바로 HTTP 헤더(Headers) 를 통해서입니다.
Service A가 Service B로 REST API 호출을 할 때, 트레이싱 라이브러리가 요청을 자동으로 가로채서 특수한 헤더를 주입합니다.
현대적 표준 (W3C Trace Context):
GET /api/inventory/item/123 HTTP/1.1
Host: inventory-service
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
4bf92f...: 이것이 Trace ID입니다.Service B는 이 값을 추출하고 "아, 나는 이 거대한 여정의 일부구나"라고 인식합니다.00f067...: 이것은 부모 Span ID(Service A의 ID)입니다.
만약 Kafka나 RabbitMQ 같은 메시지 큐를 사용한다면, HTTP 헤더 대신 메시지 헤더(Message Headers)에 이 트레이스 컨텍스트가 주입됩니다.
4. 트레이스 시각화: 간트 차트 (Gantt Chart)
모든 마이크로서비스가 자신들의 Span 정보를 중앙 추적 서버로 보내면, 서버는 Trace ID와 부모 Span ID를 사용하여 이 조각들을 퍼즐처럼 맞춥니다.
그 결과, 아름다운 폭포수(Waterfall) / 간트 차트가 만들어집니다.
이 차트를 보면 다음 사실들을 즉시 알 수 있습니다:
- 전체 요청 처리에 2.5초가 걸렸습니다.
AuthService(인증)는 50ms 걸렸습니다.InventoryService(재고)는 100ms 걸렸습니다.PaymentService(결제)가 무려 2.3초나 걸렸습니다! (누구 멱살을 잡아야 할지 즉시 알 수 있죠).- 서비스들이 순차적으로 호출되었는지, 아니면 병렬(Parallel)로 호출되었는지도 한눈에 보입니다.
5. 표준 도구 (OpenTelemetry의 혁명)
과거에는 회사마다 각기 다른 라이브러리(Zipkin, Jaeger, New Relic 에이전트 등)를 사용하여 표준화가 악몽과도 같았습니다.
오늘날 업계는 OpenTelemetry (OTel) 라는 범용 표준에 합의했습니다.
- OpenTelemetry는 백엔드 저장소가 아닙니다. Java/Go/Python 코드에 삽입하는 오픈 표준 SDK입니다. 트레이스, 로그, 메트릭을 생성하고 이를 표준 형식으로 내보내는(Export) 역할을 합니다.
- 백엔드 (저장소/UI): 생성된 OTel 데이터를 Jaeger, Zipkin, Datadog, Honeycomb, Grafana Tempo 같은 시각화 도구(백엔드)로 전송하여 차트를 봅니다.
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시의 궁극적인 디버깅 워크플로우
- 메트릭(Metrics) 이 당신을 깨웁니다: "CPU가 치솟고 있고, 에러율이 15%를 넘었습니다!"
- 트레이스(Traces) 가 어디를 봐야 할지 정확히 짚어줍니다: "간트 차트를 보세요.
OrderService의 DB 쿼리 스팬(Span)이 엄청나게 길어서 응답에 5초나 걸리고 있습니다." - 로그(Logs) 가 왜 그런 일이 벌어졌는지 설명해 줍니다: "그 느린 스팬의 Trace ID를 복사해서 Elasticsearch에 검색해 보니 이런 로그가 있네요:
Deadlock found when trying to get lock; try restarting transaction (데드락 발생)."
당신은 DB 쿼리 데드락을 해결하고, 다시 잠자리에 들며, 다음 날 아침 개발팀의 영웅이 됩니다.
references