이번 포스팅에서는 Spring Boot Actuator로 관측성(observability)을 구축하는 과정을 정리하고자 한다. 서비스를 배포하고 나면 “지금 살아 있나”, “요청이 얼마나 들어오나”, “어디가 느린가” 같은 질문이 곧바로 따라온다. Actuator는 이 질문에 답할 수 있도록 애플리케이션의 상태와 메트릭을 HTTP 엔드포인트로 드러내 주는 Spring Boot 공식 모듈이다. 여기에 Micrometer로 커스텀 메트릭을 심고 Prometheus로 수집해 Grafana로 그리면, 로그를 뒤지지 않고도 서비스가 어떻게 돌아가는지 한눈에 본다. 이 글에서는 Spring Boot 4.1을 기준으로 의존성 추가부터 엔드포인트 노출과 보안, 쿠버네티스 프로브, 커스텀 메트릭, Prometheus 연동, 그리고 실제로 돌려 본 결과까지 실무 순서대로 짚는다.
관측성이 뭐고 왜 Actuator부터인가
Spring Boot Actuator는 애플리케이션의 상태(health), 메트릭(metrics), 환경 정보 등을 별도 코드 없이 HTTP·JMX 엔드포인트로 노출해 주는 모듈이다. 관측성 구축의 출발점이 Actuator인 이유는, 서비스 내부를 밖에서 들여다볼 수 있는 “창문”을 프레임워크가 이미 만들어 두었기 때문이다. 우리는 그 창문을 열고, 필요한 지표를 몇 개 더 다는 일만 하면 된다.
관측성은 흔히 세 기둥(메트릭·로그·트레이스)으로 설명하는데, Actuator는 그중 메트릭과 상태 점검을 담당한다. /actuator/health로 서비스 생사를, /actuator/metrics로 JVM 메모리·HTTP 요청 수·응답 시간 같은 수치를 확인한다. 이 지표들은 Micrometer라는 계측 라이브러리를 통해 수집되는데, Micrometer는 Prometheus·Datadog·InfluxDB 같은 백엔드에 독립적인 API를 제공해, 코드를 바꾸지 않고도 모니터링 백엔드를 갈아 끼울 수 있다.
이 글의 스택은 세 층으로 나뉜다. 애플리케이션 안에서 Micrometer가 지표를 측정하고, Actuator가 그 지표를 엔드포인트로 노출하며, Prometheus가 그 엔드포인트를 주기적으로 긁어(scrape) 저장한 뒤 Grafana가 그린다.
Spring Boot 4에서는 OpenTelemetry 지원이 기본으로 강화됐는데, 그 배경은 Spring Boot 4 신기능 정리에서 함께 볼 수 있다. 이 글의 코드는 Java 21과 Spring Boot 4.1 기준이다.
의존성 추가와 첫 엔드포인트 열기
Spring Boot Actuator를 시작하려면 spring-boot-starter-actuator 의존성 하나면 된다. 추가하는 순간 /actuator/health 엔드포인트가 자동으로 열리고, 애플리케이션이 정상이면 {"status":"UP"}를 반환한다. 다만 보안을 위해 기본적으로는 health만 노출되고, 나머지 엔드포인트는 명시적으로 열어야 한다.
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// Prometheus 형식으로 메트릭을 노출하는 레지스트리
implementation 'io.micrometer:micrometer-registry-prometheus'
// @Timed 애너테이션을 쓰려면 AspectJ가 필요하다 (뒤에서 설명)
implementation 'org.aspectj:aspectjweaver'
}Groovyspring-boot-starter-actuator가 Actuator 본체이고, micrometer-registry-prometheus를 추가하면 /actuator/prometheus 엔드포인트가 생겨 Prometheus가 읽을 수 있는 형식으로 메트릭을 내보낸다. 이 레지스트리가 클래스패스에 있으면 Spring Boot가 PrometheusMeterRegistry 빈을 자동으로 만든다.
# 애플리케이션 실행 후 기본 health 확인
curl localhost:8080/actuator/health
# {"status":"UP"}ShellScript의존성만 추가하면 위처럼 /actuator/health가 바로 응답한다. 이 한 줄이 로드밸런서나 쿠버네티스가 “이 인스턴스에 트래픽을 보내도 되나”를 판단하는 근거가 된다. 여기까지가 관측성의 최소 출발선이다.
엔드포인트 노출과 보안: 열 것만 골라서 연다
Spring Boot Actuator 엔드포인트는 management.endpoints.web.exposure.include로 노출 대상을 고른다. 기본값은 health 하나뿐이라, 메트릭이나 Prometheus 엔드포인트를 쓰려면 명시적으로 포함시켜야 한다. 이는 실수로 민감한 정보(환경 변수·빈 목록)를 외부에 노출하지 않게 하는 안전장치다. 노출 설정의 전체 옵션은 Actuator 엔드포인트 공식 문서에 정리돼 있다.
# application.yml
management:
endpoints:
web:
exposure:
include: "health,info,metrics,prometheus" # 열 엔드포인트만 명시
base-path: /actuator # 기본 경로 (변경 가능)
endpoint:
health:
show-details: always # health 상세 표시 (개발용)YAMLexposure.include에 필요한 엔드포인트만 나열하는 게 핵심이다. "*"로 전부 열 수도 있지만 운영에서는 권장하지 않는다. env, beans, heapdump 같은 엔드포인트는 내부 정보를 그대로 드러내므로, 정말 필요한 것만 열고 나머지는 닫아 둔다. show-details: always는 health의 세부 항목(DB·디스크 등)까지 보여주는데, 편리하지만 인증 없이 열어 두면 정보 노출이 되므로 운영에서는 when-authorized가 안전하다.
운영 환경이라면 Actuator 엔드포인트 자체를 Spring Security로 보호하는 게 정석이다. 아래처럼 Actuator 엔드포인트에만 인증을 걸 수 있다.
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception {
// 모든 Actuator 엔드포인트에 매칭
http.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
// health는 공개 (로드밸런서 프로브용), 나머지는 인증 필요
.requestMatchers(EndpointRequest.to("health")).permitAll()
.anyRequest().hasRole("ACTUATOR_ADMIN"))
.httpBasic(basic -> {});
return http.build();
}
}JavaEndpointRequest.toAnyEndpoint()는 모든 Actuator 엔드포인트를 대상으로 하는 매처다. 위 설정은 health만 공개하고 나머지(metrics, prometheus 등)는 ACTUATOR_ADMIN 역할을 요구한다. 로드밸런서는 health만 찔러 생사를 확인하면 되므로 이 조합이 실용적이다. Prometheus 서버가 /actuator/prometheus를 긁어야 한다면, 그쪽에는 별도 인증 토큰을 두거나 내부 네트워크에서만 접근하게 막는다.
health 엔드포인트와 쿠버네티스 프로브

Spring Boot Actuator의 /actuator/health는 단순한 UP/DOWN을 넘어, 쿠버네티스의 liveness·readiness 프로브로 바로 쓸 수 있다. management.endpoint.health.probes.enabled를 켜면 /actuator/health/liveness(재시작이 필요한가)와 /actuator/health/readiness(트래픽을 받을 준비가 됐나)가 자동으로 생긴다. 이 둘을 구분하는 게 무중단 배포의 핵심이다.
# application.yml — 쿠버네티스 프로브 활성화
management:
endpoint:
health:
probes:
enabled: true # liveness/readiness 그룹 생성
group:
readiness:
include: "db,redis" # readiness에 DB·Redis 상태 포함YAMLprobes.enabled: true를 주면 두 프로브 엔드포인트가 열린다. liveness는 “프로세스가 살아 있는가”만 보고, readiness는 “의존하는 DB·캐시가 준비됐는가”까지 본다. 위 설정은 readiness 그룹에 db와 redis 상태를 포함시켜, DB가 끊기면 그 인스턴스를 트래픽 대상에서 빼도록 한다. 쿠버네티스는 liveness가 실패하면 파드를 재시작하고, readiness가 실패하면 서비스에서 잠시 제외한다.
애플리케이션 고유의 상태를 health에 더하고 싶다면 HealthIndicator를 구현한다.
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class QueueHealthIndicator implements HealthIndicator {
@Override
public Health health() {
int backlog = checkQueueBacklog(); // 큐에 쌓인 작업 수 조회
if (backlog > 1000) {
return Health.down().withDetail("backlog", backlog).build();
}
return Health.up().withDetail("backlog", backlog).build();
}
private int checkQueueBacklog() {
return 42; // 실제로는 메시지 큐 등에서 조회
}
}JavaHealthIndicator를 구현한 빈을 등록하면 Spring이 자동으로 /actuator/health에 그 상태를 합쳐 준다. 위 예제는 큐 백로그가 1000을 넘으면 DOWN으로 보고해, 그 인스턴스가 과부하 상태임을 로드밸런서에 알린다. 빈 이름(queue)이 health 응답의 컴포넌트 키가 된다. 메시지 큐를 다룬다면 Spring Boot Kafka 연동 가이드와 함께 보면 큐 상태 점검의 맥락이 잡힌다.
메트릭의 시작: Micrometer와 /actuator/metrics
메트릭은 /actuator/metrics로 확인한다. Spring Boot Actuator는 Micrometer를 통해 JVM 메모리, GC, HTTP 요청 수와 응답 시간, 톰캣 스레드 같은 지표를 별도 설정 없이 자동으로 수집한다. /actuator/metrics로 지표 목록을, /actuator/metrics/{이름}으로 개별 값을 본다.
# 수집 중인 메트릭 목록
curl localhost:8080/actuator/metrics
# HTTP 요청 지표 상세 (횟수, 총 소요시간 등)
curl localhost:8080/actuator/metrics/http.server.requestsShellScript/actuator/metrics/http.server.requests는 들어온 HTTP 요청의 횟수와 총 처리 시간을 담고 있어, 별도 계측 없이도 트래픽과 지연을 파악할 수 있다. 이 자동 수집 메트릭만으로도 “요청이 급증했다”, “특정 시점부터 느려졌다” 같은 신호를 잡는다.
이 지표들은 모두 MeterRegistry라는 중앙 저장소에 모인다. Micrometer의 모든 미터(meter)는 이 레지스트리에서 만들어지고 보관되므로, 커스텀 메트릭을 만들 때도 이 MeterRegistry 빈을 주입받아 쓴다. Prometheus 레지스트리를 추가했다면 Spring Boot가 PrometheusMeterRegistry를 자동 구성해 이 역할을 맡긴다.
비즈니스 지표를 심는 커스텀 메트릭
기본 메트릭은 인프라 관점의 수치라, 비즈니스 관점의 지표는 직접 심어야 한다. Micrometer는 세 가지 핵심 미터를 제공한다. 늘어나기만 하는 값은 Counter, 실행 시간을 재는 것은 Timer, 현재 상태값을 읽는 것은 Gauge다. 아래는 주문 생성 수와 처리 시간을 계측하는 예제다.
먼저 각 미터의 성격을 표로 짚어 본다.
| 미터 | 용도 | 예시 |
|---|---|---|
| Counter | 단조 증가하는 누적 값 | 주문 생성 수, 에러 발생 수, 처리한 요청 수 |
| Timer | 실행 빈도와 소요 시간 | API 처리 시간, 외부 호출 지연 |
| Gauge | 특정 시점의 현재 값(오르내림) | 큐 크기, 활성 커넥션 수, 캐시 항목 수 |
| DistributionSummary | 값의 분포 | 요청 페이로드 크기, 배치 건수 |
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final Counter orderCounter;
private final MeterRegistry registry;
public OrderService(MeterRegistry registry) {
this.registry = registry;
// 주문 생성 수를 세는 Counter (태그로 채널 구분)
this.orderCounter = Counter.builder("orders.created")
.description("생성된 주문 수")
.tag("channel", "web")
.register(registry);
}
public void createOrder(String product) {
// ... 주문 처리 ...
orderCounter.increment(); // 주문 하나 생성될 때마다 +1
}
}JavaCounter.builder("orders.created")로 만든 카운터는 주문이 생성될 때마다 increment()로 1씩 올라간다. tag("channel", "web")처럼 태그를 붙이면 나중에 채널별로 쪼개 볼 수 있어, Prometheus에서 {channel="web"} 라벨로 조회된다. 이때 메트릭 이름은 Prometheus 규칙에 맞게 변환되는데, 실제로 어떤 이름으로 노출되는지는 뒤의 실행 캡처에서 확인한다. MeterRegistry를 생성자에서 주입받아 미터를 등록하는 게 표준 패턴이다.
실행 시간 측정은 @Timed 애너테이션이 가장 간편하다. 다만 이 애너테이션이 동작하려면 TimedAspect 빈을 직접 등록해야 한다는 점이 자주 놓치는 부분이다.
import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MetricsConfig {
// @Timed 애너테이션을 활성화하려면 이 빈이 반드시 필요하다
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}JavaTimedAspect 빈을 등록하면 임의의 메서드에 @Timed("order.process.time")을 붙여 실행 시간을 자동으로 계측할 수 있다. 두 가지를 함께 챙겨야 한다. 첫째, @Timed는 AspectJ 기반으로 동작하므로 org.aspectj:aspectjweaver 의존성이 클래스패스에 있어야 한다. 없으면 애플리케이션 기동 시 NoAspectBoundException으로 실패한다. 둘째, Spring Boot가 TimedAspect 빈을 자동으로 등록해 주지 않으므로 위처럼 직접 빈으로 올려야 한다. @Timed를 붙였는데 메트릭이 안 잡힌다면 이 둘 중 하나가 빠진 경우가 대부분이다. Gauge는 registry.gauge("queue.size", queue, Queue::size)처럼 현재 값을 읽어 오는 함수를 넘겨 등록한다.
Prometheus와 Grafana로 연결하기
수집한 메트릭을 저장하고 시각화하려면 Prometheus가 /actuator/prometheus를 주기적으로 긁어 가게 설정한다. micrometer-registry-prometheus 의존성과 prometheus 엔드포인트 노출까지 됐다면, 애플리케이션 쪽 준비는 끝났다. 남은 건 Prometheus 서버에 “이 주소를 몇 초마다 긁어라”라고 알려 주는 것뿐이다.
# prometheus.yml — Prometheus 스크레이프 설정
scrape_configs:
- job_name: "spring-boot-app"
metrics_path: "/actuator/prometheus" # Actuator가 노출하는 경로
scrape_interval: 15s # 15초마다 수집
static_configs:
- targets: ["localhost:8080"]YAML이 설정은 Prometheus가 15초마다 localhost:8080/actuator/prometheus를 긁어 메트릭을 저장하게 한다. metrics_path를 Actuator 경로로 지정하는 게 핵심이다. Prometheus를 로컬에서 빠르게 띄우려면 Docker로 이 설정 파일을 마운트해 실행하면 된다. 컨테이너 배포가 익숙지 않다면 Spring Boot Docker 배포 가이드를 참고하면 좋다.
Prometheus가 데이터를 모으기 시작하면 Grafana를 Prometheus에 연결해 대시보드를 그린다. Grafana에는 Micrometer/Spring Boot용 공개 대시보드(예: JVM Micrometer 대시보드)가 있어, 대시보드 ID만 넣으면 JVM 메모리·GC·HTTP 지표가 즉시 그려진다. 커스텀 메트릭(orders_created_total 등)은 Grafana 쿼리에서 직접 패널을 만들어 그린다. 여기까지 오면 “요청 수가 튀는 순간 응답 시간이 어떻게 변했나”를 로그 없이 그래프로 읽게 된다.
직접 돌려본 결과: health와 커스텀 메트릭 확인
앞의 설정과 코드를 실제 프로젝트로 만들어 돌려봤다. 캡처를 그대로 재현하는 데 쓴 소스는 다음 네 파일이다. application.yml로 엔드포인트를 노출하고, OrderService가 커스텀 Counter를, OrderController가 @Timed로 실행 시간을 계측하며, MetricsConfig가 그 @Timed를 활성화한다.
# application.yml — 엔드포인트 노출 + health 상세 + 쿠버네티스 프로브
management:
endpoints:
web:
exposure:
include: "health,info,metrics,prometheus"
endpoint:
health:
show-details: always
probes:
enabled: trueYAML이 설정이 /actuator/health·/actuator/metrics·/actuator/prometheus를 열고, probes.enabled로 liveness·readiness 그룹을 만든다. 캡처의 health 그룹과 prometheus 엔드포인트가 모두 이 설정에서 나온다.
// OrderService.java — 주문 생성 수를 세는 커스텀 Counter
@Service
public class OrderService {
private final Counter orderCounter;
public OrderService(MeterRegistry registry) {
this.orderCounter = Counter.builder("orders.created")
.description("생성된 주문 수")
.tag("channel", "web")
.register(registry);
}
public void createOrder(String product) {
orderCounter.increment();
}
}JavaOrderService는 orders.created Counter를 만들고 주문마다 increment()한다. 캡처의 orders_total{channel="web"} 10.0이 이 카운터의 값이다. 코드의 이름(orders.created)과 노출 이름(orders_total)이 다른 이유는 Micrometer의 Prometheus 명명 규칙 변환 때문이다.
// OrderController.java — POST /orders 엔드포인트, @Timed로 실행 시간 계측
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService service;
public OrderController(OrderService service) {
this.service = service;
}
@Timed(value = "order.process.time", description = "주문 처리 시간")
@PostMapping
public String create(@RequestParam String product) {
service.createOrder(product);
return "created: " + product;
}
}JavaOrderController는 앞의 curl이 호출한 POST /orders 엔드포인트다. @Timed가 붙어 있어 이 메서드 호출마다 실행 시간이 order_process_time_seconds로 기록된다. 캡처에서 order_process_time_seconds_count가 10인 것은 이 메서드가 열 번 호출됐다는 뜻이다.
// MetricsConfig.java — @Timed 활성화 (aspectjweaver 의존성 필요)
@Configuration
public class MetricsConfig {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}JavaMetricsConfig는 @Timed를 동작하게 하는 TimedAspect 빈을 등록한다. 이 빈과 org.aspectj:aspectjweaver 의존성이 둘 다 있어야 @Timed가 계측되며, 하나라도 빠지면 앱이 기동조차 되지 않는다. 이 네 파일과 의존성만 있으면 아래 명령으로 캡처를 그대로 재현할 수 있다.
# 1) 실행 (web + actuator + prometheus + aspectjweaver 의존성)
./mvnw spring-boot:run
# 2) 주문 10건 전송 (5종 상품을 2번씩 → 커스텀 Counter·@Timed 값 쌓기)
for i in 1 2; do
for p in keyboard mouse monitor webcam headset; do
curl -s -X POST "localhost:8080/orders?product=$p"
done
done
# 3) 상태와 메트릭 확인
curl -s localhost:8080/actuator/health
curl -s localhost:8080/actuator/prometheus | grep ordersShellScript위 순서로 앱을 띄우고 주문을 전송한 뒤 엔드포인트를 조회한다. @Timed가 동작하려면 앞서 강조한 aspectjweaver 의존성과 TimedAspect 빈이 둘 다 있어야 한다. 하나라도 빠지면 앱이 NoAspectBoundException으로 기동조차 되지 않는다.

/actuator/health가 전체 상태를 UP으로 보고하고, 그 아래 diskSpace·livenessState·readinessState·ping·ssl 컴포넌트가 각각 UP으로 나온다. probes.enabled를 켰기 때문에 liveness·readiness 그룹이 생겼고, /actuator/health/readiness로 준비 상태만 따로 조회할 수 있다. 로드밸런서나 쿠버네티스는 바로 이 응답을 근거로 트래픽을 보낼지 판단한다.

Prometheus 엔드포인트 출력에서 세 가지가 눈에 띈다. 첫째, 코드에서 orders.created로 만든 커스텀 Counter가 실제로는 orders_total{channel="web"} 10.0으로 노출된다. 이름에서 created가 사라졌는데, 이는 Micrometer가 카운터에 Prometheus 관례인 _total 접미사를 붙이는 과정에서, 이름의 .created(→ _created)가 하필 OpenMetrics의 예약 접미사(카운터 생성 시각을 나타내는 시계열)와 겹쳐 제거되기 때문이다. 그래서 orders + _total = orders_total이 된다. 실제로 이름을 orders.placed로 바꾸면 orders_placed_total로 정상 변환된다. 카운터 이름 끝에 .created나 _total·_count 같은 예약 접미사를 쓰지 않는 게 좋다는 걸 실행으로 확인해 두는 게 좋다. 둘째, @Timed가 계측한 order_process_time_seconds_count가 10으로, 주문 처리 메서드가 열 번 호출된 것과 정확히 맞는다. 셋째, 아무 계측도 안 했는데 http_server_requests_seconds_count에 POST /orders가 10회로 자동 기록돼 있다. 이 자동 수집 지표만으로도 트래픽과 지연을 파악할 수 있다.
FAQ
Actuator 엔드포인트가 /actuator/health 말고는 안 보여요.
기본 노출이 health 하나뿐이라 그렇다. Spring Boot는 보안을 위해 나머지 엔드포인트를 닫아 두므로, application.yml의 management.endpoints.web.exposure.include에 필요한 엔드포인트(metrics, prometheus 등)를 명시적으로 추가해야 한다. "*"로 전부 열 수도 있지만 env·beans 같은 민감한 엔드포인트가 함께 열리므로, 운영에서는 필요한 것만 나열하는 게 안전하다.
@Timed를 붙였는데 메트릭이 안 잡혀요.
두 가지를 확인한다. 첫째, org.aspectj:aspectjweaver 의존성이 있어야 한다. @Timed는 AspectJ 기반이라 이게 없으면 기동 시 NoAspectBoundException으로 실패한다. 둘째, TimedAspect 빈을 직접 등록해야 한다. @Bean TimedAspect timedAspect(MeterRegistry registry)를 설정 클래스에 추가한다. Spring Boot가 이 빈을 자동 등록하지 않기 때문이다. Counter나 Timer를 코드로 직접 만들어 쓸 때는 둘 다 필요 없다.
/actuator/prometheus 엔드포인트가 없어요.
micrometer-registry-prometheus 의존성이 없거나, prometheus를 노출 목록에 넣지 않아서다. 두 가지를 모두 확인한다. 의존성을 추가하면 Spring Boot가 PrometheusMeterRegistry를 자동 구성하고, exposure.include에 prometheus를 넣으면 /actuator/prometheus가 열린다. 둘 중 하나라도 빠지면 이 엔드포인트가 나타나지 않는다.
Actuator 엔드포인트를 외부에 그대로 열어도 되나요?
안 된다. metrics, env, heapdump 같은 엔드포인트는 내부 정보를 드러내므로 인증으로 보호해야 한다. Spring Security로 EndpointRequest.toAnyEndpoint()에 인증을 걸고, 로드밸런서 프로브용인 health만 공개하는 조합이 실용적이다. Prometheus 스크레이프는 내부 네트워크에서만 접근하게 막거나 별도 인증을 둔다.
liveness와 readiness 프로브는 뭐가 다른가요?
liveness는 “프로세스가 살아 있는가”, readiness는 “트래픽을 받을 준비가 됐는가”를 본다. management.endpoint.health.probes.enabled=true로 켜면 /actuator/health/liveness와 /actuator/health/readiness가 생긴다. 쿠버네티스는 liveness가 실패하면 파드를 재시작하고, readiness가 실패하면 서비스에서 잠시 제외한다. DB 연결 같은 의존성 점검은 readiness 그룹에 넣는 게 맞다.
/actuator/prometheus 값이 4.26957E-4처럼 지수 표기로 나오는데 정상인가요?
정상이다. Prometheus는 아주 작은 값(대략 0.001 미만)을 지수 표기로 직렬화하고, 큰 값은 10진수로 낸다. Prometheus 서버와 Grafana는 두 표기를 동일하게 파싱·표시하므로 조회나 그래프에는 아무 영향이 없다.
마치며
지금까지 Spring Boot Actuator로 관측성을 구축하는 과정을 정리해 보았다. 개인적으로 Actuator의 가치를 절감한 건 장애가 났을 때가 아니라 장애를 미리 피했을 때였다. 한번은 특정 API의 응답 시간이 서서히 우상향하는 걸 Grafana에서 발견하고, 커넥션 풀이 조금씩 새는 걸 배포 전에 잡은 적이 있다. 로그만 봤다면 에러가 없으니 몰랐을 문제였다. 메트릭은 “아직 터지지 않은 문제”를 숫자의 추세로 보여준다는 점에서 로그와 결이 다르다.
그래서 새 서비스를 만들 때는 기능 코드보다 Actuator와 커스텀 메트릭 몇 개를 먼저 심어 두길 권한다. orders.created 카운터 하나, 핵심 API에 @Timed 하나면 충분하다. 나중에 “그때 트래픽이 얼마였지?”라고 물었을 때 답할 수 있느냐 없느냐는, 그 지표를 미리 심어 뒀는지에 달려 있다. 관측성은 장애가 난 뒤에 급히 붙이는 것이 아니라, 처음부터 서비스의 일부로 두는 편이 언제나 이득이다.
