3 Pillars 메트릭 분산 추적 프로파일링·덤프 트러블슈팅 플레이북 📝 문제풀이
◀ 심화 CH 14 📋 목차 심화 CH 16 ▶
🔭 심화 CHAPTER 15 · 5년차 트랙

장애는 반드시 온다 — 그때 무엇을 보고, 어떤 도구를 잡을까?

기능을 잘 만드는 것과 운영 중 터진 문제를 빠르게 짚는 것은 다른 근육이에요. 이번 장은 관찰성(Observability)의 세 기둥과, "CPU 100%·메모리 누수·지연 급증·데드락" 같은 실전 증상을 증상 → 도구 → 원인으로 잇는 트러블슈팅 플레이북을 다룹니다. 시니어라면 반드시 손에 익혀야 할 마지막 관문이에요.

🎯 이 장을 끝내면
🔭
3 Pillars
로그 · 메트릭 · 트레이스
"무슨 일이 일어났나"를 세 각도에서 본다.

관찰성(Observability)은 시스템 외부로 나오는 신호만 보고 내부 상태를 추론할 수 있는 성질이에요. 그 신호를 세 종류로 나눈 게 세 기둥입니다 — 셋은 대체재가 아니라 서로 보완해요.

📜
로그(Logs)
개별 이벤트의 상세 기록 · "정확히 뭐가?"
·
📈
메트릭(Metrics)
시간별 집계 수치 · "얼마나·언제부터?"
·
🧵
트레이스(Traces)
요청의 서비스 간 여정 · "어디서 느려졌나?"
기둥정체언제 쓰나
로그(Logs)타임스탬프가 붙은 개별 이벤트의 상세 텍스트/구조체특정 요청의 정확한 사정·스택트레이스·예외 원인을 파고들 때
메트릭(Metrics)일정 간격으로 집계된 수치 시계열(카운터·게이지·히스토그램)대시보드·알림(alert)·추세("언제부터 나빠졌나")를 볼 때. 저장 비용이 싸다
트레이스(Traces)하나의 요청이 여러 서비스를 거친 경로(span의 트리)MSA에서 어느 구간이 병목인지, 요청이 어디서 실패했는지 짚을 때
🧱 구조적 로깅(Structured Logging)과 상관관계 ID 운영 로그는 사람이 눈으로 읽는 문장이 아니라 기계가 파싱하는 데이터여야 해요. 그래서 JSON 구조적 로깅(키-값)을 씁니다 — level·timestamp·traceId·userId 같은 필드로 필터·집계가 가능해지죠.
핵심 필드가 상관관계 ID(correlation/trace ID)예요. 요청 진입점에서 ID를 하나 발급해 MDC(Mapped Diagnostic Context)에 넣으면, 그 요청이 남기는 모든 로그 줄에 같은 ID가 붙어요. 그래서 수백만 줄 로그에서 ID 하나로 특정 요청의 전 여정을 한 번에 뽑을 수 있어요. 이게 상관관계 ID의 존재 이유입니다.
// 요청 진입점(필터/인터셉터)에서 상관관계 ID를 MDC에 심는다
String traceId = req.getHeader("X-Trace-Id");
if (traceId == null) traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);        // 이후 이 스레드의 모든 로그에 자동 첨부
try {
    chain.doFilter(req, res);
} finally {
    MDC.clear();                    // 스레드 풀 재사용 → 반드시 정리!
}
💡
세 기둥을 잇는 게 실전이에요. 메트릭 알림이 "p99 지연이 튀었다"고 알려주면 → 트레이스로 어느 서비스 구간인지 좁히고 → 그 구간의 trace ID로 로그를 뒤져 정확한 원인을 봅니다. 로그·메트릭·트레이스에 같은 traceId가 흐르도록 설계하는 것이 관찰성의 완성이에요.
📈
메트릭
무엇을, 어떤 분위수로 측정할까
Micrometer→Prometheus · RED/USE · p95/p99 · 카디널리티

스프링 부트 세계의 표준 계측 파사드는 Micrometer예요. 코드에서는 Micrometer API로 계측만 하고, 백엔드는 Prometheus·Datadog 등으로 갈아끼울 수 있죠("SLF4J의 메트릭 버전"). Prometheus가 /actuator/prometheus 엔드포인트를 주기적으로 pull(scrape)해 시계열로 저장합니다.

방법론대상보는 지표
RED요청 기반 서비스(사용자가 보는 관점)Rate(초당 요청) · Errors(에러율) · Duration(지연 분포)
USE리소스(CPU·메모리·디스크·풀)Utilization(사용률) · Saturation(포화·대기큐) · Errors
⚠️
지연을 "평균"으로 보면 안 됩니다. 응답시간 분포는 롱테일(long tail)이라 소수의 느린 요청이 평균에 묻혀요. 평균 50ms여도 p99가 2초면 100명 중 1명은 2초를 기다리는 겁니다 — 그리고 한 페이지가 여러 API를 호출하면 사용자 대부분이 그 꼬리를 밟아요. 그래서 SLO는 p95/p99 같은 분위수(percentile)로 잡습니다. "평균은 아무도 경험하지 않는 값"이라는 격언이 여기서 나와요.
💣 카디널리티 폭발(Cardinality Explosion) Prometheus는 레이블(label) 조합마다 별도의 시계열을 만들어요. 그래서 레이블에 userId·주문번호·요청 URL의 경로 변수처럼 값의 종류가 무한한 것을 넣으면 시계열 수가 폭발해 메모리·저장소가 터집니다. 레이블은 값의 가짓수가 유한하고 낮은 것(HTTP 메서드·상태코드·엔드포인트 템플릿 /user/{id})만 써야 해요. 고유 식별자는 레이블이 아니라 로그·트레이스로 넘깁니다.
// Micrometer: 타이머로 지연 분포(분위수 포함)를 계측
Timer timer = Timer.builder("http.server.requests")
    .tag("uri", "/orders/{id}")     // ✅ 템플릿(유한) — 카디널리티 안전
    // .tag("orderId", orderId)     // ❌ 고유값 — 카디널리티 폭발!
    .publishPercentiles(0.95, 0.99) // p95·p99 산출
    .register(registry);
timer.record(() -> handleRequest());
🧭
분위수는 합산되지 않아요. 여러 인스턴스의 p99를 단순 평균하면 틀립니다. 그래서 Prometheus는 히스토그램(버킷)을 수집하고 histogram_quantile()집계 시점에 분위수를 계산해요. 서비스별 SLO를 정확히 보려면 이 구조를 이해해야 합니다.
🖼️ 그림으로 보기 — 응답시간 분위수 — 평균의 함정
응답 시간 → 요청 수 p50 중앙 평균 p95 p99 (2초!) 평균은 아무도 경험 못하는 값 · 느린 꼬리가 UX를 지배 → SLO는 p95/p99
🧵
분산 추적
요청 하나가 지나간 길을 잇는다
OpenTelemetry · trace · span · 컨텍스트 전파

MSA에서 요청 하나는 게이트웨이 → 주문 → 결제 → 재고처럼 여러 서비스를 넘나들어요. 로그만으론 "이 서비스가 느리다"까지가 한계죠. 분산 추적은 요청 전체를 하나로 꿰어 어느 구간이 병목·실패인지 시각화합니다.

개념의미
Trace하나의 요청이 시스템 전체를 지나간 전체 여정. 고유 traceId로 식별
Span그 여정 안의 개별 작업 단위(예: "결제 API 호출", "DB 쿼리"). 시작·종료 시각·태그를 가짐
부모-자식span은 트리 구조. 상위 작업이 하위 작업을 호출하면 자식 span이 됨
컨텍스트 전파서비스 경계를 넘을 때 HTTP 헤더(traceparent, W3C Trace Context)로 traceId를 전달
🔧 OpenTelemetry(OTel) — 벤더 중립 표준 OpenTelemetry는 추적·메트릭·로그 계측의 업계 표준 규격+SDK예요. 자바 에이전트(-javaagent:opentelemetry-javaagent.jar)를 붙이면 코드 수정 없이 자동 계측(HTTP·JDBC·메시징)이 되고, 수집한 데이터는 Jaeger·Tempo·Zipkin 같은 백엔드로 내보냅니다. 핵심은 서비스 경계를 넘어도 같은 traceId가 이어진다는 것. 그래서 게이트웨이에서 시작된 요청이 결제 서비스에서 500이 났을 때, 그 한 요청의 전 구간 타임라인을 한 화면에서 봅니다.
💡
샘플링(sampling)이 필요해요. 모든 요청을 100% 추적하면 오버헤드·저장비가 큽니다. 보통 일부만 샘플링하되(예: 1~10%), 에러·느린 요청은 우선 채집(tail-based sampling)하도록 정책을 둬요. 정상 트래픽은 메트릭으로, 이상 요청은 트레이스로 — 역할 분담이 핵심입니다.
🖼️ 그림으로 보기 — 분산 추적 — traceId가 서비스를 관통한다
traceId = abc123 · 하나의 요청이 서비스들을 관통 게이트웨이 210ms 주문 서비스 170ms 결제 서비스 병목 · 90ms 재고 서비스 40ms 시간 → 같은 traceId가 traceparent 헤더로 전파 → 결제 span이 병목임을 한눈에
🩺
프로파일링 · 덤프
JVM 속을 열어보는 네 가지 도구
스레드 덤프 · 힙 덤프 · JFR/async-profiler · GC 로그

메트릭·트레이스가 "밖에서 본 신호"라면, 이제부터는 JVM 내부를 직접 뜯어보는 저수준 도구예요. 각각 진단하는 문제의 결이 다릅니다.

도구뜨는 것주 용도
스레드 덤프 (jstack <pid>)그 순간 모든 스레드의 스택·상태(RUNNABLE/BLOCKED/WAITING)데드락·블로킹 스레드 진단, "지금 스레드들이 어디서 멈춰있나"
힙 덤프 (jmap -dump + MAT)그 순간 힙의 모든 객체 스냅샷메모리 누수 분석 — MAT의 지배자 트리(dominator tree)로 누가 메모리를 붙잡는지
JFR / async-profiler일정 시간 샘플링된 CPU·할당 프로파일어느 메서드가 CPU·할당을 먹나(플레임 그래프), 저오버헤드 상시 프로파일
GC 로그 (-Xlog:gc*)GC 빈도·pause 시간·회수량 이력지연의 원인이 GC(특히 잦은 Full GC)인지 판별
🔍 스레드 덤프 vs 힙 덤프 — 헷갈리지 말자 스레드 덤프"지금 스레드들이 무엇을 하고 있나"의 스냅샷 — 가볍고, 여러 번 연속으로 떠서 어느 스레드가 계속 바쁜지/멈춰있는지 비교해요. CPU 급증·데드락·행(hang)에 쓰죠.
힙 덤프"메모리에 무슨 객체가 얼마나"의 스냅샷 — 무겁고(힙 크기만큼), 뜰 때 STW가 걸릴 수 있어요. 메모리 누수 분석용입니다. 한 줄 요약: CPU·행(hang)·데드락 → 스레드 덤프, 메모리 누수·OOM → 힙 덤프.
💡
덤프는 자동으로 확보해두세요. -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump를 걸면 OOM 순간의 힙이 자동 저장돼요(사후 부검용). 운영 상시 프로파일은 JFR(-XX:StartFlightRecording)이 오버헤드가 낮아 실전에서 많이 씁니다. async-profiler는 커널까지 포함한 플레임 그래프가 강력해요.
jstack jmap + MAT JFR async-profiler GC log jcmd
🚑
트러블슈팅 플레이북
증상 → 도구 → 원인
새벽 3시 알림에도 순서대로 움직일 수 있게.

장애 대응의 본질은 증상을 보고 올바른 도구를 즉시 집는 것이에요. 네 가지 대표 증상을 매핑해둡시다.

① 증상② 첫 도구③ 흔한 원인
CPU 100%스레드 덤프(top -H로 바쁜 TID → 16진 변환 → jstack에서 매칭)무한 루프·바쁜 스핀·과도한 GC·정규식 백트래킹. 가장 CPU 먹는 스레드의 스택을 본다
메모리 누수힙 덤프 + MAT(사용량이 계속 우상향하는지 추세 확인 후 덤프)제거 안 되는 컬렉션·캐시, ThreadLocal 미정리, 리스너 등록 후 해제 누락 → 지배자 트리로 범인 찾기
지연 급증트레이스 + GC 로그 + 풀 지표느린 다운스트림, 잦은 Full GC, 커넥션 풀/스레드 풀 고갈(대기 큐 포화)
데드락 / 행(hang)스레드 덤프(jstack이 "Found one Java-level deadlock"을 직접 알려줌)두 스레드가 락을 서로 반대 순서로 잡음(순환 대기). BLOCKED 스레드가 어떤 락을 기다리는지 본다
⚠️
"지연 급증 = 무조건 코드가 느림"이 아니에요. 5년차에 자주 놓치는 진짜 범인은 자원 고갈이에요. DB 커넥션 풀(HikariCP)이 고갈되면 스레드가 커넥션을 기다리며 대기해요 — CPU는 한가한데 응답만 느려지죠. 스레드 덤프에서 다수가 getConnection·풀 대기에 WAITING이면 풀 사이즈·다운스트림을 의심하세요. "느림"의 원인이 CPU인지, GC인지, 대기(풀)인지부터 갈라야 합니다.
🧭 원칙: 추측하지 말고 관측하라 장애 대응의 실수는 대부분 "아마 이거일 거야"로 코드를 고치는 것에서 나와요. 순서는 항상 ① 증상 확정(메트릭)② 범위 좁히기(트레이스)③ 근본 원인(덤프·로그)④ 가설 검증 후 조치예요. 그리고 조치 뒤엔 같은 증상을 알림으로 잡을 지표를 추가해 다음엔 더 빨리 알게 만드세요. 관찰성은 사후에도 계속 자라야 합니다.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
시니어 관점으로 점검
면접·장애대응에서 실제로 묻는 결의 문제예요. 틀려도 바로 해설이 나옵니다.
🧪
개념을 "안다"와 "설명할 수 있다"는 달라요. 네 유형(객관식 · O/X · 빈칸 · 코드결과)으로 점검해보세요.
심화 CH 16 📡 gRPC와 API 통신 — Protobuf·HTTP/2·스트리밍, REST vs gRPC vs GraphQL