Observability는 왜 Monitoring과 다른가
임계값 알림이 답하지 못하는 Unknown Unknowns부터 메트릭/로그/트레이스 세 기둥의 역할 분담, OTel 표준화, 계측 방법론까지 — 분산 시스템 관찰 가능성의 근본 구조를 추적한다.
- 01 Observability는 왜 Monitoring과 다른가
- 02 Java Agent는 코드 한 줄 없이 어떻게 계측하는가
- 03 분산 추적은 어떻게 서비스 경계를 넘는가
- 04 Prometheus는 왜 Pull 방식인가
- 05 로그는 왜 구조여야 하는가 — Observability 로깅의 설계 철학
- 06 관찰 가능성 스택은 어떻게 하나로 연결되는가
- 07 Observability는 왜 세 신호가 함께여야 하는가
마이크로서비스 환경에서 장애가 나면 알림은 온다. “order-service 응답 시간 임계값 초과.” 그런데 CPU는 정상, 메모리도 정상, 에러율도 정상이다. 느리다는 것은 알지만 왜 느린지는 모른다. 이 상황이 Monitoring과 Observability의 경계다.
Monitoring이 답하지 못하는 질문
Monitoring은 내가 미리 중요하다고 정의한 것을 추적한다. CPU 80% 초과, 에러율 1% 초과 — 이런 임계값은 Known Unknowns, 즉 “발생할 수 있다고 알고 있는 문제”에만 반응한다.
분산 시스템의 장애 대부분은 다르다. 특정 이미지 크기에서만 CDN이 타임아웃을 낸다거나, 특정 사용자 ID 패턴이 해시 충돌을 일으켜 한 샤드에 부하를 몰아넣는 경우 — 이런 Unknown Unknowns는 임계값을 아무리 세밀하게 설정해도 사전에 예측할 수 없다.
Observability는 이 문제를 다르게 푼다. “내가 미처 생각하지 못한 질문에도 사후에 답할 수 있는 능력”이다. CNCF 정의로 풀면 세 가지 속성이 필요하다: 시스템이 내부 상태를 데이터로 노출하는 계측 가능성, 메트릭-로그-트레이스가 같은 요청을 서로 참조할 수 있는 연결 가능성, 그리고 사전에 정의하지 않은 질문에도 사후에 답할 수 있는 임의 조회 가능성. 셋 중 하나라도 빠지면 Observability가 아니라 고급 Monitoring에 가깝다.
세 기둥이 역할을 나누는 이유
메트릭, 로그, 트레이스는 동일한 요청을 서로 다른 각도로 표현한다.
메트릭은 숫자의 시간 흐름이다. p99 응답 시간이 3초로 치솟는 것을 감지한다. 하지만 집계된 숫자이므로 “왜?”는 답하지 못한다. Prometheus는 낮은 카디널리티 설계를 채택했는데, user_id를 레이블로 쓰면 1,000만 사용자 × 메트릭 수만큼 시계열이 폭발하기 때문이다. 메트릭은 추세와 알림을 위한 도구다.
트레이스는 하나의 요청이 시스템을 거치는 전체 경로의 기록이다. Waterfall 뷰로 보면 어느 서비스가 어느 구간에서 얼마나 걸렸는지 즉시 파악된다.
POST /api/payments 2850ms
└─ fraud-detection 2800ms ← 병목
└─ external-api 2750ms ← 진짜 원인
로그는 특정 시점에 발생한 이벤트의 기록이다. 트레이스가 “어디서”를 알려주면, 로그가 “왜”를 알려준다. 이 연결이 작동하려면 로그에 traceId가 포함되어야 한다. MDC에 TraceId를 삽입하면 모든 로그에 자동으로 포함된다.
세 기둥을 연결하는 접착제가 Exemplar다. Prometheus 메트릭 샘플에 TraceId를 첨부하는 메커니즘으로, Grafana에서 p99 스파이크 시점의 점을 클릭하면 Tempo로 자동 이동한다. 메트릭 → 트레이스 → 로그의 3단계 드릴다운이 가능해지고 MTTR이 30분에서 5분으로 줄어든다.
세 기둥의 비용 구조는 다르다. 메트릭은 하루 수십 MB로 수년 보관이 가능하다. 로그는 압축 후에도 하루 수십 GB, 7~30일 보관이 현실적이다. 트레이스는 100% 수집 시 하루 수백 GB에 달하므로 샘플링이 필수다. Tail Sampling — 에러/느린 요청은 100%, 나머지는 1% — 으로 비용과 완전성을 동시에 잡는다.
OpenTelemetry가 해결한 파편화
2019년 이전의 분산 추적 생태계는 파편화되어 있었다. Zipkin, Jaeger, AWS X-Ray, Datadog — 각각 다른 SDK, 다른 TraceId 포맷. 서비스 A(Zipkin)가 서비스 B(Jaeger)를 호출하면 하나의 Trace로 연결되지 않았다. 도구를 바꾸려면 모든 서비스 코드를 수정해야 했다.
OpenTelemetry는 계측 코드와 백엔드를 분리함으로써 이 문제를 해결한다. 아키텍처는 네 계층으로 구성된다.
OTel API ← 계측 코드가 의존하는 인터페이스
↕
OTel SDK ← 실제 구현체 (Span 생성, 샘플링, 버퍼링)
↕ OTLP
OTel Collector ← 수신/처리/전송 중간 계층 (선택)
↕
Backend ← Tempo, Loki, Prometheus, Datadog 등
API와 SDK를 분리한 이유가 있다. 라이브러리(Spring MVC, JDBC 드라이버)가 SDK에 의존하면 버전 충돌이 발생한다. API에만 의존하면 SDK가 없을 때 No-op으로 동작하고, 사용자 앱의 SDK 설정에 자동으로 연결된다. 백엔드 변경은 Collector 설정 변경만으로 가능하고 코드는 그대로다.
OTLP(OpenTelemetry Protocol)는 이 계층을 연결하는 표준 전송 포맷이다. gRPC(포트 4317)와 HTTP(포트 4318)를 지원하며, Resource(서비스 메타데이터) + Scope(계측 라이브러리 정보) + Signal(실제 Span/메트릭/로그 데이터) 구조로 기존 Zipkin/Jaeger 포맷보다 훨씬 풍부하다.
계측은 어떻게 추가하는가
계측 방법에는 세 가지가 있고, 계층별로 역할이 다르다.
Java Agent 자동 계측은 코드 변경 없이 인프라 레이어를 커버한다. -javaagent:opentelemetry-javaagent.jar 한 줄로 Spring MVC, JDBC, Redis, Kafka 등의 Span이 자동 생성된다. 단, 비즈니스 로직 안은 블랙박스로 남는다.
@Observed 수동 계측은 비즈니스 서비스 메서드 경계를 Trace로 표현한다. Spring AOP 기반으로 동작하며, 어노테이션 하나로 Trace Span과 메트릭(호출 횟수, 실행 시간)이 자동 생성된다. 단, private 메서드에는 AOP가 적용되지 않고, 루프 안에서 반복 호출되는 메서드에 붙이면 Span이 수백 개 생성되므로 주의해야 한다.
Micrometer 직접 계측은 비즈니스 KPI를 직접 정의한다. 결제 방법별 성공률, 주문 금액 분포 같은 도메인 의미 있는 메트릭은 자동 계측으로 얻을 수 없다. 태그 설계에서 낮은 카디널리티 값(결제 수단, 지역)만 사용해야 한다는 원칙은 여기서도 유효하다.
정리
- Monitoring은 Known Unknowns를 감지한다. Observability는 Unknown Unknowns도 사후 조사할 수 있게 한다.
- 메트릭은 “무슨 일이 일어나는가”, 트레이스는 “어디서”, 로그는 “왜”를 답한다. Exemplar가 세 기둥을 TraceId로 연결한다.
- OpenTelemetry는 계측 코드와 백엔드를 분리해 벤더 종속을 끊는다. 한 번 계측하면 어떤 백엔드로도 전송할 수 있다.
- 계측은 Java Agent(인프라) →
@Observed(서비스 경계) → Micrometer(비즈니스 KPI) 순으로 계층을 쌓는다.
Monitoring을 버리는 것이 아니다. Monitoring이 Observability 안에 포함된다 — 임계값 알림은 여전히 유효하고, 그 이후의 “왜?”를 추적하는 능력이 추가된 것이다.
다음 글에서는 Prometheus TSDB가 메트릭을 어떻게 저장하는지, Gorilla 압축과 시계열 청크 구조를 추적한다.