개요
팀에서 개발중인 웹 어플리케이션에 모니터링 환경을 구성했다.
이전부터 속도 이슈가 있거나 장애가 났을 때마다 모니터링 방법이 없는게 매우 매우 아쉬워왔기 때문에 필요성은 계속 있어왔다.
업무로 할당받은 내용은 아니었지만 개인적으로 공부도 할 겸, 최신 시스템 설계에서 적용할만한 것들이 있는지 찾다가 이건 유용하겠다 싶어서 구성해보기로 했다.
사실 모니터링 필요성은 사례를 들어 필요하다고 말하기에도 민망할 정도로 필요하다..
오히려 지금까지 모니터링 없이 어떻게 PDM 클라우드 서비스를 제공하고 있었는지가 더 놀라울 지경이다. 사용자가 많지 않아서 트래픽 문제가 없었기 때문에 아직까지 밝혀지지 않은 리스크 문제들도 있을거라고 생각한다. 하지만 트래픽 테스트 이전에 이걸 관측하기 위한 모니터링 환경이 필요하다.
도입 목적과 구성
모니터링 목적
시스템 개선을 위해 모니터링해야하는 몇가지 지표들이 있다.
- 성능 분석: HTTP 응답 시간, 서비스 메소드 실행 시간, 데이터베이스 쿼리 성능 측정
- 병목 지점 식별: 느린 엔드포인트, 서비스 메소드, 데이터베이스 쿼리 파악
- 리소스 관리: JVM 메모리, GC, CPU 사용률 모니터링으로 리소스 최적화
- 장애 감지: 에러율, 응답 시간 timeout 시 인지
모니터링 아키텍처
Prometheus + Grafana 선택 이유
모니터링 툴은 New Relic, Datadog, Elastic APM, Scouter(LG CNS), Pinpoint(Naver) 등 APM(Application Performance Monitoring)이 많이 쓰이는 것 같다. 난 Prometheus도 APM으로 구분될 줄 알았는데(많이들 그렇게 구분하는걸 봐오기도 했고..) 엄밀하게는 APM이 아니라 Metrics 기반 Observability/Monitoring 시스템 이라고 한다.
결과적으로는 Prometheus + Grafana로 모니터링 환경을 구성하기로 결정했다.
내 필요조건들에 가장 적합했기 때문인데 내가 필요했던 조건들은 아래와 같다.
- Java Spring 프레임워크에서 적용 가능할 것
- 오픈소스 일 것
- 보조적인 구성이기 때문에 모니터링 아키텍처가 없이도 기존 시스템 운영에는 전혀 영향이 없을 것
- 오버헤드가 적을 것
- 폐쇄망에서 구성 가능할 것
Prometheus 라이선스는 Apache v2.0라 매우 관대하고, 물론 Grafana는 AGPLv3라 주의가 필요하지만 Grafana를 수정하거나 자체 UI 기능을 붙이거나 하지 않고 있는 그대로 내부 관제용으로만 쓰면 문제가 없다고 한다. 어차피 Grafana의 자체 대쉬보드 기능으로 다 해결할거라 괜찮을 거라 생각한다.
또한 대부분의 APM들이 Push 방식 + 에이전트 전송인 것에 비해 Prometheus + Grafana는 Pulling 방식이라 Prometheus 서버가 죽어도 기존 시스템 운영에 영향을 주지 않는다. 그만큼 Push 방식에 비해 실시간성이 떨어진다고는 하는데 우리 시스템상에서 실시간 분석/알람이 필요한 쪽은 단순 시스템 헬스 체크와 가동률 기록이기 때문에 충분히 커버된다고 생각했다.
(그리고 다른 서비스 아키텍처들에서 많이 보이던 기술 스택이기 때문에 대중적인 구성을 한번 체험해보고 싶었다.)
모니터링 구성도
┌────────────────────────────────────┐
│ Spring Application │
│ ┌─────────────────────────────────┐ │
│ │ HTTP Layer (HttpMetricsInterceptor) │ │
│ │ - 요청 수, 응답 시간, 상태 코드 │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Service Layer (ServiceMetricsAspect - AOP) │ │
│ │ - 서비스 메소드 호출 수, 실행 시간 │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Database Layer (MetricsInterceptor - MyBatis) │ │
│ │ - 쿼리 실행 수, 실행 시간, 작업 유형 │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ JVM Metrics (Micrometer Binders) │ │
│ │ - 메모리, GC, 스레드, CPU │ │
│ └─────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────┐ │
│ │ Micrometer MeterRegistry │ │
│ │ - 메트릭 수집 및 변환 │ │
│ └─────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────┐ │
│ │ Prometheus Exposition Format │ │
│ │ Endpoint: /app/actuator/prometheus │ │
│ └─────────────────────────────────┘ │
└────────────────────────────────────┘
↓ (HTTP Scraping)
┌────────────────────────────────────┐
│ Prometheus │
│ - 메트릭 수집 (Scraping) │
│ - 시계열 데이터 저장 │
│ - PromQL 쿼리 엔진 │
└────────────────────────────────────┘
↓ (Query)
┌────────────────────────────────────┐
│ Grafana │
│ - 시각화 대시보드 │
│ - 알람 설정 │
│ - 데이터 분석 │
└────────────────────────────────────┘구성 요소별 역할
| 구성 요소 | 역할 | 필요성 |
|---|---|---|
| Spring Application | 메트릭 생성 및 노출 | 어플리케이션 내부 상태를 외부로 공개하기 위해 필요 |
| Micrometer | 메트릭 수집 추상화 라이브러리 | 다양한 모니터링 시스템과 호환되는 메트릭 생성 |
| Prometheus | 메트릭 저장 및 쿼리 엔진 | 시계열 데이터를 효율적으로 저장하고 검색 |
| Grafana | 시각화 대시보드 | 메트릭을 사람이 이해하기 쉬운 그래프로 표현 |
메트릭(Metric)
메트릭은 어플리케이션 상태와 성능을 수치로 나타낸 측정값이다. 시계열 데이터로 수집된다.
계층별 모니터링
타겟 시스템의 4개 계층에서 메트릭을 수집하려 한다.
1. HTTP Layer (웹 계층)
- 측정 대상: 사용자 HTTP 요청
- 수집 항목:
- 요청 처리 시간
- 요청 수 (URI, HTTP 메소드별)
- HTTP 상태 코드 (200, 404, 500 등)
- 성공/실패 여부
- 활용: 엔드포인트별 성능 분석, 에러율 추적, 사용자 경험 측정
2. Service Layer (비즈니스 로직 계층)
- 측정 대상: Spring
@Service클래스의 메소드 - 수집 항목:
- 메소드 실행 시간
- 메소드 호출 수 (클래스, 메소드별)
- 성공/실패 여부
- 활용: 비즈니스 로직 병목 지점 파악, 서비스별 사용 빈도 분석
3. Database Layer (데이터베이스 계층)
- 측정 대상: MyBatis 쿼리 실행
- 수집 항목:
- 쿼리 실행 시간
- 쿼리 실행 횟수 (Mapper, 작업 유형별)
- 작업 유형 (SELECT, INSERT, UPDATE, DELETE)
- 성공/실패 여부
- 활용: 느린 쿼리 식별, 데이터베이스 부하 분석, N+1 문제 탐지
4. JVM Layer (런타임 환경 계층)
- 측정 대상: Java Virtual Machine
- 수집 항목:
- Heap 메모리 사용량 (Used, Max)
- GC 발생 횟수 및 일시 정지 시간
- 스레드 수 (Live, Peak, Daemon)
- 클래스 로딩 수
- CPU 사용률 (Process, System)
- 활용: 메모리 누수 감지, GC 튜닝, 리소스 최적화
계층별 모니터링을 위해 필요한 라이브러리
JMX (Java Management Extensions)
Java 어플리케이션 모니터링 및 관리를 위한 표준 API로 JVM 내부 정보(메모리, 스레드, GC 등)를 MBean으로 노출시켜 JVM 레이어 모니터링을 가능하게 한다.
JMX 설정
Apache Tomcat 어플리케이션 서버의 보안 JMX 연결을 구성해야 한다.
JMX 라이브러리인 jmx_prometheus_javaagent-{버전}.jar를 다운받은 후 jmx_config.yaml를 구성한다.
startDelaySeconds: 0
ssl: false
lowercaseOutputName: true
lowercaseOutputLabelNames: true
# JMX 패턴 규칙
rules:
# JVM 기본
- pattern: 'java.lang:type=MemoryPool,name=(.*)'
- pattern: 'java.lang:type=GarbageCollector,name=(.*)'
- pattern: 'java.lang:type=Threading.*'
- pattern: 'java.lang:type=OperatingSystem.*'
# 톰캣 커넥터
- pattern: 'Catalina:type=ThreadPool,name="(.*)"'
- pattern: 'Catalina:type=GlobalRequestProcessor,name="(.*)"'
# JDBC 커넥션 풀(있는 경우에만)
- pattern: 'com.zaxxer.hikari:type=Pool \(.*\)'
이후 Tomcat의 setenv.bat에 아래와 같이 옵션을 추가한다.
setenv.bat
set "JAVA_OPTS=%JAVA_OPTS% -javaagent:{jmx 경로}\jmx_prometheus_javaagent-{버전}.jar=9404:{jmx config 경로}\jmx_config.yaml"setenv.sh
# JMX Exporter 설정
JMX_EXPORTER_PORT=9404
JMX_EXPORTER_JAR="{jmx 경로}/jmx_prometheus_javaagent-1.5.0.jar"
JMX_EXPORTER_CONFIG="{jmx config 경로}/jmx_config.yaml"
export CATALINA_OPTS="$CATALINA_OPTS -javaagent:${JMX_EXPORTER_JAR}=${JMX_EXPORTER_PORT}:${JMX_EXPORTER_CONFIG}"Micrometer
다양한 모니터링 시스템을 위한 메트릭 수집 추상화 라이브러리이다. JMX만으로는 HTTP 요청, 서비스 메소드, 데이터베이스 쿼리 같은 어플리케이션 레벨 메트릭을 수집할 수 없기 때문에 이 라이브러리의 도움을 받아야 한다.
- 어플리케이션 메트릭 수집: HTTP, Service, Database 계층 메트릭 생성
- JVM 메트릭 바인딩: JMX 정보를 Micrometer 형식으로 변환
- 다양한 모니터링 시스템 지원: Prometheus, Datadog, New Relic 등
- 간편한 API: Counter, Timer, Gauge 등 직관적인 메트릭 생성 API 제공
- 태그 기반 분류: 다차원 메트릭 분석 가능
의존성 추가
프로젝트의 build.gradle에 다음 의존성을 추가한다.
| 의존성 | 설명 | 필요성 |
|---|---|---|
| micrometer-core | Micrometer의 핵심 라이브러리 | Counter, Timer, Gauge 등 메트릭 생성 API 제공. JVM 메트릭 바인더 포함 |
| micrometer-registry-prometheus | Prometheus 형식 메트릭 변환기 | Micrometer 메트릭을 Prometheus exposition format으로 변환하여 /app/actuator/prometheus 엔드포인트에서 제공 |
| aspectjweaver | AspectJ 위빙 라이브러리 | Service 계층 메트릭 수집을 위한 AOP(Aspect-Oriented Programming) 지원. @Aspect 기반 메소드 인터셉션 가능 |
dependencies {
// Micrometer Core
implementation 'io.micrometer:micrometer-core:{버전}'
// Micrometer Prometheus Registry
implementation 'io.micrometer:micrometer-registry-prometheus:{버전}'
// AspectJ Weaver (AOP)
implementation 'org.aspectj:aspectjweaver:{버전}'
}의존성 버전 근거
micrometer-core와 micrometer-registry-prometheus는 둘다 현 버전 1.x 대에서는 전부 JDK 8 이후로는 지원한다고 해서 Java 버전 호환성도, aspectj와의 버전 종속성도 없다고 해서 자유롭게 쓸 수 있어보인다. 스냅샷은 1.16.0까지 나와있는데 안정적으로 쓰고 싶어서 1.15.x 버전 중 가장 최신인 1.15.6 버전을 쓰려고 한다.
처음에는 AI 픽이었던 1.9.17로 세팅했었는데 1.15.6으로 버전을 올리고 나니까 패키지명이
io.micrometer.prometheus에서io.micrometer.prometheusmetrics로 변경된 부분들이 있다. 유의하자.
그리고 우리 회사 프로젝트는 JDK 11을 쓰고있기 때문에, aspectj 버전은 1.9.20.1을 사용했다.
메트릭 수집 컴포넌트
Foundation 모듈 : Service 계층 + Database 계층
foundation/
└── src/
└── main/
├── java/
│ └── {패키지 경로}/spring/
│ ├── config/
│ │ └── MetricsConfig.java
│ ├── metrics/
│ │ └── ServiceMetricsAspect.java
│ └── mybatis/
│ └── MetricsInterceptor.java
└── resources/
└── root-context.xmlMetricsConfig.java
foundation/src/main/java/{패키지 경로}/spring/config/MetricsConfig.java
역할
- Micrometer의
MeterRegistry빈을 생성하여 전체 어플리케이션에서 메트릭을 수집할 수 있는 중앙 레지스트리 제공 - JVM 관련 메트릭을 자동으로 수집하도록 바인더 등록
- Prometheus 형식으로 메트릭을 노출하기 위한
PrometheusMeterRegistry설정
기능
PrometheusMeterRegistry빈 생성 (Prometheus 형식 메트릭 레지스트리)- JVM 메모리 메트릭 자동 수집 (
JvmMemoryMetrics) - GC 메트릭 자동 수집 (
JvmGcMetrics) - JVM 스레드 메트릭 자동 수집 (
JvmThreadMetrics) - 클래스로더 메트릭 자동 수집 (
ClassLoaderMetrics) - 프로세서 메트릭 자동 수집 (
ProcessorMetrics) @Timed애노테이션 지원을 위한TimedAspect등록
코드 예시
@Bean
public PrometheusMeterRegistry meterRegistry() {
PrometheusMeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
// JVM 메트릭 바인더 등록
new JvmMemoryMetrics().bindTo(registry);
new JvmGcMetrics().bindTo(registry);
new JvmThreadMetrics().bindTo(registry);
new ClassLoaderMetrics().bindTo(registry);
new ProcessorMetrics().bindTo(registry);
return registry;
}ServiceMetricsAspect.java
foundation/src/main/java/{패키지 경로}/spring/metrics/ServiceMetricsAspect.java
역할
- Spring
@Service애노테이션이 붙은 모든 비즈니스 로직 클래스의 메소드 실행을 자동으로 추적 - 서비스 계층의 성능 병목 지점을 식별하고 메소드별 호출 빈도 파악
- 각 서비스 메소드의 성공/실패 여부를 태그로 기록하여 에러율 추적
기능
@Service클래스의 모든 public 메소드 실행 시간 측정- 메소드별 호출 횟수 카운팅
- 클래스명, 메소드명, 성공 여부를 태그로 기록
- Histogram 활성화로 P50, P95, P99 백분위수 계산 지원
- 예외 발생 시 자동으로
success=false태그 추가
코드 예시
/**
* Intercept all public methods in classes annotated with @Service
* 서비스 어노테이션이 있는 서비스들의 pulic 메서드 인터셉터
*/
@Around("within(@org.springframework.stereotype.Service *) && execution(public * *(..))")
public Object measureServiceMethod(ProceedingJoinPoint pjp) throws Throwable {
String className = pjp.getTarget().getClass().getSimpleName();
String methodName = pjp.getSignature().getName();
Timer.Sample sample = Timer.start(meterRegistry);
boolean success = true;
try {
return pjp.proceed();
} catch (Throwable e) {
success = false;
throw e;
} finally {
sample.stop(Timer.builder("astra.service.method")
.description("Service layer method execution time")
.tag("class", className)
.tag("method", methodName)
.tag("success", String.valueOf(success))
.serviceLevelObjectives(
Duration.ofMillis(10),
Duration.ofMillis(50),
Duration.ofMillis(100),
Duration.ofMillis(500),
Duration.ofSeconds(1)
)
.publishPercentileHistogram()
.register(meterRegistry));
}
} @Service애노테이션이 붙은 클래스 내부의 모든 메소드를 타겟으로 지정
MetricsInterceptor.java (MyBatis)
foundation/src/main/java/{패키지 경로}/spring/mybatis/MetricsInterceptor.java
역할
- 모든 데이터베이스 쿼리의 실행 시간을 자동으로 추적
- 느린 쿼리를 식별하여 데이터베이스 성능 최적화 포인트 파악
- Mapper별, 작업 유형별(SELECT, INSERT, UPDATE, DELETE) 쿼리 실행 패턴 분석
- 데이터베이스 부하를 QPS(Query Per Second)로 모니터링
기능
- MyBatis의 모든 쿼리 실행 전/후 자동 인터셉션
- 쿼리 실행 시간 측정 (나노초 정밀도)
- SQL 명령어 유형 자동 파싱 (SELECT, INSERT, UPDATE, DELETE, UNKNOWN)
- Mapper 클래스명, 메소드명, 작업 유형, 성공 여부를 태그로 기록
- Histogram 활성화로 쿼리 응답 시간 분포 분석
- 예외 발생 시 자동으로
success=false태그 추가
인터셉트 대상
@Intercepts({
@Signature(type = Executor.class, method = "update", ...),
@Signature(type = Executor.class, method = "query", ...)
})- MyBatis
Executor의update(INSERT/UPDATE/DELETE)와query(SELECT) 메소드
유형 별 파싱 로직
private String extractOperation(String sql) {
sql = sql.trim().toUpperCase();
if (sql.startsWith("SELECT")) return "SELECT";
if (sql.startsWith("INSERT")) return "INSERT";
if (sql.startsWith("UPDATE")) return "UPDATE";
if (sql.startsWith("DELETE")) return "DELETE";
return "UNKNOWN";
}Web-App Module : HTTP 계층 메트릭 수집 + Prometheus 엔드포인트
web-app/
└── src/
└── main/
├── java/
│ └── {패키지 경로}/web/spring/
│ ├── config/
│ │ └── WebMvcConfig.java
│ ├── interceptor/
│ │ └── HttpMetricsInterceptor.java
│ └── mvc/
│ └── MetricsController.java
└── resources/
└── shiro.properties
HttpMetricsInterceptor.java
web-app/src/main/java/{패키지 경로}/web/spring/interceptor/HttpMetricsInterceptor.java
역할
- 모든 HTTP 요청의 응답 시간과 상태 코드를 추적하여 사용자 경험 측정
- URI별 트래픽 패턴 분석으로 인기 엔드포인트 파악
- HTTP 에러율(4xx, 5xx) 모니터링으로 장애 조기 감지
- 높은 카디널리티 방지를 위해 URI 정규화 적용 (ID 파라미터 제거)
기능
- Spring MVC
HandlerInterceptor를 통한 요청 전/후 처리 - HTTP 요청 처리 시간 측정 (밀리초 정밀도)
- URI 정규화:
/doc/view/12345→/doc/view/{id} - HTTP 메소드(GET, POST, PUT, DELETE), 상태 코드(200, 404, 500 등), 성공 여부를 태그로 기록
- Histogram 활성화로 응답 시간 백분위수 분석
- 예외 발생 시 자동으로
success=false태그 추가
URI 정규화 로직:
private String simplifyUri(String uri) {
// /doc/view/123 → /doc/view/{id}
// /biz/edit/456 → /biz/edit/{id}
uri = uri.replaceAll("/\\d+(/|$)", "/{id}$1");
// UUID 제거
uri = uri.replaceAll("/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(/|$)", "/{uuid}$1", Pattern.CASE_INSENSITIVE);
return uri;
}메트릭 시스템에서는 고유한 태그 조합마다 별도의 시계열 데이터가 생성되는데 ID가 포함된 URI를 그대로 수집하면 수백만 개의 시계열 데이터가 생성되어 메모리 고갈의 위험이 있다.
정규화를 통해 파라미터를 제외하고 하나로 통합한다.
MetricsController.java
web-app/src/main/java/{패키지 경로}/web/spring/mvc/MetricsController.java
역할
- Prometheus가 메트릭을 수집(Scraping)할 수 있도록 HTTP 엔드포인트 제공
- 어플리케이션 헬스 체크 엔드포인트 제공
- Micrometer가 수집한 모든 메트릭을 Prometheus exposition format으로 변환하여 노출
기능
/app/actuator/prometheus엔드포인트로 Prometheus 형식 메트릭 노출/app/actuator/health엔드포인트로 어플리케이션 상태 확인PrometheusMeterRegistry의scrape()메소드를 통해 현재 수집된 모든 메트릭 반환Content-Type: text/plain; version=0.0.4헤더로 Prometheus 표준 형식 맞춤
코드 예시
@GetMapping(value = "/prometheus", produces = "text/plain; version=0.0.4")
@ResponseBody
public String prometheus() {
return meterRegistry.scrape();
}노출되는 메트릭 형식 예시:
# HELP http_request_seconds HTTP request execution time
# TYPE http_request_seconds histogram
http_request_seconds_bucket{uri="/doc/list",method="GET",status="200",success="true",le="0.1"} 42.0
http_request_seconds_count{uri="/doc/list",method="GET",status="200",success="true"} 50.0
http_request_seconds_sum{uri="/doc/list",method="GET",status="200",success="true"} 1.234
WebMvcConfig.java
web-app/src/main/java/{패키지 경로}/web/spring/config/WebMvcConfig.java
역할
- Spring MVC 인터셉터 체인에
HttpMetricsInterceptor를 자동 등록 - 모든 HTTP 요청이 메트릭 인터셉터를 거치도록 설정
- 특정 경로를 인터셉터에서 제외할 수 있는 유연성 제공
기능
HttpMetricsInterceptor빈을 자동 주입하여 인터셉터 체인에 추가- 모든 경로(
/**)에 대해 메트릭 수집 활성화 - 필요시 제외 경로 패턴 설정 가능 (예:
/static/**,/app/actuator/**)
코드 예시
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(httpMetricsInterceptor)
.addPathPatterns("/**");
}Spring 설정 수정
메트릭 수집을 위해 Spring 설정 파일을 수정했다.
root-context.xml 수정
foundation/src/main/resources/root-context.xml
AOP 활성화
AOP(Aspect-Oriented Programming)는 어플리케이션의 핵심 로직과 부가 기능(로깅, 트랜잭션, 보안, 메트릭 수집 등)을 분리하여 모듈화하는 방법이라고 한다.
Service 계층의 메소드 실행 시간을 자동으로 측정하려면, 메소드 호출 전후에 실행 시간을 감싸는 형태의 로직이 필요한데 Spring AOP의 @Around 어노테이션을 사용하면 대상 메소드 실행 전후를 모두 가로채어 메소드 시작 시점과 종료 시점을 직접 제어할 수 있따고 한다.
이를 위해 AOP를 활성화 해 줄것이다.
<aop:aspectj-autoproxy />MetricsConfig 빈 등록
<context:component-scan base-package="{패키지 경로}.spring.config" />
<context:component-scan base-package="{패키지 경로}.spring.metrics" />MetricsConfig클래스의@Bean메소드들이 스캔되어야MeterRegistry빈 생성ServiceMetricsAspect클래스가 스캔되어야 AOP 어드바이스 등록- Component scan 없이는 메트릭 관련 빈들이 Spring 컨테이너에 등록되지 않음
MyBatis Configuration 블럭에 MetricsInterceptor 등록
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="plugins">
<array>
<ref bean="metricsInterceptor"/>
</array>
</property>
</bean>
<bean id="metricsInterceptor" class="{패키지 경로}.spring.mybatis.MetricsInterceptor">
<constructor-arg ref="meterRegistry"/>
</bean>- MyBatis는 플러그인(인터셉터) 메커니즘을 통해 쿼리 실행 전/후 로직 삽입 가능
SqlSessionFactory에 인터셉터를 등록하지 않으면 데이터베이스 쿼리 메트릭 수집 불가MeterRegistry를 생성자 주입하여 메트릭 기록 가능하게 설정
shiro.properties 수정
web-app/src/main/resources/shiro.properties
설정 추가
session.auth.filterUrl.{번호}=/app/actuator/**=anon- Prometheus는 인증 없이 메트릭 엔드포인트에 접근해야 함
- Apache Shiro 보안 필터가
/app/actuator/**경로를 차단하면 Prometheus가 메트릭 수집 불가 anon필터는 익명 접근을 허용하여 인증 없이 엔드포인트 접근 가능하게 설정
메트릭 엔드포인트는 민감한 정보(비즈니스 데이터)를 포함하지 않아서 anon으로 일단은 설정했는데, 필요하면 Prometheus에서 Basic Auth 설정이 가능하다고 한다.
Histogram 활성화
Histogram은 메트릭 값의 분포를 측정하는 메트릭 타입이다. 특정 구간(Bucket)별로 값을 분류하여 백분위수(Percentile)를 정확하게 계산할 수 있다고 한다. 기본 Timer로 대쉬보드를 보다가 평균만 확인하기에는 조금 아쉬움이 생겨서 메모리 사용량이 조금 늘어나더라도 측정 정확도를 높이고자 Histogram을 활성화 했다.
기본 Timer vs Histogram Timer 비교
| 항목 | 기본 Timer | Histogram Timer |
|---|---|---|
| 수집 데이터 | _count, _sum, _max | _count, _sum, _max, _bucket |
| 백분위수 (P50, P95, P99) | 부정확 | 정확 |
| 메모리 사용량 | 낮음 | 높음 (Bucket 수만큼) |
Histogram 기반 모니터링 이점
- SLA(Service Level Agreement) 모니터링
- “95%의 요청이 500ms 이내에 응답해야 한다”와 같은 SLA 준수 여부 확인 필요
- 평균만으로는 SLA 준수 여부를 정확히 판단할 수 없음
-
성능 병목 지점 정확한 식별
- P95, P99를 통해 대부분의 사용자가 아닌 일부 사용자가 겪는 문제 발견
- 느린 엔드포인트를 우선순위로 최적화
-
성능 분포 시각화
- Grafana에서 히트맵(Heatmap)으로 응답 시간 분포 표시 가능
- 특정 시간대에 응답 시간이 급격히 증가하는 패턴 파악
Histogram 활성화 방법
각 메트릭 수집 컴포넌트(ServiceMetricsAspect, HttpMetricsInterceptor, MetricsInterceptor)에서 Timer.builder()에 두 가지 설정을 추가하면 된다.
serviceLevelObjectives (SLO)
성능 목표 기준값을 나타내는 Bucket 구간을 설정한다.
설정된 SLO
| 계층 | SLO 구간 | 의미 |
|---|---|---|
| HTTP Layer | 100ms, 500ms, 1s, 3s, 5s | 사용자 요청 응답은 일반적으로 1~5초 이내 목표 |
| Service Layer | 10ms, 50ms, 100ms, 500ms, 1s | 비즈니스 로직은 100ms~1s 이내 목표 |
| Database Layer | 10ms, 50ms, 100ms, 500ms, 1s | 데이터베이스 쿼리는 50~100ms 이내 목표 |
코드 예시 (HttpMetricsInterceptor):
.serviceLevelObjectives(
Duration.ofMillis(100), // 0.1초
Duration.ofMillis(500), // 0.5초
Duration.ofSeconds(1), // 1초
Duration.ofSeconds(3), // 3초
Duration.ofSeconds(5) // 5초
)생성되는 Bucket 메트릭
http_request_seconds_bucket{le="0.1",...} 30.0 # 100ms 이하: 30건
http_request_seconds_bucket{le="0.5",...} 45.0 # 500ms 이하: 45건
http_request_seconds_bucket{le="1.0",...} 48.0 # 1초 이하: 48건
http_request_seconds_bucket{le="3.0",...} 50.0 # 3초 이하: 50건
http_request_seconds_bucket{le="5.0",...} 50.0 # 5초 이하: 50건
http_request_seconds_bucket{le="+Inf",...} 50.0 # 전체: 50건
publishPercentileHistogram
Histogram 수집을 활성화하여 Prometheus가 백분위수를 계산할 수 있도록 설정한다.
코드 예시:
.publishPercentileHistogram()효과
_bucket메트릭이 추가로 생성됨- Prometheus의
histogram_quantile()함수로 백분위수 계산 가능
메트릭 확인
어플리케이션을 실행한 후 메트릭 엔드포인트에 접속하여 수집된 메트릭을 확인할 수 있다.
메트릭 엔드포인트
http://[ip]:[port]/app/actuator/prometheus
출력 내용
브라우저나 curl 명령어로 접속하면 Prometheus exposition format으로 메트릭이 출력된다. 이 출력에서 다음을 확인할 수 있다.
- 수집되는 메트릭 목록: HTTP, Service, Database, JVM 계층별 메트릭 이름
- 메트릭 타입: Counter, Gauge, Histogram 등
- 태그 조합: 각 메트릭에 어떤 태그(uri, method, class 등)가 붙는지 확인
- 현재 측정값: 실시간으로 수집된 메트릭 값
확인 방법
- 브라우저에서 엔드포인트 URL 접속
# HELP로 시작하는 줄에서 메트릭 설명 확인# TYPE으로 시작하는 줄에서 메트릭 타입 확인- 메트릭 이름과 태그, 값이 포함된 줄에서 실제 데이터 확인
메트릭은 실제 요청/호출이 발생한 후에 생성된다.
어플리케이션 시작 직후에는 JVM 메트릭만 표시되고, HTTP/Service/Database 메트릭은 해당 계층이 사용된 후 나타난다.
Prometheus & Grafana 실행
Docker Compose를 사용하여 Prometheus와 Grafana를 간편하게 실행할 수 있다.
구조는 원하는대로 작성하면 되기는 하지만 나는 아래 구조로 docker용 폴더를 구성했다.
monitoring/
├── docker-compose.yml
├── prometheus/
│ └── prometheus.yml
└── grafana/
└── grafana.ini
docker-compose.yml 작성
version: "3.9"
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- "9090:9090"
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-storage:/var/lib/grafana
- ./grafana/grafana.ini:/etc/grafana/grafana.ini
depends_on:
- prometheus
restart: unless-stopped
volumes:
prometheus-data:
grafana-storage:prometheus.yml 작성
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# JMX Exporter
- job_name: 'tomcat-jmx'
scheme: http
static_configs:
- targets: ['{타겟 ip}']
metrics_path: '/metrics'
# Spring Application Metrics
- job_name: 'spring-application'
# localhost 인 경우
# static_configs:
# - targets: ['host.docker.internal:8080']
# 호스트가 다른 경우
scheme: https
static_configs:
- targets: ['{spring application url}:443']
metrics_path: '/app/actuator/prometheus'
scrape_interval: 15sscrape_interval: 15초마다 메트릭 수집 (필요시 5s, 10s로 조정 가능)targets: Spring 어플리케이션의 호스트와 포트- localhost:
host.docker.internal사용 - 다른 호스트인 경우: https 연결 시
{spring application url}:443사용
- localhost:
metrics_path: 메트릭 엔드포인트 경로 (/app/actuator/prometheus)
jmx는 http로만 서비스 되니 설정에 유의하자
grafana.ini 작성
[smtp]
enabled = true
host = smtp.gmail.com:587
user = your_email_id
password = your_app_password
from_address = your_email@gmail.com
from_name = Grafana Alert
starttls_policy = opportunisticGrafana에서 알림을 메일로 보내려면 메일 서버 세팅이 필요하다.
메일 서버는 그라파나의 설정에서 수정해야하는데 그라파나의 Setting에서는 수정이 불가능하고, grafana.ini 에 Docker가 뜰 때부터 설정을 해줘야한다.
메일 서버 정보를 본인 환경에 맞게 수정하자.
Prometheus 엔드포인트 확인
Prometheus가 Spring 어플리케이션에서 메트릭을 정상적으로 수집하는지 확인한다.
Prometheus UI 접속
브라우저에서 다음 URL로 접속한다. docker-compose에서 Prometheus 포트를 9090으로 띄웠기 때문에 해당 포트로 접근하면된다.
http://{ip}:9090
State 확인
상단 메뉴에서 Status 클릭 후 드롭다운에서 Targets 선택하면 연결 상태를 확인할 수 있다.

State가 UP이어야 정상 상태이다. 쿼리 페이지에서 쿼리를 날려 동작을 테스트 해 볼 수도 있다.
Grafana 데이터 소스 추가
Grafana에서 Prometheus를 데이터 소스로 추가하여 수집한 메트릭을 시각화한다.
Grafana 로그인
docker-compose에서 Grafana 포트를 3000으로 띄웠기 때문에 해당 포트로 접근하면된다.
http://localhost:3000
기본 로그인 정보(docker-compose에서 설정한 기본값)
- Username:
admin - Password:
admin
첫 로그인 시 비밀번호 변경을 요구하면 변경하거나 Skip 가능하다.
데이터 소스에 Prometheus 추가
- 좌측 사이드바에서 ⚙️ Configuration (톱니바퀴 아이콘) 클릭
- Data Sources 선택
- Add data source 버튼 클릭
- 데이터 소스 목록에서 Prometheus 클릭
- Name과 URL(Docker Compose 네트워크 내 Prometheus 주소) 입력
- Scrape interval은
15s로 prometheus.yml의 scrape_interval과 동일하게 설정
Grafana 대시보드 구성
Grafana에서 메트릭을 시각화하는 대시보드를 생성할 수 있다.
대시보드는 다른 사람이 만들어둔 대시보드 구성을 대시보드 ID로 Import 하거나, JSON 데이터를 토대로 Load 해서 적용할 수 있다.
한땀한땀 작성하는 것 보다는 기존에 잘 만들어져 있는 것에서 수정해 나가는 것이 유리하다.
Monitoring Dashboard JSON
완성된 대시보드 JSON을 Import 해서 붙여넣을 수 있다.
JSON이 너무 길어서 따로 링크를 걸었다.
Monitoring Dashboard JSON
수집되는 메트릭 목록
HTTP Layer
| 메트릭 이름 | 타입 | 설명 |
|---|---|---|
http_request_seconds_count | Counter | HTTP 요청 총 횟수 |
http_request_seconds_sum | Counter | HTTP 요청 총 실행 시간 (초) |
http_request_seconds_max | Gauge | HTTP 요청 최대 실행 시간 (초) |
http_request_seconds_bucket | Histogram | HTTP 요청 실행 시간 분포 (100ms, 500ms, 1s, 3s, 5s) |
Service Layer
| 메트릭 이름 | 타입 | 설명 |
|---|---|---|
service_method_seconds_count | Counter | 서비스 메소드 호출 총 횟수 |
service_method_seconds_sum | Counter | 서비스 메소드 총 실행 시간 (초) |
service_method_seconds_max | Gauge | 서비스 메소드 최대 실행 시간 (초) |
service_method_seconds_bucket | Histogram | 서비스 메소드 실행 시간 분포 (10ms, 50ms, 100ms, 500ms, 1s) |
Database Layer
| 메트릭 이름 | 타입 | 설명 |
|---|---|---|
mybatis_query_seconds_count | Counter | 데이터베이스 쿼리 총 실행 횟수 |
mybatis_query_seconds_sum | Counter | 데이터베이스 쿼리 총 실행 시간 (초) |
mybatis_query_seconds_max | Gauge | 데이터베이스 쿼리 최대 실행 시간 (초) |
mybatis_query_seconds_bucket | Histogram | 데이터베이스 쿼리 실행 시간 분포 (10ms, 50ms, 100ms, 500ms, 1s) |
JVM Metrics
| 메트릭 이름 | 타입 | 설명 |
|---|---|---|
jvm_memory_used_bytes | Gauge | JVM 메모리 사용량 (바이트) |
jvm_memory_max_bytes | Gauge | JVM 메모리 최대 크기 (바이트) |
jvm_memory_committed_bytes | Gauge | JVM 메모리 커밋 크기 (바이트) |
jvm_gc_pause_seconds_count | Counter | GC 발생 총 횟수 |
jvm_gc_pause_seconds_sum | Counter | GC 총 일시 정지 시간 (초) |
jvm_gc_pause_seconds_max | Gauge | GC 최대 일시 정지 시간 (초) |
jvm_threads_live_threads | Gauge | 현재 활성 스레드 수 |
jvm_threads_peak_threads | Gauge | 최대 스레드 수 |
jvm_threads_daemon_threads | Gauge | 데몬 스레드 수 |
jvm_classes_loaded_classes | Gauge | 현재 로드된 클래스 수 |
jvm_classes_unloaded_classes_total | Counter | 언로드된 클래스 총 수 |
process_cpu_usage | Gauge | JVM 프로세스 CPU 사용률 (0~1) |
system_cpu_usage | Gauge | 시스템 전체 CPU 사용률 (0~1) |
process_uptime_seconds | Gauge | JVM 프로세스 가동 시간 (초) |
