-
커스텀 메트릭 사용해보기글또 2025. 8. 17. 12:06
들어가며
안녕하세요.
이번 글에서는 모니터링에 대해 다뤄보려고 합니다.
최근 회사에서 여러 서비스와 연동되는 기능을 개발하던 중,
기능 테스트 과정에서 “테스트 과정에서 보이는 수치들을 시각화할 수 있다면 검증이 훨씬 수월하겠다”는 생각을 하게 되었습니다.
우선 1차적으로는 그라파나를 활용해 모니터링 환경을 구축했습니다.
각 DB에 직접 연결해 SQL을 조회하고, Mixed 패널을 통해 여러 DB 데이터를 한 화면에서 확인할 수 있도록 했습니다.
하지만 이 방식에는 빈틈이 있었습니다.
구현 상 DB에서 하드 딜리트된 데이터는 로그로만 남아 검증에서 누락되는 경우가 있었고, 이를 추적하기가 까다로웠습니다.이 문제를 해결하기 위해 로그로 남는 정보들을 메트릭화할 수 없을까 고민하다가 알게 된 것이 바로 커스텀 메트릭이었습니다.
이를 적용하면 지금의 테스트뿐만 아니라 다양한 정보를 모니터링하고, 필요할 때 알림까지 받을 수 있겠다고 생각했습니다.
커스텀 메트릭 구현
Spring을 사용해 어플리케이션을 구현하고 있었기 떄문에 위와 같은 흐름으로 모니터링을 할 수 있습니다.
애플리케이션에서 메트릭을 쌓고, 프로메테우스가 일정 주기로 이를 수집한 뒤, 그라파나에서 시각화하는 방식입니다.
방법은 의외로 간단했습니다.
Spring 애플리케이션이 모니터링 엔드포인트를 열어두면 프로메테우스가 주기적으로 데이터를 가져갑니다.
Spring Actuator를 통해 /actuator/prometheus URL을 확인할 수 있고, 그라파나 대시보드에서도 해당 데이터를 조회할 수 있습니다.
또한 다양한 메트릭을 AOP를 활용해 쉽게 측정할 수 있었고, 프로메테우스를 통해 결과를 모니터링할 수 있었습니다.제공되는 AOP
사용법 자체는 다른 글에서도 쉽게 확인할 수 있으니, 여기서는 간단히만 정리하겠습니다.
- Counter: 단방향 증가만 가능 (예: 주문 수)
- Timed: 시간 측정 (예: 메소드 수행 시간)
- ...
처음에는 제공되는 AOP가 없는 줄 알고 직접 구현했는데, 알고 보니 이미 Micrometer에서 지원하는 AOP가 있었습니다.
이를 활용하면 훨씬 간단하게 구현할 수 있습니다.
@Bean fun timedAspect(registry: MeterRegistry): TimedAspect { return TimedAspect(registry) } @Bean fun countedAspect(registry: MeterRegistry): CountedAspect { return CountedAspect(registry) }
메소드에 애노테이션만 추가하면 됩니다:
@Counted("custom_orders_total") @Timed("custom_orders_total_seconds") fun incrementOrderCount(productId: String) { // do something }
이를 통해 메트릭 정보가 MeterRegistry 안에 저장되고, 프로메테우스가 주기적으로 스크래핑하여 데이터를 가져갑니다.
수집된 정보는 그라파나에서 손쉽게 시각화할 수 있습니다.
아래는 그라파나 대시보드 예시입니다.간단한 내부 동작
/actuator/prometheus 엔드포인트는 PrometheusScrapeEndpoint 클래스에서 담당합니다.
내부에서 scrape메소드가 prometheusRegistry에 쌓인 메트릭 정보를 읽어서 반환하는 구조입니다.
@WebEndpoint(id = "prometheus") public class PrometheusScrapeEndpoint { ... @ReadOperation(producesFrom = PrometheusOutputFormat.class) public WebEndpointResponse<byte[]> scrape(PrometheusOutputFormat format, @Nullable Set<String> includedNames) { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(this.nextMetricsScrapeSize); MetricSnapshots metricSnapshots = (includedNames != null) ? this.prometheusRegistry.scrape(includedNames::contains) : this.prometheusRegistry.scrape(); format.write(this.expositionFormats, outputStream, metricSnapshots); byte[] content = outputStream.toByteArray(); this.nextMetricsScrapeSize = content.length + METRICS_SCRAPE_CHARS_EXTRA; return new WebEndpointResponse<>(content, format); } catch (IOException ex) { throw new IllegalStateException("Writing metrics failed", ex); } }
메트릭 라벨 관리
기본적인 구현은 MeterRegistry 에서 이뤄집니다.
우리가 사용하는 메트릭 정보들은 MeterRegistry 내부에는 meterMap 형태로 객체가 저장됩니다.
/** * Even though writes are guarded by meterMapLock, iterators across value space are * supported. Hence, we use CHM to support that iteration without * ConcurrentModificationException risk. */ private final Map<Meter.Id, Meter> meterMap = new ConcurrentHashMap<>();
메트릭은 메모리에 저장되기 때문에 라벨 수가 지나치게 많아지면 메모리 사용량이 증가할 수 있습니다.
따라서 사용자 ID, 상품 ID처럼 동적으로 끝없이 늘어날 수 있는 라벨 말고, 서비스 단위나 상태 단위, 카테고리처럼 제한된 범위에서 관리하는 것이 더 유용한 선택일 수 있습니다.보안/접근 제한
/actuator/prometheus 엔드포인트는 기본적으로 인증 절차가 없기 때문에, 보안 설정을 추가해야 합니다.
Spring을 사용하고 있다면 Spring Security로 접근 제어를 걸 수 있습니다.
# application.properties spring.security.user.name=prometheus spring.security.user.password=secret spring.security.user.roles=ACTUATOR # prometheus yml scrape_configs: - job_name: 'spring-boot' metrics_path: '/actuator/prometheus' static_configs: - targets: ['host.docker.internal:8080'] basic_auth: username: 'prometheus' password: 'secret'
위와 같이 설정하면 HTTP Basic 인증을 적용할 수 있고, 프로메테우스 쪽 설정에서도 인증 정보를 함께 등록해주면 됩니다.다만, 단순 Basic 인증만으로는 충분하지 않을 수 있습니다.
예를 들어 레인보우 테이블 기반의 무차별 대입 공격이 가능하기 때문에, IP 제한, 인증 블랙리스트, 네트워크 레벨 보안 등의 추가 방어 전략을 고려할 수도 있습니다.
마무리
아직 커스텀 메트릭을 실제로 적용하지는 않았습니다.
그럼에도 앞으로 다양한 정보를 모니터링할 수 있겠다는 가능성을 확인했습니다.
이를 기반으로 더 단단하고 안정적인 프로그램을 만들 수 있을 것 같습니다.GitHub - choigiseong/custom-metric
Contribute to choigiseong/custom-metric development by creating an account on GitHub.
github.com
- +
actuator를 활용해 헬스 체크로 사용사례
Netty를 사용해 헬스 체크하는 사례도 있습니다.
'글또' 카테고리의 다른 글
높은 트래픽 환경에서의 현재 재고 처리 구조 고민 (0) 2025.06.29 카프카 부하 분산: 동적 쓰로틀링과 캐시 (0) 2025.04.26 글또 10기 소감 (0) 2025.03.29 DB 장애 분석: Galera Cluster Segfault (1) 2025.01.31 개발자를 위한 커리어 관리 핸드북 (0) 2025.01.04