Spring/[인프런 김영한 스프링 부트 - 핵심 원리와 활용]
[인프런 김영한 스프링 부트 - 핵심 원리와 활용] 모니터링 메트릭 활용
h2boom
2025. 2. 10. 16:05
모니터링 메트릭 활용
메트릭 등록 - 예제 만들기
- 공통으로 사용되는 기술 메트릭(CPU, 메모리 사용량, 쓰레드...)은 이미 등록되어 있기에 사용해서 모니터링 할 수 있다.
- 비지니스에 특화된 부분들(재고량, 취소수...)과 관련된 비지니스 메트릭이 있으면 비지니스와 관련된 문제들을 빠르게 인지할 수 있다.
- 비지니스 메트릭은 직접 등록하고 확인해야 한다.
- 비지니스 메트릭을 위한 예제와 정의
- 주문수 / 취소수
- 상품 주문 시 주문수가 증가
- 상품 취소 시 주문수는 유지되지만 취소수가 증가
- 재고 수량
- 상품 주문 시 재고 수량 감소
- 상품 주문 취소 시 재고 수량 증가
- 재고 물량이 들어오면 재고 수량 증가
- 주문수, 취소수는 계속 증가하기에 카운터를 사용
- 재고 수량은 증가하거나 감소하므로 게이지를 사용
- 주문수 / 취소수
public interface OrderService {
void order();
void cancel();
//멀티 쓰레드 상황에서 안전하게 값을 증감하기 위한 타입
AtomicInteger getStock();
}
- 주문 서비스 인터페이스 예제
- 주문, 취소, 재고 수량을 확인
@Slf4j
public class OrderServiceV0 implements OrderService {
//멀티 쓰레드 상황에서 안전하게 값을 증감하기 위한 타입
private AtomicInteger stock = new AtomicInteger(100);
@Override
public void order() {
log.info("주문");
stock.decrementAndGet();
}
@Override
public void cancel() {
log.info("취소");
stock.incrementAndGet();
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
- 서비스 인터페이스를 구현한 구현체 V0
- 멀티쓰레드 상황에서 안전하게 값을 증감할 수 있도록 AtomicInteger 타입을 사용
@Configuration
public class OrderConfigV0 {
@Bean
OrderService orderService() {
return new OrderServiceV0();
}
}
- 자바 설정 클래스 예제 V0
- 서비스 구현체를 빈으로 직접 등록하는 설정 클래스
@Slf4j
@RestController
public class OrderController {
public final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/order")
public String order() {
log.info("order");
orderService.order();
return "order";
}
@GetMapping("/cancel")
public String cancel() {
log.info("cancel");
orderService.cancel();
return "cancel";
}
@GetMapping("/stock")
public int stock() {
log.info("stock");
return orderService.getStock().get();
}
}
- 주문, 취소, 재고를 확인하는 컨트롤러 예제
- 예제의 단순함을 위해 GET 사용
@Import(OrderConfigV0.class)
@SpringBootApplication(scanBasePackages = "hello.controller")
public class ActuatorApplication {
public static void main(String[] args) {
SpringApplication.run(ActuatorApplication.class, args);
}
@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
return new InMemoryHttpExchangeRepository();
}
}
- 애플리케이션 클래스
- @Import로 자바 설정 파일을 별도로 등록
메트릭 등록 - 카운터
- MeterRegistry : 마이크로미터 기능을 제공하는 핵심 컴포넌트
- 스프링을 통해 주입받아 사용하고 이것을 통해 카운터, 게이지등을 등록한다.
- Counter(카운터) : 단조롭게 증가하는 단일 누적 측정항목
- 단일 값으로 보통 하나씩 증가하며 누적이므로 전체 값을 포함한다.
- 프로메테우스에서는 일반적으로 카운터의 이름 마지막에 _total을 붙여서 표현
- 값을 증가하거나 0으로 초기화하는 것만 가능하다.
- 값을 감소하는 기능도 제공은 하지만 목적에 맞지 않는다.
@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())
.tag("method", "order")
.description("order")
.register(registry).increment();
}
@Override
public void cancel() {
log.info("취소");
stock.incrementAndGet();
Counter.builder("my.order")
.tag("class", this.getClass().getName())
.tag("method", "cancel")
.description("order")
.register(registry).increment();
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
- 서비스 인터페이스를 구현한 구현체 V1
- MeterRegistry : 마이크로미터를 사용하기 위한 핵심 컴포넌트
- Counter.builder() : 카운터를 생성하며 인자로 메트릭 이름을 지정
- tag() : 프로메테우스에서 필터할 수 있는 레이블로 사용된다.
- 예제에서 주문과 취소의 메트릭 이름은 같지만 tag를 통해서 구분하도록 했다.
- register(registry) : 만든 카운터를 MeterRegistry에 등록함으로 동작하게 한다.
- increment() : 카운터의 값을 하나 증가시킨다.
- 예제에서 만든 카운터를 그라파나에 등록
- 카운터이기 때문에 단순히 등록하면 계속 증가하는 형태이기에 increase(), rate()를 사용할 것
- legend(범례)를 직접 지정할 수도 있지만 {{method}} 형태와 같이 지정하면 tag로 등록한 method 이름을 가져온다.
- ex) increase(my_order_total{method="order"}[1m]), Lenged => {{method}}
increase(my_order_total{method="cancel"}[1m]), Legend => {{method}}
- 카운터이기 때문에 단순히 등록하면 계속 증가하는 형태이기에 increase(), rate()를 사용할 것
메트릭 등록 - @Counter
- 이전 예제인 V1의 단점은 메트릭 관리 로직이 핵심 비지니스 개발 로직에 침투했다는 점이다.
- 마이크로미터에서 제공하는 스프링 AOP를 사용하면 문제를 해결할 수 있다.
=> @Counted
- 마이크로미터에서 제공하는 스프링 AOP를 사용하면 문제를 해결할 수 있다.
@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;
}
}
- 서비스 인터페이스를 구현한 구현체 V2
- @Counted 어노테이션을 측정을 원하는 메소드에 적용
- 인자로 메트릭 이름을 지정
- tag에 method, class, result, exception을 기준으로 자동으로 분류해서 적용해준다.
- @Counted 어노테이션을 측정을 원하는 메소드에 적용
@Configuration
public class OrderConfigV2 {
@Bean
OrderService orderService() {
return new OrderServiceV2();
}
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
}
- 자바 설정 파일 V2
- CountedAspect를 등록하면 @Counted를 인지해서 Counter를 사용하는 AOP를 적용한다.
- CountedAspect가 빈으로 등록되지 않으면 @Counted 관련 AOP가 적용되지 않는다.
- CountedAspect를 등록하면 @Counted를 인지해서 Counter를 사용하는 AOP를 적용한다.
메트릭 등록 - Timer
- Timer : 메트릭 측정 도구로 시간 측정을 하는데 사용한다.
- seconds_count : 누적 실행 수 (=카운터)
- seconds_sum : 실행 시간의 합 (=sum)
- seconds_max : 최대 실행 시간 (=게이지)
- seconds_sum / seconds_count로 평균 실행 시간도 계산할 수 있다.
@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())
.tag("method", "order")
.description("order")
.register(registry);
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(registry);
timer.record(() -> {
log.info("취소");
stock.incrementAndGet();
});
}
private void sleep(int millis) {
try {
Thread.sleep(millis + new Random().nextInt(200));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
- 서비스 인터페이스를 구현한 구현체 V3
- Timer.builder() : 타이머를 생성하며 인자로 메트릭 이름을 지정한다.
- timer.record() : 타이머를 실행하며 그 안에 시간을 측정할 내용을 포함한다.
- 나머지(tag, register..)는 카운터와 유사하다.
- 그라파나 등록
- 실행 횟수
- 누적 실행 수인 seconds_count는 카운터이기에 increase(), rate()를 사용해야한다.
- ex) increase(my_order_seconds_count{method="order"}[1m])
Legend => {{method}}
- 최대 실행시간
- 최대 실행 시간인 seconds_max는 게이지이기에 바로 측정하면 된다.
- ex) my_order_seconds_max
Legend => {{method}}
- 평균 실행시간
- 실행시간의 합인 seconds_sum을 누적 실행 수인 seconds_count로 나눠준다.
- seconds_sum / seconds_count
- ex) my_order_seconds_sum / my_order_seconds_count 표현해도 되지만
카운터이기에 increase(my_order_seconds_sum[1m]) / increase(my_order_seconds_count[1m])로 표현하면 특정 시간에 얼마나 증가했는지 보기 좋다.
- 실행시간의 합인 seconds_sum을 누적 실행 수인 seconds_count로 나눠준다.
- 실행 횟수
메트릭 등록 - @Timed
- Timer는 @Timed 어노테이션을 통해 AOP를 적용할 수 있다.
@Slf4j
@Timed(value = "my.order")
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 void sleep(int millis) {
try {
Thread.sleep(millis + new Random().nextInt(200));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
- 서비스 인터페이스를 구현한 구현체 V4
- @Timed(value) : 타입이나 메소드 중에 AOP로 Timer를 적용할 수 있다.
- 타입에 적용하는 경우 모든 public 메소드에 Timer가 적용된다.
- @Timed(value) : 타입이나 메소드 중에 AOP로 Timer를 적용할 수 있다.
@Configuration
public class OrderConfigV4 {
@Bean
OrderService orderService() {
return new OrderServiceV4();
}
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
- 자바 설정 파일 V4
- @Timed 어노테이션을 작동시키기 위해서 TimedAspect를 빈으로 등록해줘야 한다.
메트릭 등록 - 게이지
- Gauge(게이지) : 임의로 오르내릴 수 있는 단일 숫자 값을 나타내는 메트릭
- 값의 현재 상태를 보는데 사용하며 값이 증가하거나 감소할 수 있다.
- 카운터와 게이지를 구분할 때는 값이 감소할 수 있는지 구분하면 된다.
- 값이 감소할 수 있으면 게이지, 없다면 카운터이다.
@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);
}
}
}
- 게이지 예제 V1
- Gauge.builder()를 통해 my.stock 이라는 이름의 게이지를 등록
- 게이지를 만들 때 함수를 전달하는데 함수는 외부에서 메트릭을 확인할 때마다 호출된다.
- 함수의 반환 값이 게이지의 값
- 메트릭을 확인할 때마다 인자로 넘겨준 함수, 예제에서는 람다함수가 호출된다.
@Configuration
@Slf4j
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);
}
}
- 게이지 예제 V2
- V1 예제보다 더 단순하게 게이지 등록 코드를 작성할 수 있다.
실무 모니터링 환경 구성 팁
- 모니터링 3단계
- 대시보드
- 애플리케이션 추적 - 핀포인트
- 로그
- 대시보드 : 전체를 한 눈에 볼 수 있는 가장 높은 뷰
- 제품 - 마이크로미터, 프로메테우스, 그라파나
- 모니터링 대상 - 시스템 메트릭(CPU, 메모리), 애플리케이션 메트릭 (톰켓 쓰레드 풀, DB 커넥션 풀, 애플리케이션 호출 수), 비즈니스 메트릭(주문수, 취소수)
- 애플리케이션 추적 : 각각의 HTTP 요청을 추적, 일부는 마이크로서비스 환경에서 분산 추적
- 제품 - 핀포인트(오픈소스), 스카우트(오픈소스), 와탭(상용), 제니퍼(상용)
- 로그 : 가장 자세한 추적, 원하는대로 커스텀이 가능
- 같은 HTTP 요청을 묶어서 확인할 수 있는 방법이 중요 => MDC 적용
- 파일로 직접 로그를 남기는 경우
- 일반 로그와 에러 로그는 파일을 구분해서 남길 것
- 에러 로그만 확인해서 문제를 바로 정리할 수 있다.
- 일반 로그와 에러 로그는 파일을 구분해서 남길 것
- 클라우드에 로그를 저장하는 경우
- 검색이 잘 되도록 구분할 것
- 큰 범위(전체) -> 좁은 범위로 점점 좁게 모니터링하는 것이 좋다.
- 알람
- 모니터링 툴에서 일정 이상 수치가 넘어가면 슬랙, 문자 등을 연동해서 설정해놓는 것이 좋다.
- 알람은 2가지 종류로 꼭 구분해서 관리
- 경고 - 하루 한 번 정도 사람이 직접 확인해도 되는 수준
- 심각 - 즉시 확인해야 하는 수준
정리
- 메트릭은 100% 정확한 숫자를 보는데 사용하는 것이 아닌 약간의 오차를 감안하고 실시간으로 대략의 데이터를 보는 것이 목적이다.
- MeterRegistry : 마이크로미터 기능을 제공하는 핵심 컴포넌트로 이것을 통해 카운터, 게이지등을 등록한다.
- Counter : 단조롭게 증가하는 단일 누적 측정항목이다.
- @Counted 어노테이션을 통해 Counter를 AOP로 적용할 수 있다.
- Gauge : 임의로 오르내릴 수 있는 단일 숫자 값을 나타내는 메트릭이다.
- Timer : 시간 측정하는데 사용하며 Counter와 유사하다.
- @Timed 어노테이션을 통해 Timer를 AOP로 적용할 수 있다.
- Tag, 레이블 : 데이터를 나눠서 확인하는 용도로 사용되며 카디널리티가 낮으면서 그룹화할 수 있는 단위에 사용해야 한다.
- ex) 성별, 주문 상태, 결제 수단 등등
- 카디널리티가 높은 안되는 예시 => 주문번호, PK 등등
출처 : [인프런 김영한 스프링 부트 - 핵심 원리와 활용]
스프링 부트 - 핵심 원리와 활용 강의 | 김영한 - 인프런
김영한 | 실무에 필요한 스프링 부트는 이 강의 하나로 모두 정리해드립니다., 백엔드 개발자를 위한 스프링 부트 끝판왕! 실무에 필요한 내용을 모두 담았습니다. [임베딩 영상] 김영한의 스
www.inflearn.com