Micrometer 5편. Spring Boot Actuator auto-configuration이 의존성 한 줄로 /actuator/prometheus를 열어주는 원리, JVM·HikariCP·Tomcat·HTTP 요청·Logback 등 공짜로 따라오는 자동 계측 메트릭 목록, @Timed와 TimedAspect 빈 등록 패턴, MeterRegistryCustomizer로 common tags를 모든 레지스트리에 주입하는 방법, Observation API가 metrics와 tracing을 한 번에 생성하는 구조까지.
이 글은 Micrometer 입문에서 운영까지 시리즈 5편이에요. 4편까지는 Micrometer 코어 — Counter·Timer·Gauge·DistributionSummary 타입, 태그 설계, 카디널리티 방어까지 직접 손으로 Meter 를 만드는 쪽을 다뤘어요. 5편은 방향이 조금 달라요. Spring Boot 가 자동으로 해주는 것을 먼저 알아보는 자리예요.
Spring Boot Actuator 를 처음 붙여보면 아무것도 안 한 것 같은데 Prometheus 에 수십 개 메트릭이 쭉 올라와 있어요. "내가 코드를 짠 게 없는데 이게 다 어디서 나온 거지?" 하는 의문이 생기죠. 그 의문에 답하는 게 이번 글의 핵심이에요. 자동 계측이 뭘 해주는지, 그 위에 직접 계측(@Timed, MeterRegistryCustomizer)을 어떻게 얹는지, 그리고 metrics 와 tracing 을 한 번에 생성하는 Observation API 가 어떤 구조인지 순서대로 풀어볼게요.
이 시리즈는 Micrometer 공식 문서, Spring Boot Actuator 공식 가이드, 여러 observability 학습 자료 등 공개 자료를 참고해 한국어 학습 노트로 풀어쓴 자료입니다.
Spring Boot 3.x + micrometer-registry-prometheus 의존성 환경을 기준으로 작성했어요. Spring Boot 2.x 에서는 일부 API 이름과 동작이 다를 수 있어요.
이번 글의 범위
이번 글은 Spring Boot Actuator 와 Micrometer 의 연동 계층 전체를 다뤄요.
| 자리 | 내용 |
|---|---|
| auto-configuration | 의존성 추가 → /actuator/prometheus 자동 노출 원리 |
| 자동 계측 메트릭 | JVM·HikariCP·Tomcat·HTTP·RestTemplate·Logback·Kafka 등 |
| 엔드포인트 비교 | /actuator/metrics (탐색용) vs /actuator/prometheus (scrape용) |
| @Timed · @Counted | 애너테이션 계측, TimedAspect 빈 등록 필요 이유 |
| MeterRegistryCustomizer | common tags 와 필터를 모든 레지스트리에 일괄 주입 |
| management.metrics.* 설정 | 분포·SLO·태그 활성/비활성 properties |
| Observation API | metrics + tracing 한 번에, @Observed, Spring Cloud Sleuth 흡수 |
auto-configuration — 의존성만 넣으면
Spring Boot 의 Actuator 통합이 강력한 이유는 auto-configuration 덕분이에요. spring-boot-starter-actuator 와 micrometer-registry-prometheus 두 의존성만 추가하면, Spring Boot 가 클래스패스를 보고 알아서 PrometheusMeterRegistry 빈을 만들고 /actuator/prometheus 엔드포인트를 연결해요. 코드 한 줄 없이요.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-actuator'
runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
의존성만 넣는다고 끝이 아니에요. /actuator/prometheus 엔드포인트를 외부에 노출하려면 설정을 명시해야 해요.
# application.yml
management:
endpoints:
web:
exposure:
include: health, info, prometheus, metrics
기본값으로 /actuator/health 만 노출돼 있어요. prometheus 를 include 에 추가하지 않으면 Prometheus 가 긁어갈 URL 이 없어요. 이 설정을 빠뜨리는 게 가장 흔한 초기 설정 실수예요.
auto-configuration 의 동작 원리를 한 줄로 정리하면 이래요.
클래스패스에 micrometer-registry-prometheus 존재
→ PrometheusMetricsExportAutoConfiguration 활성화
→ PrometheusMeterRegistry 빈 생성
→ PrometheusScrapeEndpoint (/actuator/prometheus) 등록
→ Spring Boot 자동 MeterBinder 들이 registry 에 자동 계측 Meter 등록
이 흐름을 이해하면, "왜 의존성 하나가 이렇게 많은 메트릭을 만들어주냐"는 의문이 풀려요. 자동 계측은 Spring Boot 가 내부에 준비해둔 MeterBinder 구현체들이 Actuator 초기화 시점에 한꺼번에 동작하는 거예요.
Spring Boot Actuator 공식 문서에서 전체 auto-configuration 옵션을 확인할 수 있어요.
공짜로 따라오는 메트릭
auto-configuration 이 활성화되면 코드를 건드리지 않아도 다음 메트릭들이 자동으로 Prometheus 에 올라와요. 각 그룹별로 어떤 이름으로 오고 어떤 태그가 붙는지 알아두면, Grafana 대시보드 만들 때 찾는 시간을 크게 줄일 수 있어요.
JVM 메트릭
JVM 의 상태를 나타내는 메트릭이 가장 먼저 눈에 들어와요.
jvm.memory.used — 메모리 사용량 (area: heap/nonheap, id: Eden Space 등)
jvm.memory.max — 메모리 최대값
jvm.memory.committed — OS 에 커밋된 메모리
jvm.gc.pause — GC 발생 시 pause 시간 (Timer, cause·action 태그)
jvm.gc.memory.promoted — GC 후 old gen 으로 승격된 바이트
jvm.gc.memory.allocated — Eden 에 새로 할당된 바이트
jvm.threads.live — 현재 살아있는 스레드 수 (Gauge)
jvm.threads.daemon — 데몬 스레드 수
jvm.threads.peak — 최고 스레드 수
jvm.classes.loaded — 로드된 클래스 수
jvm.classes.unloaded — 언로드된 클래스 수
system.cpu.usage — 시스템 전체 CPU 사용률
process.cpu.usage — JVM 프로세스의 CPU 사용률
process.uptime — 앱 기동 후 경과 시간
운영에서 가장 자주 보는 건 jvm.memory.used 와 jvm.gc.pause 예요. 힙 메모리가 계속 오르다가 GC 후 떨어지지 않는 패턴이 보이면 메모리 릭을 의심해볼 수 있어요.
HikariCP 메트릭
DB 커넥션 풀 상태를 실시간으로 볼 수 있어요. HikariCP 가 Micrometer 를 직접 지원하기 때문에 자동으로 따라와요.
hikaricp.connections.active — 현재 사용 중인 커넥션 수 (Gauge)
hikaricp.connections.idle — 유휴 커넥션 수
hikaricp.connections.pending — 대기 중인 요청 수 (이게 높으면 풀이 부족한 것)
hikaricp.connections.acquire — 커넥션 획득 대기 시간 (Timer)
hikaricp.connections.timeout — 타임아웃 발생 횟수 (Counter)
hikaricp.connections.pending 이 오래 높게 유지되거나 hikaricp.connections.timeout 이 올라가면 커넥션 풀 크기(maximum-pool-size)를 늘려야 한다는 신호예요.
Tomcat 메트릭
내장 Tomcat 환경이라면 tomcat.* 메트릭이 자동으로 붙어요.
tomcat.threads.busy — 바쁜 스레드 수 (max 에 가까우면 병목)
tomcat.threads.config.max — 설정된 최대 스레드 수
tomcat.global.request — 전체 요청 통계 (Timer)
tomcat.global.error — 에러 수 (Counter)
HTTP server/client 메트릭
HTTP 계층은 가장 중요한 자동 계측이에요. http.server.requests 는 Spring MVC 또는 WebFlux 에서 처리된 모든 요청에 자동으로 기록돼요.
http.server.requests
— Timer (duration + count 동시)
— 태그: uri · method · status · outcome(SUCCESS/CLIENT_ERROR/SERVER_ERROR)
— 예: http.server.requests{uri="/api/orders",method="POST",status="201",outcome="SUCCESS"}
이 메트릭 하나로 요청 수, 응답 시간 분포, 에러율을 모두 계산할 수 있어요. Grafana 에서 HTTP 대시보드의 80%가 이 메트릭 하나를 기반으로 만들어진다고 봐도 과장이 아니에요.
HTTP 클라이언트 쪽도 자동 계측이 돼요.
http.client.requests
— RestTemplate 또는 WebClient 를 통한 외부 HTTP 호출
— 태그: uri · method · status · clientName(호스트)
— Spring Boot 가 제공하는 instrumented bean 을 써야 동작
RestTemplate 의 경우, 직접 new RestTemplate() 로 생성하면 자동 계측이 안 돼요. RestTemplateBuilder 를 주입받아서 빌드해야 Micrometer 가 intercept 를 걸어줘요.
// ✅ 자동 계측 됨
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
// ❌ 자동 계측 안 됨
RestTemplate restTemplate = new RestTemplate();
기타 자동 계측 메트릭
logback.events
— 태그: level(error/warn/info/debug/trace)
— 로그 레벨별 발생 횟수 (Counter)
— error 로그가 급증하면 알람 → Grafana 에서 바로 잡기 좋음
cache.*
— Spring Cache 추상화를 쓰는 캐시 구현체에 자동 연결
— cache.gets (태그: result=hit/miss), cache.puts, cache.evictions 등
spring.kafka.*
— Kafka Consumer · Producer 메트릭
— spring.kafka.listener.* (컨슈머 처리 시간, 오프셋 lag 등)
/actuator/metrics vs /actuator/prometheus
Actuator 가 노출하는 두 메트릭 엔드포인트는 역할이 완전히 달라요.
/actuator/metrics — JSON 탐색용
GET /actuator/metrics
→ 등록된 메트릭 이름 목록 전체 JSON 반환
GET /actuator/metrics/http.server.requests
→ 특정 메트릭의 상세 정보 (값 + 사용 가능한 태그 목록)
GET /actuator/metrics/http.server.requests?tag=uri:/api/orders&tag=method:POST
→ 특정 태그 조합으로 필터링한 값
이 엔드포인트는 개발 중 탐색용이에요. 어떤 메트릭이 있는지, 태그 값에 뭐가 들어오는지 브라우저나 curl 로 빠르게 확인하기 좋아요. 성능 측면에서는 Prometheus scrape 에 쓰면 안 돼요. Prometheus 형식이 아니고, scrape 주기마다 JSON 파싱 오버헤드도 있어요.
/actuator/prometheus — scrape용
GET /actuator/prometheus
→ Prometheus text format 으로 모든 메트릭 덤프
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{method="POST",outcome="SUCCESS",status="201",uri="/api/orders"} 4231
http_server_requests_seconds_sum{method="POST",outcome="SUCCESS",status="201",uri="/api/orders"} 127.3
Prometheus 가 주기적으로 긁어가는 엔드포인트예요. 여기서 나오는 데이터 포맷은 Prometheus 표준 text format 이라서 Prometheus 가 바로 파싱할 수 있어요. 이 URL 을 prometheus.yml 에 scrape target 으로 등록하면 되는 거예요.
두 엔드포인트를 헷갈리지 않게 정리하면 이래요.
/actuator/metrics → 사람이 브라우저로 확인할 때 (JSON)
/actuator/prometheus → Prometheus 가 자동으로 긁어갈 때 (text format)
@Timed · @Counted — 애너테이션 계측
http.server.requests 는 컨트롤러 단위로 자동 계측이 돼요. 그런데 서비스 레이어의 특정 메서드 — 예를 들어 주문 처리 로직이나 외부 API 호출 — 의 latency 를 따로 잡고 싶을 때는 직접 Timer 를 만들어야 해요. 이때 가장 간단한 방법이 @Timed 어노테이션이에요.
@Timed 기본 사용
import io.micrometer.core.annotation.Timed;
@Service
public class OrderService {
@Timed(value = "orders.process.time",
description = "Order processing latency",
percentiles = {0.5, 0.95, 0.99})
public Order processOrder(CreateOrderRequest request) {
// 비즈니스 로직
return doProcess(request);
}
}
@Timed 를 붙이면 이 메서드가 실행될 때마다 Timer 에 실행 시간이 기록돼요. percentiles 를 설정하면 p50, p95, p99 도 자동으로 계산돼요.
TimedAspect 빈 등록이 필수
여기서 가장 자주 빠뜨리는 부분이 있어요. @Timed 는 혼자 동작하지 않아요. TimedAspect 빈을 직접 등록해야 AOP 가 연결돼요.
import io.micrometer.core.aop.TimedAspect;
@Configuration
public class MetricsConfig {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
이 빈 등록 없이 @Timed 만 붙이면 아무 데이터도 쌓이지 않아요. 컴파일 에러가 나지 않기 때문에 놓치기 쉬워요. http.server.requests 는 이 빈과 무관하게 항상 자동으로 동작하는 것과 헷갈리기 쉬운데, 둘은 완전히 별개예요.
@Counted 와 CountedAspect
@Counted 는 Counter 를 애너테이션으로 붙이는 방식이에요. 역시 CountedAspect 빈을 등록해야 해요.
import io.micrometer.core.annotation.Counted;
import io.micrometer.core.aop.CountedAspect;
@Configuration
public class MetricsConfig {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
}
@Counted(value = "orders.cancel.count", description = "Order cancel attempts")
public void cancelOrder(Long orderId) {
// 취소 로직
}
@Timed 가 latency 측정이라면 @Counted 는 단순 호출 횟수 카운팅이에요. 둘 다 AOP 빈 없이는 동작하지 않으니 MetricsConfig 클래스 하나에 모아두는 게 편해요.
extraTags 속성으로 메서드별 추가 태그도 붙일 수 있어요. {"team", "checkout", "priority", "high"} 처럼 key-value 쌍 문자열 배열을 넘기면 돼요.
MeterRegistryCustomizer · common tags 주입
여러 개의 Meter 에 같은 태그 세트를 붙이는 방법으로 MeterRegistryCustomizer 빈이 가장 깔끔해요. 4편에서 common tags 의 개념을 다뤘는데, Spring Boot 환경에서 어떻게 구체적으로 구성하는지 여기서 정리해요.
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
@Bean
MeterRegistryCustomizer<MeterRegistry> commonTagsCustomizer(
@Value("${spring.application.name}") String appName,
@Value("${spring.profiles.active:local}") String env) {
return registry -> registry.config()
.commonTags(
"application", appName,
"env", env,
"region", System.getenv().getOrDefault("AWS_REGION", "local")
);
}
MeterRegistryCustomizer<MeterRegistry> 의 타입 파라미터에 MeterRegistry 를 쓰면 클래스패스에 등록된 모든 레지스트리에 동시에 적용돼요. 특정 레지스트리에만 적용하고 싶으면 MeterRegistryCustomizer<PrometheusMeterRegistry> 처럼 구체 타입을 쓰면 돼요.
여러 Customizer 빈을 등록할 때는 @Order 어노테이션으로 적용 순서를 명시할 수 있어요.
@Bean
@Order(1)
MeterRegistryCustomizer<MeterRegistry> commonTagsCustomizer(...) { ... }
@Bean
@Order(2)
MeterRegistryCustomizer<MeterRegistry> meterFilterCustomizer(...) { ... }
YAML 설정으로도 common tags 를 추가할 수 있어요.
management:
metrics:
tags:
application: ${spring.application.name}
environment: ${ENVIRONMENT:local}
region: ${AWS_REGION:local}
이 YAML 설정은 내부적으로 PropertiesBasedCustomizer 를 통해 동작하고, MeterRegistryCustomizer 빈과 같은 효과예요. 팀에서 방식을 통일하는 게 중요해요. 섞어 쓰면 나중에 어디서 태그가 붙는지 추적하기 어려워요.
management.metrics.* 설정
management.metrics.* properties 는 auto-configured 메트릭의 동작을 세밀하게 제어할 수 있어요. 가장 자주 쓰는 설정을 정리해볼게요.
분포 · SLO · 비활성화 설정
management:
metrics:
distribution:
# histogram — Prometheus histogram_quantile() 에 필요
percentiles-histogram:
http.server.requests: true
# 클라이언트 사이드 percentile
percentiles:
http.server.requests: 0.5, 0.95, 0.99
# SLO 버킷 경계
slo:
http.server.requests: 50ms, 100ms, 200ms, 500ms
# 불필요한 메트릭 비활성화
enable:
tomcat: false
logback: false
spring.kafka: false
percentiles-histogram: true 를 켜면 Prometheus 에서 histogram_quantile(0.99, ...) 쿼리로 정확한 p99 를 계산할 수 있어요. 단, 버킷 수만큼 시계열이 늘어나니 실제로 쓰는 메트릭에만 켜는 게 좋아요. enable.<metric>=false 로 팀에서 쓰지 않는 auto-configured 메트릭을 차단해서 Prometheus 메모리를 아낄 수 있어요.
Observation API — metrics+traces 한 번에
Micrometer 1.10+ (Spring Boot 3.x 기본 포함) 에서 나온 Observation API 는 계측 방식을 한 단계 올려요. 하나의 Observation 이 Timer(metrics) 와 Span(tracing) 을 동시에 만들어줘요.
왜 Observation API 인가
@Timed 나 수동 Timer 로는 metrics 만 잡아요. tracing 을 함께 잡으려면 별도로 Span 을 생성해야 했어요. Observation API 는 두 가지를 한 번에 처리해요.
Observation 생성 + 실행
→ ObservationHandler(MicrometerObservationHandler)가 Timer 생성 (metrics)
→ ObservationHandler(TracingAwareMicrometerObservationHandler)가 Span 생성 (tracing)
Spring Cloud Sleuth 가 Spring Boot 3.x 에서 공식 지원 종료되고, 그 tracing 역할을 Micrometer Tracing 이 흡수했어요. Observation API 가 그 연결 다리예요.
ObservationRegistry 와 observe()
@Service
@RequiredArgsConstructor
public class OrderService {
private final ObservationRegistry observationRegistry;
public Order processOrder(CreateOrderRequest request) {
return Observation.createNotStarted("orders.process", observationRegistry)
.lowCardinalityKeyValue("region", request.getRegion()) // metrics 태그 + trace attribute
.lowCardinalityKeyValue("type", request.getType())
.highCardinalityKeyValue("orderId", request.getOrderId().toString()) // trace attribute only
.observe(() -> doProcess(request)); // start→실행→stop 자동 처리
}
}
lowCardinalityKeyValue 는 유한 집합 값일 때 써요. Prometheus 메트릭 태그와 trace span attribute 모두로 기록돼요. highCardinalityKeyValue 는 동적 값(orderId 등)에 쓰는데, trace attribute 에만 들어가고 메트릭 태그에는 포함되지 않아요. cardinality 폭발 없이 풍부한 tracing 데이터를 남기는 Observation API 의 핵심 설계예요.
직접 시작·종료를 제어할 때는 observation.start() → openScope() → observation.stop() 패턴을 써요. openScope() 는 ThreadLocal 에 현재 Observation 을 저장해서 MDC 로그 trace id 주입과 하위 Span 연결을 자동으로 처리해줘요.
@Observed 애너테이션
AOP 방식으로 더 간단하게 쓸 수 있어요.
import io.micrometer.observation.annotation.Observed;
@Service
public class OrderService {
@Observed(name = "orders.process",
contextualName = "Processing order",
lowCardinalityKeyValues = {"team", "checkout"})
public Order processOrder(CreateOrderRequest request) {
return doProcess(request);
}
}
@Observed 를 사용하려면 ObservedAspect 빈을 등록해야 해요.
@Bean
public ObservedAspect observedAspect(ObservationRegistry observationRegistry) {
return new ObservedAspect(observationRegistry);
}
@Timed + TimedAspect 와 구조가 비슷한데, @Observed 는 metrics + tracing 동시에 잡는다는 점이 달라요. Spring Boot 3.x 에서 새로 시작하는 프로젝트라면 @Timed 보다 @Observed 를 쓰는 게 더 나은 선택이에요.
tracing 백엔드 연결
Observation API 로 tracing 을 실제로 내보내려면 tracing 의존성을 추가해야 해요.
<!-- Brave (Zipkin 계열) -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!-- 또는 OpenTelemetry -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
이 bridge 의존성이 없으면 Observation.createNotStarted(...) 로 Observation 을 만들어도 tracing 데이터는 아무 데도 가지 않아요. metrics(Timer) 는 만들어지지만 Span 은 생성되지 않아요.
metrics만: spring-boot-starter-actuator + micrometer-registry-prometheus
metrics + tracing: 위 두 개 + micrometer-tracing-bridge-brave (또는 otel) + zipkin-reporter-brave
bridge 없이 @Observed 만 붙이면 metrics 는 잡히지만 trace 는 아무 데도 안 가요.
함정 정리
사고 1: management.endpoints.web.exposure.include 미설정
micrometer-registry-prometheus 를 넣었는데 /actuator/prometheus 에 접근하면 404 가 떠요.
원인 — 기본값으로 health 만 노출돼요. prometheus 를 명시적으로 include 하지 않으면 엔드포인트가 비활성 상태예요.
해결 — management.endpoints.web.exposure.include=health,prometheus,metrics 를 추가해요. 운영에서는 Prometheus 가 scrape 하는 네트워크만 /actuator/prometheus 에 접근 가능하도록 방화벽 또는 Spring Security 로 보호해야 해요.
사고 2: @Timed 를 붙였는데 메트릭이 안 잡힘
@Timed(value = "my.service.time") 을 메서드에 붙였는데 /actuator/prometheus 에 my_service_time 이 없어요.
원인 — TimedAspect 빈이 등록되지 않아서 AOP 가 동작하지 않았어요.
해결 — @Bean public TimedAspect timedAspect(MeterRegistry registry) { return new TimedAspect(registry); } 를 Configuration 클래스에 추가해요. http.server.requests 는 이 빈 없이도 항상 동작하기 때문에, "HTTP 메트릭은 잡히는데 서비스 메서드 메트릭은 없다"는 증상이 나타나면 TimedAspect 를 먼저 의심해요.
사고 3: new RestTemplate() 를 써서 http.client.requests 가 없음
외부 API 호출이 있는데 http.client.requests 메트릭이 아예 안 보여요.
원인 — RestTemplate 을 직접 new 로 생성하면 Micrometer 인터셉터가 걸리지 않아요.
해결 — RestTemplateBuilder 를 주입받아서 빌드해야 해요. WebClient 도 마찬가지로 WebClient.Builder 빈을 통해 생성해야 자동 계측이 돼요.
사고 4: percentiles-histogram 을 모든 메트릭에 켬
모든 Timer 에 percentiles-histogram: true 를 설정해요.
원인 — "p99 를 다 보고 싶다"는 의도인데, histogram 은 버킷 수만큼 Prometheus 시계열을 만들어요.
해결 — histogram 은 실제로 histogram_quantile() 쿼리를 쓰는 메트릭에만 켜야 해요. 모든 메트릭에 켜면 Prometheus 메모리 사용량이 크게 올라요. HTTP 서버 요청과 주요 서비스 메서드 정도에만 켜는 게 일반적이에요.
사고 5: @Observed 에 highCardinalityKeyValue 를 metrics 태그로 기대
observation.highCardinalityKeyValue("orderId", orderId.toString()) 를 추가했는데 Prometheus 에 orderId 태그가 안 보여요.
원인 — highCardinalityKeyValue 는 trace span attribute 로만 기록되고 metrics 태그에는 포함되지 않아요. 이게 의도된 설계예요.
해결 — Prometheus 메트릭 태그에 포함하려면 lowCardinalityKeyValue 를 써야 해요. 단, orderId 처럼 동적 ID 는 cardinality 폭발 위험이 있으니 집약된 값으로 대체해야 해요. 개별 주문 추적은 trace(highCardinalityKeyValue) 에서 하면 돼요.
사고 6: tracing bridge 의존성 없이 @Observed 로 tracing 을 기대
@Observed 를 달고 Zipkin 에 trace 가 안 보여요. 메트릭은 잡히는데요.
원인 — micrometer-tracing-bridge-brave 또는 micrometer-tracing-bridge-otel 과 실제 reporter 의존성이 없어요. @Observed 는 ObservationRegistry 에 등록된 ObservationHandler 에게 이벤트를 넘기는데, bridge 가 없으면 tracing handler 가 아무것도 안 해요.
해결 — tracing 백엔드 의존성을 세트로 추가해요 — bridge + reporter. Zipkin 이면 zipkin-reporter-brave, Jaeger OTLP 면 opentelemetry-exporter-otlp.
사고 7: ObservedAspect 빈 없이 @Observed 사용
@Observed 를 달았는데 아무 메트릭도 안 잡혀요.
원인 — @Timed 에 TimedAspect 가 필요하듯, @Observed 에는 ObservedAspect 가 필요해요.
해결 — @Bean public ObservedAspect observedAspect(ObservationRegistry r) { return new ObservedAspect(r); } 를 추가해요.
사고 8: /actuator/metrics 를 Prometheus scrape target 으로 오설정
Prometheus scrape target 을 /actuator/prometheus 대신 /actuator/metrics 로 잡아요.
원인 — 두 엔드포인트가 모두 메트릭 관련이라 혼동해요.
해결 — /actuator/metrics 는 JSON 형식이라 Prometheus 가 파싱할 수 없어요. metrics_path: /actuator/prometheus 를 명시해야 해요.
사고 9: Actuator 전체를 운영에 무보호 노출
management.endpoints.web.exposure.include=* 를 운영에도 그대로 놔요.
원인 — /actuator/env, /actuator/heapdump 같은 엔드포인트는 보안 민감 정보를 외부에 노출해요.
해결 — 운영에서는 prometheus, health, info 만 노출하고, management.server.port 를 앱 포트와 분리해서 내부 네트워크만 접근 가능하게 해요.
사고 10: MeterRegistryCustomizer 타입 파라미터 불일치
MeterRegistryCustomizer<PrometheusMeterRegistry> 로 선언했는데 Datadog registry 에도 같은 태그가 붙길 기대해요.
원인 — 구체 타입이면 그 타입의 레지스트리에만 적용돼요.
해결 — 공통 설정은 MeterRegistryCustomizer<MeterRegistry>, 레지스트리별 설정은 구체 타입으로 명확히 구분해요.
운영 권장 패턴
Pattern 1: Prometheus scrape 기본 설정 세트
# application.yml — 운영 기본 설정
management:
server:
port: 8081 # 메트릭 포트를 앱 포트(8080)와 분리
endpoints:
web:
exposure:
include: health, prometheus, metrics
metrics:
tags:
application: ${spring.application.name}
env: ${SPRING_PROFILES_ACTIVE:local}
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 50ms, 100ms, 200ms, 500ms
management.server.port 를 분리하면 Prometheus scrape 트래픽이 앱 HTTP 포트 메트릭에 섞이지 않아요. Kubernetes 에서 Prometheus ServiceMonitor 도 이 포트를 보도록 설정해요.
Pattern 2: Aspect 빈 모음 Config 클래스
@Configuration
@ConditionalOnClass(TimedAspect.class)
public class MetricsAspectConfig {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
@Bean
@ConditionalOnClass(ObservedAspect.class)
public ObservedAspect observedAspect(ObservationRegistry registry) {
return new ObservedAspect(registry);
}
}
한 클래스에 모아두면 "어느 Aspect 가 등록됐지?" 를 찾는 시간을 줄여요. @ConditionalOnClass 로 클래스패스에 없는 경우를 방어해요.
Pattern 3: MeterRegistryCustomizer 로 불필요 메트릭 차단
@Bean
MeterRegistryCustomizer<MeterRegistry> productionMeterFilters() {
return registry -> registry.config()
.meterFilter(MeterFilter.denyNameStartsWith("jvm.gc.pause")) // GC 세부 이벤트
.meterFilter(MeterFilter.deny(id ->
id.getName().startsWith("tomcat.servlet"))) // Servlet 내부 상세
.meterFilter(MeterFilter.maximumAllowableTags(
"http.server.requests", "uri", 100, MeterFilter.deny())); // uri cardinality 방어
}
auto-configured 메트릭 중 팀에서 사용하지 않는 것을 차단해서 Prometheus 메모리 부담을 줄여요.
Pattern 5: 개발 환경 메트릭 탐색 루틴
# 등록된 메트릭 이름 목록
curl -s http://localhost:8080/actuator/metrics | jq '.names[]' | sort
# 특정 메트릭 상세 (태그 목록 포함)
curl -s http://localhost:8080/actuator/metrics/http.server.requests | jq .
# 태그 조합으로 필터링
curl -s "http://localhost:8080/actuator/metrics/http.server.requests?tag=uri:/api/orders&tag=method:GET" | jq .
# Prometheus 형식 전체 덤프 (grep 으로 원하는 것만)
curl -s http://localhost:8080/actuator/prometheus | grep 'http_server_requests'
새 메트릭을 추가하고 나면 이 루틴으로 실제로 잡히는지 빠르게 확인할 수 있어요.
시험 직전 한 번 더 — Actuator 통합 압축 노트
auto-configuration
spring-boot-starter-actuator+micrometer-registry-prometheus→PrometheusMeterRegistry빈 +/actuator/prometheus자동 등록- 노출은 자동이 아님 →
management.endpoints.web.exposure.include=prometheus필수 - 내부 동작:
MeterBinder구현체들이 초기화 시점에 Meter 를 registry 에 일괄 등록
자동 계측 메트릭
- JVM —
jvm.memory.*,jvm.gc.*,jvm.threads.*,system.cpu.usage,process.* - HikariCP —
hikaricp.connections.*(active/idle/pending/acquire/timeout) - Tomcat —
tomcat.threads.*,tomcat.connections.* - HTTP 서버 —
http.server.requests(Timer, uri·method·status·outcome 태그) - HTTP 클라이언트 —
http.client.requests(RestTemplateBuilder로 생성해야 동작) - 기타 —
logback.events·cache.*·spring.kafka.*
/actuator/metrics vs /actuator/prometheus
/actuator/metrics→ JSON, 탐색용, Prometheus scrape 에 쓰면 안 됨/actuator/prometheus→ text format, Prometheus scrape target
@Timed · @Counted
@Timed는TimedAspect빈 없으면 아무 데이터도 안 잡힘 (http.server.requests 는 별개)@Counted는CountedAspect빈 필요extraTags로 추가 태그 가능
MeterRegistryCustomizer
MeterRegistryCustomizer<MeterRegistry>→ 모든 레지스트리에 적용MeterRegistryCustomizer<PrometheusMeterRegistry>→ 특정 레지스트리에만- YAML
management.metrics.tags.*와 동일한 효과
management.metrics.* 설정
distribution.percentiles-histogram.<metric>=true→ histogram 활성화distribution.slo.<metric>=50ms,100ms→ SLO 버킷 경계distribution.percentiles.<metric>=0.5,0.95,0.99→ 클라이언트 사이드 percentileenable.<metric>=false→ 특정 메트릭 비활성화
Observation API
- Spring Boot 3.x / Micrometer 1.10+ 기본 포함
ObservationRegistry+ObservationHandler→ 하나의 Observation 이 Timer + Span 동시 생성lowCardinalityKeyValue→ metrics 태그 + trace attributehighCardinalityKeyValue→ trace attribute only (metrics 태그 아님)@Observed사용 시ObservedAspect빈 필요- tracing 실제 전송:
micrometer-tracing-bridge-brave또는-bridge-otel+ reporter 의존성 세트
흔한 사고
prometheus를 expose include 에 안 넣음 → 404TimedAspect빈 미등록 →@Timed무효new RestTemplate()사용 →http.client.requests없음- histogram 전체 활성화 → Prometheus 메모리 급증
@Observed+ bridge 의존성 없음 → trace 전송 안 됨/actuator/metrics를 scrape target 으로 잘못 설정- Actuator 전체를 운영에 무보호 노출
공식 문서: Spring Boot Actuator — Metrics에서 전체 auto-configuration properties 와 지원 엔드포인트 레퍼런스를 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 1편 — 메트릭의 벤더 중립 facade·큰 그림
- 2편 — Meter 타입 깊이 (Counter·Gauge·Timer·DistributionSummary)
- 3편 — Timer·percentile·histogram·SLO 깊이
- 4편 — 태그·차원·카디널리티 깊이
다음 글: