서비스가 느려졌다는 신고가 들어왔을 때, "어디가 느린지"를 수치로 확인할 수 없다면 어떻게 원인을 찾을 수 있을까요?

Micrometer란

Micrometer는 Java 애플리케이션의 메트릭 수집 추상화 라이브러리 입니다. SLF4J가 로깅의 파사드인 것처럼, Micrometer는 메트릭의 파사드입니다. 코드에서는 Micrometer API로 메트릭을 기록하고, 런타임에 Prometheus, Datadog, CloudWatch 등 다양한 백엔드로 전달합니다.

Spring Boot Actuator에 이미 포함되어 있어, Starter만 추가하면 JVM, Tomcat, DB 커넥션 풀 등의 메트릭이 자동으로 수집됩니다.

의존성과 설정

JAVA
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
YAML
# 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 — 단조 증가 카운터

항상 증가하는 값을 측정합니다. 총 요청 수, 에러 횟수 등에 사용합니다.

JAVA
@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();

실패 시에도 카운터를 증가시켜 에러율 대시보드를 구성할 수 있습니다.

JAVA
            return order;
        } catch (Exception e) {
            // 주문 실패 카운터 증가
            meterRegistry.counter("orders.failed",
                "reason", e.getClass().getSimpleName()
            ).increment();
            throw e;
        }
    }
}

Gauge — 현재 값

증가하고 감소하는 현재 상태를 나타냅니다. 메모리 사용량, 큐 크기, 활성 사용자 수 등에 사용합니다.

JAVA
@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 — 소요 시간

작업의 소요 시간과 호출 횟수를 함께 측정합니다.

JAVA
@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 — 수동 타이밍

JAVA
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 — 값 분포

크기나 양의 분포를 측정합니다. 요청 페이로드 크기, 배치 처리 건수 등에 사용합니다.

JAVA
DistributionSummary summary = DistributionSummary.builder("http.request.size")
    .baseUnit("bytes")
    .publishPercentiles(0.5, 0.95, 0.99)
    .register(meterRegistry);

summary.record(request.getContentLength());

@Timed — 선언적 타이머

JAVA
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
    return new TimedAspect(registry);  // AOP 기반 @Timed 활성화
}
JAVA
@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는 다양한 메트릭을 자동으로 수집합니다.

카테고리메트릭 예시설명
JVMjvm.memory.used힙/논힙 메모리
JVMjvm.gc.pauseGC 일시 정지 시간
JVMjvm.threads.live활성 스레드 수
HTTPhttp.server.requests요청 처리 시간, 상태 코드
DBhikaricp.connections.active활성 커넥션 수
DBhikaricp.connections.pending대기 중 요청 수
캐시cache.gets캐시 히트/미스
시스템process.cpu.usageCPU 사용률
시스템disk.free디스크 여유 공간

Prometheus 연동

YAML
# 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) 예시

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

알림 설정

YAML
# Grafana Alerting Rule 예시
# 5분간 에러율이 5%를 초과하면 알림
expr: |
  rate(http_server_requests_seconds_count{status=~"5.."}[5m])
  / rate(http_server_requests_seconds_count[5m]) > 0.05

커스텀 메트릭 설계 가이드

네이밍 규칙

PLAINTEXT
도메인.동작.단위
예: orders.created.total, payment.process.seconds
  • 소문자, 점(.)으로 계층 구분
  • 복수형 사용 (requests, orders)
  • 단위가 명확한 이름 (.seconds, .bytes)

태그 설계

JAVA
// 좋은 태그: 카디널리티가 낮은 값
counter.increment("method", "POST", "status", "200");

// 나쁜 태그: 카디널리티가 높은 값 (사용자 ID, 타임스탬프 등)
counter.increment("userId", userId);  // 사용자 수만큼 시계열 생성 → 메모리 폭발

태그의 고유 값 조합이 많아지면 Prometheus가 관리하는 시계열(time series)이 폭발적으로 증가합니다. 태그 값은 enum이나 상태 코드처럼 값의 범위가 제한된 것을 사용하세요.

핵심 모니터링 지표 (Four Golden Signals)

Google SRE가 제안하는 4가지 핵심 지표:

지표메트릭설명
Latencyhttp.server.requests (p95)응답 시간
Traffichttp.server.requests (rate)초당 요청 수
Errorshttp.server.requests (5xx rate)에러율
Saturationhikaricp.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(지연, 트래픽, 에러, 포화도)를 기본으로 모니터링하세요.
댓글 로딩 중...