Micrometer — 스프링의 메트릭 추상화와 Prometheus 연동
서비스가 느려졌다는 신고가 들어왔을 때, "어디가 느린지"를 수치로 확인할 수 없다면 어떻게 원인을 찾을 수 있을까요?
Micrometer란
Micrometer는 Java 애플리케이션의 메트릭 수집 추상화 라이브러리 입니다. SLF4J가 로깅의 파사드인 것처럼, Micrometer는 메트릭의 파사드입니다. 코드에서는 Micrometer API로 메트릭을 기록하고, 런타임에 Prometheus, Datadog, CloudWatch 등 다양한 백엔드로 전달합니다.
Spring Boot Actuator에 이미 포함되어 있어, Starter만 추가하면 JVM, Tomcat, DB 커넥션 풀 등의 메트릭이 자동으로 수집됩니다.
의존성과 설정
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
# application.yml
management:
endpoints:
web:
exposure:
include: health, info, prometheus, metrics
metrics:
tags:
application: my-service # 모든 메트릭에 공통 태그 추가
distribution:
percentiles-histogram:
http.server.requests: true # 히스토그램 활성화
percentiles:
http.server.requests: [0.5, 0.95, 0.99] # 백분위 수
이제 http://localhost:8080/actuator/prometheus에서 Prometheus 형식의 메트릭을 확인할 수 있습니다.
메트릭 타입
Counter — 단조 증가 카운터
항상 증가하는 값을 측정합니다. 총 요청 수, 에러 횟수 등에 사용합니다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final MeterRegistry meterRegistry;
public Order createOrder(OrderRequest request) {
try {
Order order = processOrder(request);
// 주문 성공 카운터 증가
meterRegistry.counter("orders.created",
"type", request.getType(),
"region", request.getRegion()
).increment();
실패 시에도 카운터를 증가시켜 에러율 대시보드를 구성할 수 있습니다.
return order;
} catch (Exception e) {
// 주문 실패 카운터 증가
meterRegistry.counter("orders.failed",
"reason", e.getClass().getSimpleName()
).increment();
throw e;
}
}
}
Gauge — 현재 값
증가하고 감소하는 현재 상태를 나타냅니다. 메모리 사용량, 큐 크기, 활성 사용자 수 등에 사용합니다.
@Configuration
public class MetricsConfig {
@Bean
public MeterBinder queueSizeGauge(TaskQueue taskQueue) {
return registry -> Gauge.builder("task.queue.size",
taskQueue, TaskQueue::size)
.description("현재 대기 중인 작업 수")
.register(registry);
}
@Bean
public MeterBinder activeUsersGauge(SessionStore sessionStore) {
return registry -> Gauge.builder("users.active",
sessionStore, SessionStore::getActiveCount)
.description("현재 활성 사용자 수")
.register(registry);
}
}
Timer — 소요 시간
작업의 소요 시간과 호출 횟수를 함께 측정합니다.
@Service
@RequiredArgsConstructor
public class PaymentService {
private final MeterRegistry meterRegistry;
public PaymentResult processPayment(PaymentRequest request) {
// Timer로 소요 시간 측정
return Timer.builder("payment.process")
.tag("gateway", request.getGateway())
.tag("currency", request.getCurrency())
.description("결제 처리 소요 시간")
.register(meterRegistry)
.record(() -> {
// 실제 결제 처리 로직
return paymentGateway.charge(request);
});
}
}
Timer.Sample — 수동 타이밍
public void complexOperation() {
Timer.Sample sample = Timer.start(meterRegistry);
try {
// 복잡한 작업...
step1();
step2();
step3();
sample.stop(Timer.builder("complex.operation")
.tag("result", "success")
.register(meterRegistry));
} catch (Exception e) {
sample.stop(Timer.builder("complex.operation")
.tag("result", "failure")
.register(meterRegistry));
throw e;
}
}
Distribution Summary — 값 분포
크기나 양의 분포를 측정합니다. 요청 페이로드 크기, 배치 처리 건수 등에 사용합니다.
DistributionSummary summary = DistributionSummary.builder("http.request.size")
.baseUnit("bytes")
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry);
summary.record(request.getContentLength());
@Timed — 선언적 타이머
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry); // AOP 기반 @Timed 활성화
}
@Service
public class ProductService {
@Timed(
value = "product.search",
description = "상품 검색 소요 시간",
extraTags = { "layer", "service" }
)
public List<Product> search(String keyword) {
return productRepository.findByKeyword(keyword);
}
}
자동 수집되는 메트릭
Spring Boot Actuator + Micrometer는 다양한 메트릭을 자동으로 수집합니다.
| 카테고리 | 메트릭 예시 | 설명 |
|---|---|---|
| JVM | jvm.memory.used | 힙/논힙 메모리 |
| JVM | jvm.gc.pause | GC 일시 정지 시간 |
| JVM | jvm.threads.live | 활성 스레드 수 |
| HTTP | http.server.requests | 요청 처리 시간, 상태 코드 |
| DB | hikaricp.connections.active | 활성 커넥션 수 |
| DB | hikaricp.connections.pending | 대기 중 요청 수 |
| 캐시 | cache.gets | 캐시 히트/미스 |
| 시스템 | process.cpu.usage | CPU 사용률 |
| 시스템 | disk.free | 디스크 여유 공간 |
Prometheus 연동
# prometheus.yml
scrape_configs:
- job_name: 'spring-app'
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
static_configs:
- targets: ['host.docker.internal:8080']
labels:
environment: 'production'
Prometheus 쿼리 (PromQL) 예시
# 초당 요청 수 (1분 평균)
rate(http_server_requests_seconds_count[1m])
# 95백분위 응답 시간
histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m]))
# 에러율
rate(http_server_requests_seconds_count{status=~"5.."}[5m])
/ rate(http_server_requests_seconds_count[5m])
# HikariCP 대기 중인 요청
hikaricp_connections_pending{pool="HikariPool-1"}
Grafana 대시보드
Prometheus에서 수집한 메트릭을 Grafana 대시보드로 시각화합니다.
추천 대시보드 템플릿:
- JVM (Micrometer): Grafana Dashboard ID
4701 - Spring Boot Statistics: Grafana Dashboard ID
12900
알림 설정
# Grafana Alerting Rule 예시
# 5분간 에러율이 5%를 초과하면 알림
expr: |
rate(http_server_requests_seconds_count{status=~"5.."}[5m])
/ rate(http_server_requests_seconds_count[5m]) > 0.05
커스텀 메트릭 설계 가이드
네이밍 규칙
도메인.동작.단위
예: orders.created.total, payment.process.seconds
- 소문자, 점(
.)으로 계층 구분 - 복수형 사용 (
requests,orders) - 단위가 명확한 이름 (
.seconds,.bytes)
태그 설계
// 좋은 태그: 카디널리티가 낮은 값
counter.increment("method", "POST", "status", "200");
// 나쁜 태그: 카디널리티가 높은 값 (사용자 ID, 타임스탬프 등)
counter.increment("userId", userId); // 사용자 수만큼 시계열 생성 → 메모리 폭발
태그의 고유 값 조합이 많아지면 Prometheus가 관리하는 시계열(time series)이 폭발적으로 증가합니다. 태그 값은 enum이나 상태 코드처럼 값의 범위가 제한된 것을 사용하세요.
핵심 모니터링 지표 (Four Golden Signals)
Google SRE가 제안하는 4가지 핵심 지표:
| 지표 | 메트릭 | 설명 |
|---|---|---|
| Latency | http.server.requests (p95) | 응답 시간 |
| Traffic | http.server.requests (rate) | 초당 요청 수 |
| Errors | http.server.requests (5xx rate) | 에러율 |
| Saturation | hikaricp.connections.pending | 리소스 포화도 |
주의할 점
1. 태그의 카디널리티가 높으면 Prometheus 메모리가 폭발한다
태그 값에 사용자 ID, 요청 URL, 타임스탬프 같은 고유 값을 넣으면 조합 수만큼 시계열(time series)이 생성됩니다. 사용자가 10만 명이면 10만 개의 시계열이 만들어져 Prometheus의 메모리와 디스크를 빠르게 소진합니다. 태그 값은 HTTP 메서드, 상태 코드, 서비스 이름처럼 범위가 제한된 값만 사용하세요.
2. /actuator/prometheus 엔드포인트를 인증 없이 공개하면 내부 정보가 노출된다
Prometheus 메트릭에는 JVM 메모리, DB 커넥션 풀, 활성 스레드 수 등 시스템 내부 정보가 포함되어 있습니다. 이 엔드포인트가 외부에 노출되면 공격자가 시스템 상태를 파악하여 취약점을 악용할 수 있습니다. Spring Security로 접근을 제한하거나, 내부 네트워크에서만 접근 가능하도록 설정하세요.
3. @Timed를 사용하려면 TimedAspect 빈을 등록해야 한다
@Timed 어노테이션만 붙이고 TimedAspect 빈 등록을 빼먹으면 메트릭이 수집되지 않습니다. 에러도 나지 않고 단순히 메트릭이 누락되어, "분명 어노테이션을 붙였는데 Grafana에 데이터가 안 보인다"는 상황에 빠집니다. @Bean public TimedAspect timedAspect(MeterRegistry registry)를 반드시 등록하세요.
정리
- Micrometer 는 메트릭 수집의 파사드입니다. Prometheus, Datadog 등 백엔드를 자유롭게 교체할 수 있습니다.
- Counter(누적), Gauge(현재값), Timer(소요시간), DistributionSummary(분포)가 핵심 메트릭 타입입니다.
- Spring Boot Actuator가 JVM, HTTP, DB 커넥션 풀 메트릭을 ** 자동 수집 **합니다.
/actuator/prometheus엔드포인트를 Prometheus가 스크래핑하여 Grafana로 시각화합니다.- 태그의 ** 카디널리티 **에 주의하세요. 고유 값이 많은 태그는 메모리 문제를 유발합니다.
- Four Golden Signals(지연, 트래픽, 에러, 포화도)를 기본으로 모니터링하세요.