Back-End/Spring

[Spring Boot 핵심원리와 활용] 사용자 정의 메트릭

seungwook_TIL 2025. 5. 11. 16:12

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


MeterRegistry

  • 마이크로미터 기능을 제공하는 핵심 컴포넌트
  • 스프링을 통해서 주입 받아서 사용하고, 이곳을 통해서 카운터, 게이지 등을 등록한다.
  • MeterRegistry는 마이크로미터(Micrometer)에서 모든 메트릭(Counter, Gauge, Timer 등)을 등록하고 관리하는 중심 객체이다.
  • Spring Boot에서는 MeterRegistry를 빈(bean)으로 자동 주입받아 사용할 수 있으며, 이 객체를 통해 커스텀 메트릭을 생성하거나 값을 기록할 수 있다.

 

 

카운터(Counter)

Counter(카운터)

  • 단조롭게 증가하는 단일 누적 측정항목 (단일 값, 보통 하나씩 증가)
  • 누적이므로 전체 값을 포함(total)
  • 프로메테우스에서는 일반적으로 카운터의 이름 마지막에 _total 을 붙여서 my_order_total 과 같이 표현함
  • 값을 증가하거나 0으로 초기화 하는 것만 가능
  • 마이크로미터에서 값을 감소하는 기능도 지원하지만, 목적에 맞지 않음
  • 예) HTTP 요청수

 

AOP 미사용

import hello.order.OrderService;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
public class OrderServiceV1 implements OrderService {

    private final MeterRegistry registry;
    private AtomicInteger stock = new AtomicInteger(100);

    public OrderServiceV1(MeterRegistry registry) {
        this.registry = registry;
    }

    @Override
    public void order() {
        log.info("주문");
        stock.decrementAndGet();

        Counter.builder("my.order") // 메트릭 이름
                .tag("class", this.getClass().getName()) // class 태그 등록
                .tag("method", "order") // method 태그 등록
                .description("order") // 메트릭 설명
                .register(registry) // MeterRegistry에 등록
                .increment(); // 카운터 1 증가
    }

    @Override
    public void cancel() {
        log.info("취소");
        stock.incrementAndGet();

        Counter.builder("my.order") // 메트릭 이름
                .tag("class", this.getClass().getName()) // class 태그 등록
                .tag("method", "cancel") // method 태그 등록
                .description("order") // 메트릭 설명
                .register(registry) // MeterRegistry에 등록
                .increment(); // 카운터 1 증가
    }

    @Override
    public AtomicInteger getStock() {
        return stock;
    }
}
  • Counter.builder(name) 를 통해서 카운터를 생성한다. name 에는 메트릭 이름을 지정한다.
  • tag 를 사용했는데, 프로메테우스에서 필터할 수 있는 레이블로 사용된다.
  • 주문과 취소는 메트릭 이름은 같고 tag 를 통해서 구분하도록 했다.
  • register(registry) : 만든 카운터를 MeterRegistry 에 등록한다. 이렇게 등록해야 실제 동작한다.
  • increment() : 카운터의 값을 하나 증가한다.

 

  • http://localhost:8080/actuator/metrics
  • my.order라는 메트릭이 등록된 것을 확인할 수 있다.

 

 

  • class와 method가 메트릭으로 등록된 것을 확인할 수 있다.

 

  • http://localhost:8080/actuator/metrics/my.order
  • 메트릭의 세부정보를 확인할 수 있다.

 

 

그라파나로 그래프를 볼 수 있다.

Panel options

  • Title : 주문수

 

PromQL

  • increase(my_order_total{method="order"}[5m])
    Legend : {{method}}
  • increase(my_order_total{method="cancel"}[5m])
    Legend : {{method}}

 

 

AOP 사용

import hello.order.OrderService;
import io.micrometer.core.annotation.Counted;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
public class OrderServiceV2 implements OrderService {

    private AtomicInteger stock = new AtomicInteger(100);

    @Counted("my.order")
    @Override
    public void order() {
        log.info("주문");
        stock.decrementAndGet();
    }

    @Counted("my.order")
    @Override
    public void cancel() {
        log.info("취소");
        stock.incrementAndGet();
    }

    @Override
    public AtomicInteger getStock() {
        return stock;
    }
}
  • @Counted 애노테이션을 측정을 원하는 메서드에 적용한다. 주문과 취소 메서드에 적용했다.
    그리고 메트릭 이름을 지정하면 된다. 여기서는 이전과 같은 my.order 를 적용했다.
  • 이렇게 사용하면 tag 에 method 를 기준으로 분류해서 적용한다.

 

 

Timer

Timer는 좀 특별한 메트릭 측정 도구인데, 시간을 측정하는 데 사용된다.

카운터와 유사한데, Timer를 사용하면 실행 시간도 함께 측정할 수 있다.

카운터와 타이머
카운터(Counter)는 “몇 번 실행되었는지 횟수만 센다.”
예: 사람들이 문을 몇 번 열었는지 센다 → 단순 누적 숫자

타이머(Timer)는 “얼마나 자주 실행되었는지 + 얼마나 오래 걸렸는지도 함께 센다.”
예: 사람들이 문을 여는 데 몇 초 걸렸는지도 측정 → 횟수 + 시간 정보 둘 다 포함

 

Timer는 다음과 같은 내용을 한 번에 측정해준다

  • seconds_count : 누적 실행 수 - 카운터
  • seconds_sum : 실행 시간의 합 - sum
  • seconds_max : 최대 실행 시간(가장 오래 걸린 실행 시간) - 게이지

내부에 타임 윈도우라는 개념이 있어서 1~3분마다 최대 실행 시간이 다시 계산된다.

 

AOP 미사용

import hello.order.OrderService;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j;

import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
public class OrderServiceV3 implements OrderService {

    private final MeterRegistry registry;
    private AtomicInteger stock = new AtomicInteger(100);

    public OrderServiceV3(MeterRegistry registry) {
        this.registry = registry;
    }

    @Override
    public void order() {
        Timer timer = Timer.builder("my.order") // 메트릭 이름
                .tag("class", this.getClass().getName()) // class 태그
                .tag("method", "order") // method 태그
                .description("order") // 메트릭 설명
                .register(registry); // MeterRegistry에 등록

        timer.record(() -> {
            log.info("주문");
            stock.decrementAndGet();
            sleep(500);
        });
    }

    @Override
    public void cancel() {
        Timer timer = Timer.builder("my.order") // 메트릭 이름
                .tag("class", this.getClass().getName()) // class 태그
                .tag("method", "cancel") // method 태그
                .description("order") // 메트릭 설명
                .register(registry); // MeterRegistry에 등록

        timer.record(() -> {
            log.info("취소");
            stock.incrementAndGet();
            sleep(200);
        });
    }

    private static void sleep(int l) {
        try {
            Thread.sleep(l + new Random().nextInt(200));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public AtomicInteger getStock() {
        return stock;
    }
}
  • Timer.builder(name)를 통해서 타이머를 생성한다. name에는 메트릭 이름을 지정한다.
  • tag를 사용했는데, 프로메테우스에서 필터할 수 있는 레이블로 사용된다.
  • 주문과 취소는 메트릭 이름은 같고 tag를 통해서 구분하도록 했다.
  • register(registry) : 만든 타이머를 MeterRegistry에 등록한다. 이렇게 등록해야 실제 동작한다.
  • 타이머를 사용할 때는 timer.record()를 사용하면 된다. 그 안에 시간을 측정할 내용을 함수로 포함하면 된다.

 

이제 그라파나로 그래프를 확인할 수 있다.

패널 옵션

  • Title : 주문수 v3

 

PromQL

  • increase(my_order_seconds_count{method=“order”}[5m])
    Legend : {{method}}
  • increase(my_order_seconds_count{method=“cancel”}[5m])
    Legend : {{method}}

 

최대 실행 시간과 평균 실행 시간도 그래프로 나타낼 수 있다.

  • 최대 실행 시간 PromQL : my_order_seconds_max
  • 평군 실행 시간 PromQL : increase(my_order_seconds_sum[1m]) / increase(my_order_seconds_count[1m])

 

 

AOP 사용

import hello.order.OrderService;
import io.micrometer.core.annotation.Timed;
import lombok.extern.slf4j.Slf4j;

import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

@Timed("my.order")
@Slf4j
public class OrderServiceV4 implements OrderService {

    private AtomicInteger stock = new AtomicInteger(100);

    @Override
    public void order() {
        log.info("주문");
        stock.decrementAndGet();
        sleep(500);
    }

    @Override
    public void cancel() {
        log.info("취소");
        stock.incrementAndGet();
        sleep(200);
    }

    private static void sleep(int l) {
        try {
            Thread.sleep(l + new Random().nextInt(200));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public AtomicInteger getStock() {
        return stock;
    }
}
  • @Timed("my.order") 타입이나 메서드 중에 적용할 수 있다. 타입에 적용하면 해당 타입의 모든 public 메서드에 타이머가 적용된다.
  • 이 경우 getStock() 에도 타이머가 적용된다.

 

 

게이지(Gauge)

  • 게이지는 임의로 오르내릴 수 있는 단일 숫자 값을 나타내는 메트릭
  • 값의 현재 상태를 보는데 사용
  • 값이 증가하거나 감소할 수 있음
  • 예) 차량의 속도, CPU 사용량, 메모리 사용량

 

게이지 등록

여기서는 재고 수를 게이지로 나타낸다.

import hello.order.OrderService;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class StockConfigV1 {

    @Bean
    public MyStockMetric myStockMetric(OrderService orderService, MeterRegistry registry) {
        return new MyStockMetric(orderService, registry);
    }

    @Slf4j
    static class MyStockMetric {
        private OrderService orderService;
        private MeterRegistry registry;

        public MyStockMetric(OrderService orderService, MeterRegistry registry) {
            this.orderService = orderService;
            this.registry = registry;
        }

        @PostConstruct
        public void init() {
            Gauge.builder("my.stock", orderService, service -> {
                log.info("stock gauge call");
                return service.getStock().get(); // 재고 수를 리턴
            }).register(registry);
        }
    }
}
  • my.stock 이라는 이름으로 게이지를 등록했다.
  • 게이지를 만들 때 함수를 전달했는데, 이 함수는 외부에서 메트릭을 확인할 때 마다 호출된다. 이 함수의 반환 값이 게이지의 값이다.
  • 애플리케이션을 실행하면 stock gauge call 로그가 주기적으로 남는 것을 확인할 수 있다.
    게이지를 확인하는 함수는 외부에서 메트릭을 확인할 때 호출 된다. 현재 프로메테우스가 다음 경로를 통해 주기적으로 메트릭을 확인하기 때문이다.

 

그라파나에서 그래프를 확인할 수 있다.

패널 옵션

  • Title : 재고

 

PromQL

  • my_stock

 

간단히 게이지 등록

import hello.order.OrderService;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.binder.MeterBinder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class StockConfigV2 {

    @Bean
    public MeterBinder stockSize(OrderService orderService) {
        return registry -> Gauge.builder("my.stock", orderService, service -> {
            log.info("stock gauge call");
            return service.getStock().get();
        }).register(registry);
    }
}
  • MeterBinder 타입을 바로 반환해도 된다.