진행 기간: 2026.04–현재 OCR API는 처리 결과를 공통 응답 본문에 담고 HTTP 상태 코드는 대부분 200으로 반환했다. Spring Boot의 기본 HTTP 메트릭만 보면 성공과 실패가 모두 status="200"으로 집계돼 어떤 오류가 늘었는지 알 수 없었다. 2026년 4월에 응답 본문의 오류 코드를 Micrometer Counter로 기...
진행 기간: 2026.04–현재
OCR API는 처리 결과를 공통 응답 본문에 담고 HTTP 상태 코드는 대부분 200으로 반환했다. Spring Boot의 기본 HTTP 메트릭만 보면 성공과 실패가 모두 status="200"으로 집계돼 어떤 오류가 늘었는지 알 수 없었다.
2026년 4월에 응답 본문의 오류 코드를 Micrometer Counter로 기록하기 시작했다. 이후 Grafana 대시보드를 실제 운영에 사용하면서 오류율과 최근 발생 건수의 역할을 나누고, 시계열이 겹쳐 읽히거나 존재하지 않는 라벨에 의존하던 쿼리를 고쳤다.
응답 형태는 다음과 같았다.
HTTP/1.1 200 OK
Content-Type: application/json
{
"header": {
"isSuccessful": false,
"resultCode": 4001001,
"resultMessage": "요청을 처리하지 못했습니다."
}
}이 구조에서는 http_server_requests_seconds_count{status="200"}만으로 비즈니스 성공 여부를 가를 수 없다. HTTP 요청은 정상적으로 처리됐지만 OCR 결과는 실패일 수 있기 때문이다.
응답 규약을 바로 바꾸면 기존 클라이언트와의 호환성 문제가 생긴다. 공통 예외 처리기가 오류 코드를 확정하는 시점에 별도 Counter를 증가시키는 방법을 선택했다.
모든 예외가 모이는 @RestControllerAdvice에서 오류 Counter를 기록했다. 각 예외 처리 메서드에 계측 코드를 반복하지 않고 최종 ResultCode와 예외를 받는 공통 메서드로 모았다.
라벨은 네 개로 제한했다.
| 라벨 | 답하는 질문 |
|---|---|
code | 클라이언트가 받은 오류 코드는 무엇인가 |
name | 숫자 코드가 어떤 오류를 뜻하는가 |
category | 사용자 오류, 서버 오류와 시스템 오류 중 어디에 속하는가 |
exception | 같은 오류 코드가 어느 예외 경로에서 만들어졌는가 |
requestId, 사용자 식별자와 원본 URI는 라벨에 넣지 않았다. 요청마다 새로운 값이 생기면 시계열 수와 저장 비용이 함께 늘어난다. 개별 요청을 찾는 값은 로그에 두고, 메트릭에는 값의 종류가 제한된 라벨만 남겼다.
처음에는 code만 기록했다. 대시보드를 사용해 보니 숫자만으로 의미를 알 수 없어 오류 코드 표를 따로 열어야 했다. ResultCode의 이름을 name으로 함께 기록하고 범례를 {{code}} {{name}}으로 바꿨다.
code와 name은 일대일로 대응하므로 라벨 조합 수는 늘지 않았다.
Counter를 추가한 뒤에도 패널이 답해야 할 질문과 실제 쿼리가 맞지 않는 문제가 남았다.
| 시기 | 변경 | 이유 |
|---|---|---|
| 2026.04 | 오류 코드 Counter와 첫 Grafana 패널 추가 | HTTP 200 안의 실패를 운영 지표로 보기 위해 |
| 2026.05 | 오류 이름을 범례에 추가 | 숫자 코드를 따로 해석하는 시간을 줄이기 위해 |
| 2026.07 | 초당 오류율과 최근 1시간 건수를 분리 | 현재 발생 강도와 절대 건수를 한 패널로 설명할 수 없어서 |
| 2026.08 | 비누적 표시, topk, category 분류와 라벨 필터 정리 | 시리즈가 겹치고 일부 오류가 분류에서 빠지는 문제를 고치기 위해 |
현재 비즈니스 오류 행은 네 패널로 구성된다.
| 패널 | 질문 | 계산 방식 |
|---|---|---|
| 오류 코드별 초당 발생률 | 지금 어떤 오류가 빠르게 늘고 있는가 | 상위 10개 코드의 rate() |
| 최근 1시간 상위 오류 | 지난 1시간에 어떤 오류가 많았는가 | 상위 5개 코드의 increase(...[1h]) |
| 범주별 초당 발생률 | 사용자·서버·시스템 오류 비중은 어떻게 변하는가 | category별 rate() |
| 코드별 최근 1시간 발생량 | 코드별 1시간 발생량이 시간에 따라 어떻게 변하는가 | 상위 10개 코드의 increase(...[1h]) |
대시보드 전체에는 HTTP 서버와 클라이언트, JVM과 데이터베이스 연결 등 다른 운영 패널도 있다. 이 글과 화면은 그중 비즈니스 오류 행만 다룬다.
아래 쿼리는 구조만 보이도록 메트릭 이름과 환경 라벨을 일반화했다.
# 오류 코드별 초당 발생률
topk(10,
sum by (code, name) (
rate(api_business_error_total[$__rate_interval])
)
)
# 최근 1시간 상위 오류
topk(5,
sum by (code, name) (
increase(api_business_error_total[1h])
)
)
# 사용자·서버·시스템 오류별 초당 발생률
sum by (category) (
rate(api_business_error_total[$__rate_interval])
)rate()는 초당 발생 강도를 비교할 때 사용한다. Grafana의 $__rate_interval은 화면 해상도와 Prometheus 수집 간격에 맞춰 계산 구간을 정한다.
increase(...[1h])는 각 시점에서 직전 한 시간의 발생량을 계산한다. Counter 원본의 영구 누적값이 아니라 계속 이동하는 한 시간 구간이다. 오류가 멈추면 한 시간이 지난 뒤 값도 내려간다.
오류 코드별 선을 쌓으면 위쪽 선의 높이가 해당 코드의 값이 아니라 아래 선까지 더한 값이 된다. 코드별 발생률을 비교하려는 목적과 맞지 않아 누적 표시를 껐다.
오류 코드가 많아지면 색상이 반복되고 범례가 화면을 차지한다. 초당 발생률은 상위 10개, 최근 한 시간 건수는 상위 5개만 보여주고 범례를 최댓값 순서로 정렬했다.
초기에는 오류 코드를 정규식으로 나눠 4로 시작하면 사용자 오류, 5로 시작하면 서버 오류로 표시했다. 이 방식은 음수 코드와 0 계열 코드를 어느 범주에도 넣지 못했다.
애플리케이션이 기록하는 category 라벨을 기준으로 바꿔 4, 5, system을 모두 표시했다. 분류 기준을 Grafana 쿼리에서 다시 만들지 않고 애플리케이션의 단일 기준을 사용한다.
한동안 모든 쿼리에 namespace="$namespace" 조건이 있었다. 그러나 실제 시계열에는 namespace 라벨이 없었다. 변수 값이 비어 있을 때는 라벨이 없는 시계열도 우연히 일치해 패널이 정상처럼 보였다.
현재 수집 대상이 하나의 namespace로 고정되어 있어 필터의 구분 가치도 없었다. 쿼리에서 이 조건을 제거하고 환경은 cluster, 인스턴스는 pod 라벨로 선택하도록 정리했다.
![]()
Counter 동작은 Micrometer의 SimpleMeterRegistry를 주입한 단위 테스트로 확인했다. 예외 처리기를 호출한 뒤 예상한 code, name, category, exception 조합의 Counter가 증가하는지 검증했다.
대시보드는 다음 순서로 확인했다.
rate() 결과를 확인한다.increase()가 원본 Counter 증가량과 맞는지 비교한다.rate()와 increase()가 처리하는지 확인한다.2026년 9월에는 저장소의 대시보드 JSON과 실제 Grafana 화면이 같은 네 패널과 쿼리를 사용하는지도 다시 확인했다.
대시보드는 어떤 오류가 늘었는지 보여주지만 원인을 결정하지는 못한다. 개별 요청의 원인과 재시도 결과는 로그에서 확인한다.
오류 지표로 범위를 좁히고 로그를 같은 원인끼리 묶어 다음 개선 대상을 정하는 과정은 운영 지표와 주간 오류 분류 루프에 정리했다.
이 서비스에는 한 주에 약 20만 건의 요청이 들어오고, 그중 실패는 주마다 100건에서 1,000건 사이였다. 주간 오류 분류를 이어 가면서 현재는 실패 건수를 절반 수준까지 줄였고 개선은 계속 진행 중이다.
category 라벨로 바꾸고 존재하지 않는 namespace 필터를 제거했다.HTTP 상태 코드는 전송 결과이고 응답 본문의 성공 여부는 서비스 규약이다. 모든 응답을 200으로 감싸는 API에서는 둘을 같은 성공 지표로 볼 수 없다.
메트릭과 로그의 역할도 달랐다. 메트릭은 증가한 오류를 찾는 데 적합하고, 로그는 개별 요청의 원인과 최종 결과를 확인하는 데 필요하다.
대시보드 쿼리는 먼저 질문을 정해야 한다. rate()는 발생 강도를, 고정 구간 increase()는 최근 건수를 답한다. 패널 제목, 단위와 쿼리가 같은 질문을 설명해야 운영자가 값을 잘못 읽지 않는다.