![[Spring Boot 핵심원리와 활용] 사용자 정의 메트릭](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrvNvJ%2FbtsNS3t6q4a%2FPY9aikdrKJCajCMBNTvV81%2Fimg.png)
[Spring Boot 핵심원리와 활용] 사용자 정의 메트릭Back-End/Spring2025. 5. 11. 16:12
Table of Contents
이 글은 인프런 김영한님의 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 타입을 바로 반환해도 된다.
'Back-End > Spring' 카테고리의 다른 글
[Spring Boot 핵심원리와 활용] 마이크로미터, 프로메테우스, 그라파나 (0) | 2025.05.10 |
---|---|
[Spring Boot 핵심원리와 활용] 액츄에이터(Actuator) (0) | 2025.05.09 |
[Spring Boot 핵심 원리와 활용] 외부 설정 (0) | 2025.05.03 |
[Spring Boot 핵심 원리와 활용] 자동 구성 라이브러리 제작 및 사용 (0) | 2025.05.02 |
[스프링 핵심원리 - 고급] 스프링 AOP 실무 주의사항 (0) | 2025.04.27 |