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}}

메트릭 등록 - @Counter

  • 이전 예제인 V1의 단점은 메트릭 관리 로직이 핵심 비지니스 개발 로직에 침투했다는 점이다.
    • 마이크로미터에서 제공하는 스프링 AOP를 사용하면 문제를 해결할 수 있다.
      => @Counted

 

@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을 기준으로 자동으로 분류해서 적용해준다.

 

@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가 적용되지 않는다.

메트릭 등록 - 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])로 표현하면 특정 시간에 얼마나 증가했는지 보기 좋다.

메트릭 등록 - @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가 적용된다.

 

@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 등등

출처 : [인프런 김영한 스프링 부트 - 핵심 원리와 활용]

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%ED%95%B5%EC%8B%AC%EC%9B%90%EB%A6%AC-%ED%99%9C%EC%9A%A9/dashboard

 

스프링 부트 - 핵심 원리와 활용 강의 | 김영한 - 인프런

김영한 | 실무에 필요한 스프링 부트는 이 강의 하나로 모두 정리해드립니다., 백엔드 개발자를 위한 스프링 부트 끝판왕! 실무에 필요한 내용을 모두 담았습니다.  [임베딩 영상] 김영한의 스

www.inflearn.com