로그와 메트릭의 차이: 운영 중인 서비스를 어떻게 관찰할 것인가
빠른 답
- 로그는 개별 사건의 맥락을 남기고, 메트릭은 시간에 따른 상태 변화를 숫자로 요약한다.
- 이상 징후 감지와 알림은 메트릭이 빠르고, 원인 분석은 로그가 더 깊은 단서를 준다.
System.out.println만으로는 로그 레벨, 포맷, 필터링, 수집 연동, 마스킹을 안정적으로 관리하기 어렵다.- 운영 품질은 로그와 메트릭을 따로 도입했는지가 아니라, 탐지부터 분석과 재발 방지까지 이어지는지로 판단하는 편이 좋다.
목차
한눈에 비교
시간 흐름으로 이해하기
왜 운영 품질의 기준선인가
운영 중인 서비스는 배포가 성공했다는 사실만으로 설명되지 않는다. 사용자가 실제로 요청을 보내고, 외부 API가 응답하고, 데이터베이스와 큐가 함께 움직이는 동안 어디에서 느려지고 어디에서 실패하는지 계속 확인할 수 있어야 한다.
이 관찰 가능성이 부족하면 장애는 “가끔 느려진다”, “특정 사용자가 실패한다”, “배포 뒤부터 이상하다” 같은 모호한 말로 남는다. 원인을 찾는 시간은 길어지고, 같은 문제가 다시 발생해도 재발 방지 기준을 세우기 어렵다.
로그와 메트릭은 기능 요구사항처럼 화면에 바로 드러나지는 않지만, 운영 품질의 기본 장치에 가깝다. 최소한 다음 질문에 답할 수 있어야 한다.
- 지금 서비스가 정상 범위 안에서 동작하는가?
- 실패율이 어느 시점부터 증가했는가?
- 특정 API, 외부 시스템, 배포 버전에 문제가 집중되는가?
- 사용자가 겪은 실패를 요청 단위로 추적할 수 있는가?
- 알림이 울렸을 때 원인 분석으로 이어질 증거가 남아 있는가?
로그와 메트릭이 헷갈리는 이유
둘 다 운영 중인 시스템을 보기 위한 데이터라서 처음에는 비슷하게 느껴진다. 예를 들어 ERROR 로그 개수는 숫자로 셀 수 있고, CPU 사용률이 높아진 순간도 하나의 사건처럼 보인다. 그래서 “에러 로그 개수를 세면 메트릭 아닌가”, “메트릭이 있으면 로그를 줄여도 되지 않나” 같은 질문이 생긴다.
차이는 데이터가 답하려는 질문에서 나온다. 로그는 한 번의 요청이 어떤 흐름을 거쳤고, 어떤 값과 외부 응답 때문에 실패했는지 설명한다. 반면 메트릭은 많은 이벤트를 숫자로 접어 “최근 5분 동안 결제 API의 5xx 비율이 3%에서 12%로 올랐다”처럼 상태 변화를 보여준다.
예를 들어 결제 API 장애를 보면 로그는 이런 단서를 남긴다.
2026-04-13T10:15:27.431+09:00 ERROR service=payment-api trace_id=7f3a9c order_id=ORD-1042 path=/payments provider=toss status=502 elapsed_ms=1840 message="payment provider request failed"
이 한 줄은 특정 주문의 결제 요청이 어떤 외부 결제사 호출 중 실패했고, 응답 코드와 소요 시간이 어땠는지 알려준다. 반면 메트릭은 같은 상황을 다음처럼 요약한다.
http_requests_total{service="payment-api",method="POST",path="/payments",status_class="5xx"} 42
http_request_duration_seconds_p95{service="payment-api",method="POST",path="/payments"} 1.82
메트릭은 개별 주문 번호를 알려주지 않는다. 대신 POST /payments에서 실패 수와 p95 지연 시간이 올라갔다는 운영 신호를 빠르게 보여준다.
스택 중립 기준선
도구를 고르기 전에 먼저 정해야 할 기준이 있다. 어떤 로그를 남길지, 어떤 메트릭을 수집할지, 얼마나 오래 보관할지, 어떤 조건에서 알림을 보낼지 정하지 않으면 도구를 붙여도 운영 품질은 크게 좋아지지 않는다.
로그의 최소 필드는 시간, 레벨, 서비스명, 환경, 요청 ID, API 경로, 상태 코드, 지연 시간, 예외명 정도에서 시작할 수 있다. 사용자 ID나 주문 ID처럼 원인 분석에 필요한 값은 남기되, 개인정보와 인증 토큰은 제외하거나 마스킹해야 한다.
메트릭은 요청 수, 오류 수, 응답 시간, 큐 적체량, 외부 호출 실패율, CPU와 메모리 같은 런타임 지표부터 보는 편이 안정적이다. 이때 user_id, order_id, email처럼 값의 종류가 계속 늘어나는 항목을 메트릭 라벨로 넣으면 시계열 수가 폭증한다. 이런 값은 로그에 두고, 메트릭 라벨은 service, method, path, status_class, env처럼 제한된 값으로 관리하는 편이 낫다.
구조화 로그는 수집기가 필드를 파싱하기 쉬운 형태로 남긴다. JSON 로그를 쓰는 경우도 많지만, 아래처럼 로그 포맷 자체를 키-값 형태로 맞추는 것만으로도 검색성이 좋아진다.
timestamp=2026-04-13T10:15:27.431+09:00 level=ERROR service=payment-api env=prod trace_id=7f3a9c method=POST path=/payments status=502 elapsed_ms=1840 message="payment provider request failed"
보관 정책도 품질 기준에 포함된다. 디버그 로그는 짧게 보관하고, 오류 로그와 감사성 이벤트는 더 길게 보관할 수 있다. 다만 오래 보관할수록 비용과 개인정보 관리 부담이 함께 커진다. 로그와 메트릭은 “많을수록 안전한 데이터”가 아니라 “운영 판단에 필요한 만큼 설계해야 하는 데이터”에 가깝다.
장애 대응에서 함께 쓰는 방식
장애 대응은 보통 메트릭에서 시작해 로그로 깊어진다. 메트릭 알림은 오류율 증가, 응답 시간 상승, 커넥션 풀 고갈, 큐 적체 같은 이상 징후를 빠르게 알려준다. 로그는 그다음 단계에서 어떤 요청이 실패했는지, 어떤 예외가 발생했는지, 외부 시스템 응답이 어땠는지를 확인하는 데 쓰인다.
간단한 확인 흐름은 다음과 같다.
- 대시보드에서 요청 수, 오류율, 지연 시간, 포화도 지표를 확인한다.
- 문제가 시작된 시간대와 영향을 받은 API를 좁힌다.
- 같은 시간대의 로그를
trace_id, API 경로, 상태 코드, 예외명으로 검색한다. - 배포, 설정 변경, 외부 API 장애, 데이터베이스 병목 같은 후보를 비교한다.
- 재발 방지를 위해 알림 기준, 로그 필드, 테스트, 스모크 검증을 보강한다.
알림은 다음처럼 출발점을 제공한다.
ALERT HighErrorRate
service=payment-api
condition=5xx_rate > 5% for 5m
current=11.8%
since=2026-04-13T10:12:00+09:00
이 출력만으로 원인을 확정할 수는 없다. 하지만 결제 API의 5xx 비율이 5분 동안 11.8%까지 올라갔다는 사실은 알 수 있다. 이후 같은 시간대의 로그를 보면 외부 결제사 502 응답, DB 타임아웃, 인증 토큰 만료, 배포 직후 설정 누락 같은 후보를 더 구체적으로 비교할 수 있다.
설정 예시
Spring Boot 같은 특정 프레임워크를 쓰면 Actuator와 Micrometer로 메트릭을 쉽게 노출할 수 있다. 다만 이 글의 초점은 프레임워크 튜토리얼이 아니라 운영 기준이다. 어떤 스택이든 상태 확인 엔드포인트, 메트릭 노출, 서비스 공통 라벨, 로그 상관관계가 잡혀 있어야 한다.
예를 들어 Spring Boot 환경에서는 다음처럼 상태와 Prometheus 형식 메트릭을 노출할 수 있다. Actuator와 Micrometer의 자세한 옵션은 Spring Boot Actuator 문서와 Micrometer 문서를 기준으로 확인하는 편이 좋다.
management:
endpoints:
web:
exposure:
include: health,info,prometheus
endpoint:
health:
probes:
enabled: true
metrics:
tags:
service: payment-api
env: prod
logging:
level:
root: INFO
com.example.payment: INFO
이 설정은 /actuator/health와 /actuator/prometheus를 통해 상태와 메트릭을 확인할 수 있게 한다. 운영에서는 여기에 HTTP 요청 수, JVM 메모리, 스레드 풀, 커넥션 풀, 외부 API 호출 시간 같은 지표를 연결한다.
로그 추적에는 요청 단위 식별자가 필요하다. Java 애플리케이션에서는 MDC를 사용해 같은 요청의 로그에 동일한 trace_id를 넣는 방식이 자주 쓰인다.
try {
String traceId = request.getHeader("X-Request-Id");
if (traceId == null || traceId.isBlank()) {
traceId = UUID.randomUUID().toString();
}
MDC.put("trace_id", traceId);
log.info("request started method={} path={}", request.getMethod(), request.getRequestURI());
chain.doFilter(request, response);
} finally {
MDC.clear();
}
MDC.clear()를 빠뜨리면 스레드 재사용 환경에서 이전 요청의 trace_id가 다음 요청 로그에 섞일 수 있다. 이런 문제는 장애 분석을 더 어렵게 만들기 때문에, 로그 품질 기준에 “요청 종료 시 MDC 정리” 같은 세부 규칙을 포함하는 것이 좋다.
운영 확인 예시
배포 후에는 설정이 들어갔다는 사실보다 실제로 관찰 가능한지가 중요하다. 메트릭 엔드포인트가 응답하고, 주요 API 요청 후 카운터가 증가하며, 로그에 요청 ID가 남는지 확인해야 한다.
아래는 로컬이나 스테이징에서 메트릭과 로그를 함께 확인하는 예시다.
$ curl -s http://localhost:8080/actuator/prometheus | grep 'http_server_requests_seconds_count'
http_server_requests_seconds_count{env="prod",method="GET",outcome="SUCCESS",status="200",uri="/orders"} 18342.0
http_server_requests_seconds_count{env="prod",method="POST",outcome="SERVER_ERROR",status="502",uri="/payments"} 42.0
$ grep 'trace_id=7f3a9c' app.log
2026-04-13T10:15:26.102+09:00 INFO service=payment-api trace_id=7f3a9c path=/payments message="request started"
2026-04-13T10:15:27.431+09:00 ERROR service=payment-api trace_id=7f3a9c provider=toss status=502 message="payment request failed"
2026-04-13T10:15:27.438+09:00 INFO service=payment-api trace_id=7f3a9c status=502 elapsed_ms=1840 message="request finished"
이 출력에서는 POST /payments의 서버 오류 카운터가 보이고, 같은 요청 ID를 가진 로그에서 외부 결제사 호출 실패까지 따라갈 수 있다. 메트릭은 조사 대상을 좁히고, 로그는 실패한 요청의 문맥을 복원한다.
실제로 막히는 지점
운영에서 자주 막히는 지점은 “많이 남기면 안전하다”는 생각이다. 로그를 너무 많이 남기면 저장 비용이 커지고, 중요한 로그가 묻히며, 검색 쿼리도 느려진다. 반대로 로그를 너무 줄이면 장애 당시의 요청 맥락이 사라진다. DEBUG 로그는 필요한 구간에 제한적으로 켜고, 운영 기본값은 INFO, WARN, ERROR 중심으로 관리하는 편이 유지하기 쉽다.
메트릭도 비슷하다. 라벨을 많이 붙이면 나중에 자세히 분석할 수 있을 것 같지만, 값의 종류가 제한되지 않은 라벨은 비용과 성능 문제를 만든다. 사용자 ID, 주문 ID, 이메일, 원문 요청 본문은 메트릭 라벨보다 로그 필드나 별도 감사 저장소에 두는 편이 낫다.
System.out.println은 로컬에서 값을 확인하는 정도에는 쓸 수 있지만, 운영 로그의 요구사항을 감당하기 어렵다. 로그 레벨별 필터링, 환경별 출력 제어, 구조화 포맷, 비동기 출력, 수집기 연동, 민감정보 마스킹, 샘플링 같은 기능이 필요해지기 때문이다. 운영 로그는 단순 출력이 아니라 저장, 검색, 보안, 비용을 함께 고려하는 데이터 파이프라인의 일부로 보는 편이 더 현실적이다.
테스트와 품질 게이트
관측성도 테스트 대상에 포함할 수 있다. 모든 로그 문구를 테스트로 고정할 필요는 없지만, 운영 판단에 필요한 최소 증거가 사라지지 않도록 품질 게이트를 두면 배포 후 조사 비용을 줄일 수 있다.
단위 테스트는 로그 마스킹 유틸리티, 메트릭 이름 생성 규칙, 오류 분류 함수처럼 작은 규칙을 검증하기에 좋다. 예를 들어 이메일, 토큰, 주민등록번호 같은 민감정보 패턴이 로그 필드에 남지 않는지 검사할 수 있다.
통합 테스트는 실제 HTTP 요청을 보냈을 때 trace_id가 로그에 포함되는지, 메트릭 카운터가 증가하는지 확인하는 데 맞다. 스모크 테스트는 배포 직후 /health와 /metrics 또는 /actuator/prometheus가 정상 응답하는지 확인하는 용도로 두기 좋다. 회귀 테스트는 과거 장애에서 빠졌던 로그 필드나 잘못된 알림 조건이 다시 사라지지 않도록 잡는 역할을 한다.
CI 품질 게이트에는 다음 항목을 넣을 수 있다.
- 민감정보 패턴이 샘플 로그에 포함되지 않는지 검사한다.
- 메트릭 엔드포인트가 정상 응답하고 주요 지표 이름이 유지되는지 확인한다.
- 주요 API 통합 테스트에서 요청 ID가 로그에 연결되는지 검증한다.
- 배포 산출물에 서비스명, 환경, 버전 라벨이 포함되는지 확인한다.
- 실패 시 테스트 리포트, 샘플 로그, 메트릭 응답, 배포 버전 정보를 함께 남긴다.
이런 증거가 남아 있으면 장애 분석은 추측보다 비교에 가까워진다. 어느 배포부터 오류율이 증가했는지, 어떤 API에 집중됐는지, 같은 요청의 로그가 어디까지 남았는지를 확인할 수 있기 때문이다.
운영 점검 순서
처음부터 완벽한 관측성 구성을 만들기는 어렵다. 작은 기준선을 먼저 만들고, 실제 장애나 리허설을 거치며 보강하는 방식이 더 유지하기 쉽다.
- 주요 사용자 흐름을 고른다. 로그인, 주문 생성, 결제, 정산처럼 실패 영향이 큰 흐름부터 시작한다.
- 각 흐름에서 성공, 실패, 지연을 관찰할 메트릭을 정한다.
- 실패 원인을 추적할 로그 필드를 정한다. 요청 ID, 외부 시스템명, 상태 코드, 예외명은 기본 후보가 된다.
- 알림 기준을 정한다. 단일 실패보다 일정 시간 지속된 오류율, p95 지연 시간, 큐 적체량처럼 행동으로 이어질 수 있는 기준을 둔다.
- 배포 후 스모크 테스트와 대시보드 확인을 자동화한다.
- 장애나 리허설 이후 부족했던 로그 필드, 메트릭, 알림 조건, 회귀 테스트를 보강한다.
도구 이름보다 중요한 것은 알림 이후의 다음 행동이다. Prometheus, Grafana, Grafana Loki, OpenTelemetry 같은 도구는 관찰 데이터를 다루는 데 도움을 주지만, 어떤 질문에 답할지 정하지 않으면 대시보드와 로그 저장소만 늘어난다. 로그와 메트릭은 운영자가 문제를 인지하고, 원인을 좁히고, 재발 방지 기준을 세울 때 가치가 생긴다.
원문 참고
https://www.maeil-mail.kr/question/66
댓글
댓글 쓰기