이전 포스팅에서 예제프로그램을 만들어 보았다. 여기에 타이머(Timer) 개념을 추가해보겠다.
1) 카운터 ( Counter )
시간에 따라 '증가'만하는 데이터이다. 주문수는 항상 증가한다. 중간에 감소하지 않는다.
2) 게이지 ( Gauage )
시간에 따라 '증가'와 '감소'를 반복하는 데이터이다. 재고량은 쌓였다가 줄었다가 반복한다.
3) 타이머 ( Timer )
시간 관련된 데이터이다. 실행횟수,실행시간의 합, 최장실행시간 등 시간과 관련된 정보를 제공한다.
타이머(Timer)
MeterResistry는 데이터를 메트릭으로 등록하는 마이크로미터 모듈이다. MeterResistry는 스프링이 자동으로 IOC컨테이너에 Bean으로 등록한다. 우리는 의존관계 주입만 하면 사용할 수 있다.
OrderConfigTimer 클래스
@Configuration
public class OrderConfigTimer {
@Bean
public OrderService orderService(MeterRegistry meterRegistry) {
return new OrderServiceTimerV0(meterRegistry);
}
}
OrderConfigTimer 클래스는 OrderServiceTimerV0 클래스를 Bean으로 등록하는 설정클래스이다. MeterRegistry는 Spring이 엑츄에이터 라이브러리를 읽고 자동등록한 Bean인데, 커스텀 메트릭을 등록할때 사용하는 클래스이다. 의존관계 주입으로 해당 Bean을 생성자파라미터로 넣어준다.
OrderServiceTimerV0 클래스
타이머 메트릭 등록은 2가지로 나뉜다.
1. Timer 메트릭 등록
2. Timer가 기록할 로직
order() 메소드가 호출되면 타이머 메트릭이 생성되고 order() 메소드 로직 실행 관련 시간정보를 수집 및 측정한다. sleep() 메소드는 로직 실행시간이 다양하게 측정되도록 임의로 넣은 것이다.
@Slf4j
@RequiredArgsConstructor
public class OrderServiceTimerV0 implements OrderService {
private final MeterRegistry meterRegistry; //메트릭 등록모듈
private AtomicInteger stock = new AtomicInteger(100);
@Override
public void order() {
//타이머 메트릭 등록하기
Timer timer = Timer.builder("my.order")
.tag("class",this.getClass().getName())
.tag("method","order")
.description("order")
.register(meterRegistry);
// 타이머 메트릭이 기록할 로직
timer.record(()->{
log.info("주문...");
stock.decrementAndGet();
sleep(500); // 측정시간 랜덤하게 설정하기
});
}
@Override
public void cancel() {
Timer timer = Timer.builder("my.order")
.tag("class",this.getClass().getName())
.tag("method","cancel")
.description("order")
.register(meterRegistry);
timer.record(()->{
log.info("주문취소...");
stock.incrementAndGet();
sleep(200);
});
}
@Override
public AtomicInteger getStock() {
return stock;
}
private void sleep(int time) {
try {
Thread.sleep( time + new Random().nextInt(200)); // 측정시간은 time과 time + 200ms 사이의 임의의 시간이다.
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
전체코드는 위와 같다. 그럼 프로젝트를 실행해보자.
DemoApplication 클래스
@Import(OrderConfigTimer.class)
@SpringBootApplication(scanBasePackages = "com.example.demo.controller")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
main 함수를 실행하고 아래 URL로 접근한다.
☑︎ http://localhost:8080/order
☑︎ http://localhost:8080/cancel
주문과 취소를 실행하고 정상작동되면 아래 URL로 이동하여 메트릭이 잘 생성되었는지 확인해본다.
☑︎ http://localhost:8080/actuator/metrics/my.order
// 20230518112310
// http://localhost:8080/actuator/metrics/my.order
{
"name": "my.order",
"description": "order",
"baseUnit": "seconds",
"measurements": [
{
"statistic": "COUNT",
"value": 47.0
},
{
"statistic": "TOTAL_TIME",
"value": 20.902607671000002
},
{
"statistic": "MAX",
"value": 0.0
}
],
"availableTags": [
{
"tag": "method",
"values": [
"cancel",
"order"
]
},
{
"tag": "class",
"values": [
"com.example.demo.orderservice.timer.OrderServiceTimerV0"
]
}
]
}
메트릭 데이터가 우리가 설정한대로 잘 생성되었다.
한 가지 리팩토링 할 부분이 있다. 타이머 메트릭을 등록하고 기록하는 부분의 코드가 반복된다. Spring은 AOP 개념을 이용하여 코드를 단순화할 수 있도록 지원한다.
AOP로 리팩토링하기
TimeAspect 클래스는 MeterRegistry를 주입받아 @Timed 어노테이션으로 표시된 메소드를 메트릭으로 자동등록한다. 개발자가 MeterRegistry에 직접접근하여 등록하지 않고 자동화 모듈 하나를 두는 방식이다.
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-aop'
build.gradle의 AOP 디펜던시 하나를 추가한다.
OrderConfigTimer 클래스
@Configuration
public class OrderConfigTimer {
@Bean
OrderService orderService() {
return new OrderServiceTimerV1();
}
@Bean // TimedAspect Bean으로 등록
TimedAspect timedAspect(MeterRegistry meterRegistry){
return new TimedAspect(meterRegistry);
}
}
TimedAspect 클래스를 MeterRegistry 객체를 주입받아 Beand으로 생성한다. OrderServiceTimerV1은 @Timed로 메트릭을 등록한다.
OrderServiceTimerV1 클래스
@Slf4j
public class OrderServiceTimerV1 implements OrderService {
private AtomicInteger stock = new AtomicInteger(100);
@Override
@Timed("my.order") //어노테이션 등록
public void order() {
log.info("주문...");
stock.decrementAndGet();
sleep(500); // 측정시간 랜덤하게 설정하기
}
@Override
@Timed("my.order")
public void cancel() {
log.info("주문취소...");
stock.incrementAndGet();
sleep(200);
}
@Override
public AtomicInteger getStock() {
return stock;
}
private void sleep(int time) {
try {
Thread.sleep( time + new Random().nextInt(200)); // 측정시간은 time과 time + 200ms 사이의 임의의 시간이다.
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
코드가 한결 깔끔해졌다.
그럼 제대로 실행되는지 확인해보자.
☑︎ http://localhost:8080/order
☑︎ http://localhost:8080/cancel
주문과 취소를 실행하고 정상작동되면 아래 URL로 이동하여 메트릭이 잘 생성되었는지 확인해본다.
☑︎ http://localhost:8080/actuator/metrics/my.order
JSON 데이터가 잘 뜨면 메트릭등록이 완료됨을 의미한다.
프로메테우스, 그라파나 연동하기
프로메테우스 서버를 실행하고 SpringBoot 프로젝트와 연동하였다.
위 그림과 같이,
spring-actuator Endpoint의 status가 up이면 정상적으로 연동이 완료된 것이다.
그라파나 서버를 실행하고 Prometheus와 연동하였다.
그라파나는 프로메테우스에 쿼리를 보내 데이터를 가져온다.
▹ 주문기능 평균실행시간 추이 쿼리 : my_order_seconds_sum/my_order_seconds_count{method="order"}
▹ 취소기능 평균실행시간 추이 쿼리 : my_order_seconds_sum/my_order_seconds_count{method="cancel"}
주문 기능과 취소 기능의 평균실행시간이 어떻게 변화하는지 추이를 그래프로 보여준다. 이를 통해, 특정기능의 지연여부를 시각화할 수 있다. 주문기능과 취소기능을 연달아 수행해보자.
▹ 주문수행 : http://localhost:8080/order
▹ 취소수행 : http://localhost:8080/cancel
주문기능은 0.5~0.7초 사이로, 취소기능은 0.2~0.4초 사이로 설정하여 평균실행시간이 위 그림과 같이 시각회되었다. 이렇게 커스텀 메트릭도 데이터가 시각화를 완료하였다. 마이크로미터가 제공하는 기본 메트릭이 아닌 커스텀 메트릭도 마이크로미터에 등록할 수 있다.
'SPRING > Spring Boot' 카테고리의 다른 글
[SpringBoot] 커스텀 메트릭(Metric) 등록하기 - 카운터(Counter) (0) | 2023.05.16 |
---|---|
[SpringBoot] 커스텀 메트릭(Metric) 등록하기 - 예제 만들기 (0) | 2023.05.15 |
[SpringBoot] 메트릭(Metric) 이란? (0) | 2023.05.11 |
[SpringBoot] 엑츄에이터(Actuator)(3) - 로그 및 HTTP 기록 확인하기 (0) | 2023.05.11 |
[SpringBoot] 엑츄에이터(Actuator)(2) - info 엔드포인트 (0) | 2023.05.11 |