서비스를 운영하다 보면 “서버가 느려졌는데 원인을 모르겠다”는 상황을 자주 마주칩니다. CPU 사용률, 메모리, API 응답 시간 같은 시스템 메트릭을 실시간으로 확인할 수 있다면 장애 원인을 훨씬 빠르게 파악할 수 있습니다.
이번 글에서는 Prometheus + Grafana + Spring Actuator 조합으로 Spring Boot 애플리케이션의 시스템 메트릭을 수집하고 시각화하는 모니터링 환경을 구축한 경험을 공유합니다.
도입 배경
모니터링 시스템 도입을 통해 다음 네 가지 목표를 달성하고자 했습니다:
- 메트릭 가시화를 통한 성능 모니터링 기반 마련: CPU, 메모리, JVM 힙, GC 등 애플리케이션 핵심 메트릭을 실시간 대시보드로 시각화하여 시스템 상태를 즉시 파악할 수 있는 환경을 구성합니다.
- API 응답 속도(Latency) 분석 체계 구축: API 엔드포인트별 응답 시간, 요청 빈도, 에러율을 한눈에 확인할 수 있도록 정리하여 성능 병목 구간을 빠르게 식별하고 대응할 수 있는 기반을 마련합니다.
- 다중 서버 통합 모니터링: 여러 서버 인스턴스의 메트릭을 하나의 모니터링 시스템에서 수집하고, 서버 간 부하 분포를 비교·분석할 수 있는 통합 관제 환경을 구성합니다.
- 장애 조기 감지를 위한 확장 기반 마련: Grafana의 알림(Alert) 기능과 연계하여, 메트릭 임계값 초과 시 장애 징후를 조기에 감지할 수 있는 확장 기반을 마련합니다.
아키텍처
전체 흐름
각 컴포넌트의 역할
| 컴포넌트 | 역할 |
|---|---|
| Spring Actuator | 애플리케이션 메트릭 엔드포인트 노출 (/actuator/prometheus) |
| Micrometer | 메트릭을 Prometheus 포맷으로 변환하는 어댑터 |
| Prometheus | Pull 방식으로 메트릭을 주기적으로 수집 & 시계열 DB에 저장 |
| Grafana | 수집된 메트릭을 대시보드로 시각화, 임계값 기반 알림 설정 |
Pull 방식이란 Prometheus가 설정된 간격(기본 15초)마다 Spring 애플리케이션의 /actuator/prometheus 엔드포인트를 직접 호출하여 메트릭을 가져오는 구조입니다. 애플리케이션이 메트릭을 보내는 Push 방식과 달리, 모니터링 대상이 증가해도 Prometheus 설정만 추가하면 되므로 확장성이 좋습니다.
구현 가이드
Step 1: Spring Actuator + Micrometer 설정
먼저 Spring Boot 애플리케이션에 메트릭 수집 기능을 추가합니다.
build.gradle:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator:2.7.0'
implementation 'io.micrometer:micrometer-registry-prometheus:1.10.3'
}
application.yml:
management:
metrics:
tags:
application: my-api # Grafana 대시보드에서 애플리케이션 식별용
export:
prometheus:
enabled: true
enable:
http.server.requests: true
distribution:
percentiles-histogram:
http.server.requests: false
endpoints:
web:
exposure:
include: prometheus # prometheus 엔드포인트만 노출
설정 후 애플리케이션을 실행하면 http://localhost:8080/actuator/prometheus에서 메트릭을 확인할 수 있습니다.
Step 2: URI Cardinality Explosion 방지
Spring Actuator의 HTTP 메트릭은 기본적으로 요청 URI를 태그로 기록합니다. 그런데 /api/users/1, /api/users/2처럼 Path Variable이 포함된 URI는 각각 별도 메트릭으로 기록되어 Cardinality Explosion(메트릭 폭발)이 발생할 수 있습니다.
이를 방지하기 위해 정규화된 URI 템플릿(/api/users/{id})을 사용하도록 WebMvcTagsProvider를 커스터마이징합니다.
TimerConfig.java:
@Configuration
public class TimerConfig {
@Bean
public WebMvcTagsProvider webMvcTagsProvider() {
return new DefaultWebMvcTagsProvider() {
@Override
public Iterable<Tag> getTags(HttpServletRequest request,
HttpServletResponse response,
Object handler, Throwable exception) {
// URI Cardinality Explosion 방지를 위해 정규화된 URI 템플릿 사용
String uriTemplate = (String) request.getAttribute(
HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (uriTemplate == null) {
uriTemplate = request.getRequestURI(); // fallback
}
return List.of(
Tag.of("uri", uriTemplate),
Tag.of("method", request.getMethod()),
Tag.of("status", String.valueOf(response.getStatus())),
Tag.of("exception", exception == null
? "None" : exception.getClass().getSimpleName()),
Tag.of("outcome", response.getStatus() >= 200
&& response.getStatus() < 300 ? "SUCCESS" : "ERROR")
);
}
};
}
}
Step 3: Actuator 엔드포인트 보안 설정
Actuator 엔드포인트는 애플리케이션의 민감한 정보를 노출하므로, 반드시 접근 제어가 필요합니다.
SecurityConfig.java (내부 네트워크에서만 접근 허용):
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/actuator/**");
}
}
보안 주의: Spring Security에서
/actuator/**를 무시(ignoring)하더라도, 반드시 Nginx 등 웹 서버 레벨에서 외부 접근을 차단해야 합니다. Actuator 엔드포인트가 외부에 노출되면 중대한 보안 사고가 발생할 수 있습니다.{hostUrl}/actuator를 호출하면 현재 오픈된 Actuator 엔드포인트 목록을 확인할 수 있습니다.
Nginx - Actuator 엔드포인트 외부 차단:
location ~ ^/actuator(/.*)?$ {
deny all;
}
Prometheus는 내부 네트워크(Private IP)를 통해 직접 메트릭을 수집하므로, Nginx를 경유하지 않아 이 차단 규칙에 영향을 받지 않습니다.
Step 4: Prometheus + Grafana 설치 (Docker Compose)
모니터링 서버에 Prometheus와 Grafana를 Docker Compose로 설치합니다.
디렉토리 구조:
/opt/monitoring
├─ docker-compose.yml # Prometheus + Grafana 컨테이너 정의
├─ prometheus/
│ ├─ prometheus.yml # 수집 대상(targets), 주기 등 설정
│ └─ data/ # 시계열 메트릭 저장소 (볼륨 마운트)
└─ grafana/
├─ data/ # 대시보드, 사용자 설정 등 영구 저장소
└─ provisioning/
├─ dashboards/ # 대시보드 자동 프로비저닝 JSON
└─ datasources/ # 데이터 소스 자동 프로비저닝 YAML
Docker 설치 (Amazon Linux 2 기준):
sudo yum update -y
# Docker 설치
sudo amazon-linux-extras install docker -y
sudo service docker start
sudo usermod -aG docker ec2-user
# Docker Compose 설치
sudo curl -L \
https://github.com/docker/compose/releases/download/v2.29.2/docker-compose-linux-x86_64 \
-o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
docker-compose.yml:
services:
prometheus:
image: prom/prometheus:v2.54.0
container_name: prometheus
user: "1000:1000" # 호스트 사용자 권한으로 실행 (볼륨 권한 이슈 방지)
restart: always # 컨테이너 비정상 종료 시 자동 재시작
networks:
- monitoring-net
ports:
- "9094:9090" # 호스트 9094 → 컨테이너 9090 포트 매핑
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro # 설정 파일 (읽기 전용)
- ./prometheus/data:/prometheus # 메트릭 저장소
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=15d" # 메트릭 보관 기간
- "--storage.tsdb.retention.size=4GB" # 최대 저장 용량
- "--web.enable-admin-api" # 관리 API 활성화 (스냅샷, 삭제 등)
- "--web.console.libraries=/usr/share/prometheus/console_libraries"
- "--web.console.templates=/usr/share/prometheus/consoles"
grafana:
image: grafana/grafana:10.4.3
container_name: grafana
depends_on:
- prometheus # Prometheus 컨테이너 먼저 실행
user: "1000:1000"
restart: always
networks:
- monitoring-net
ports:
- "3004:3000" # 호스트 3004 → 컨테이너 3000 포트 매핑
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: <your-password>
GF_USERS_ALLOW_SIGN_UP: "false" # 외부 회원가입 차단
GF_SERVER_DOMAIN: "grafana.prod.local" # 리버스 프록시 도메인
volumes:
- ./grafana/data:/var/lib/grafana # 대시보드, 설정 영구 저장
- ./grafana/provisioning:/etc/grafana/provisioning # 자동 프로비저닝
networks:
monitoring-net:
driver: bridge # 컨테이너 간 내부 통신용 브릿지 네트워크
Step 5: Prometheus 수집 대상 설정
prometheus.yml:
global:
scrape_interval: 15s # 메트릭 수집 주기
scrape_timeout: 10s # 수집 타임아웃
evaluation_interval: 15s # 알림 규칙 평가 주기
scrape_configs:
- job_name: 'spring-actuator'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['10.0.1.10:8080'] # Server1 Private IP
labels:
instance: 'server1'
- targets: ['10.0.1.11:8080'] # Server2 Private IP
labels:
instance: 'server2'
targets에는 Spring Boot 애플리케이션의 Private IP를 지정합니다. Prometheus가 내부 네트워크를 통해 직접 /actuator/prometheus를 호출합니다.
Step 6: Docker Compose 실행
cd /opt/monitoring
docker-compose up -d
docker ps # 컨테이너 상태 확인
정상 실행 후 접속 확인:
- Prometheus:
http://<monitoring-server>:9094
- Grafana:
http://<monitoring-server>:3004
Step 7: Nginx 리버스 프록시 설정
외부에서 Prometheus와 Grafana에 접근할 때는 Nginx 리버스 프록시를 통해 IP 제한을 적용합니다.
grafana.conf:
server {
listen 3104;
server_name monitoring.example.com;
access_log /var/log/nginx/grafana/access.log main;
error_log /var/log/nginx/grafana/error.log;
# IP 제한 - 허용된 IP만 접근 가능
allow 10.0.0.0/8; # 사내 네트워크
allow 203.0.113.50; # VPN IP
deny all;
location / {
proxy_pass http://localhost:3004;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
prometheus.conf:
server {
listen 9194;
server_name monitoring.example.com;
access_log /var/log/nginx/prometheus/access.log main;
error_log /var/log/nginx/prometheus/error.log;
# IP 제한
allow 10.0.0.0/8;
allow 203.0.113.50;
deny all;
location / {
proxy_pass http://localhost:9094;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Step 8: Grafana 대시보드 설정
8-1. Data Source 연결
- Grafana 로그인 후 Configuration → Data Sources → Add data source 클릭
- Prometheus 선택
- URL에
http://prometheus:9094입력 (Docker 네트워크 내부 통신) - Save & Test 클릭하여 연결 확인
8-2. Dashboard Import
Grafana는 커뮤니티에서 만든 대시보드를 Import하여 바로 사용할 수 있습니다.
- Dashboards → Import 클릭
- Dashboard ID 입력 후 Load 클릭
- Data Source에서 앞서 추가한 Prometheus 선택
- Import 클릭
추천 대시보드:
| Dashboard | ID | 특징 |
|---|---|---|
| JVM Micrometer | 4701 | JVM 메모리, GC, 스레드 모니터링 |
| JustAI System Monitor | 11378 | 시스템 리소스 종합 모니터링 |
| Spring Boot Statistics | 12464 | HTTP 요청 통계, 응답 시간 분석 |
Step 9: 대시보드 모니터링
대시보드 Import가 완료되면 Prometheus에서 수집한 메트릭이 실시간으로 시각화됩니다. CPU 사용률, JVM 힙 메모리, GC 빈도, 스레드 수 등 주요 시스템 지표를 한 화면에서 확인할 수 있습니다.
또한 API 엔드포인트별 응답 속도(Latency) 리더보드를 통해, 어떤 API가 느린지 한눈에 파악할 수 있습니다. 응답 시간이 긴 API를 기준으로 정렬되므로 성능 병목 구간을 빠르게 식별하고 우선순위를 정하여 대응할 수 있습니다.
운영 팁
수집 메트릭 용량 관리
Prometheus는 시계열 데이터를 로컬 디스크에 저장하므로, 주기적으로 용량을 확인해야 합니다.
# 모니터링 디렉토리 용량 확인
sudo du -h --max-depth=1 ~/monitoring/
docker-compose.yml에서 설정한 retention.time=15d와 retention.size=4GB에 의해 오래된 데이터는 자동으로 정리됩니다.
컨테이너 관리
# 컨테이너 중지 및 제거
docker-compose down
# Grafana 데이터 초기화 (대시보드, 설정 포함)
rm -rf ./grafana/data/*
# 재시작
docker-compose up -d
정리
Prometheus + Grafana + Spring Actuator 조합은 Spring Boot 기반 서비스에서 가장 널리 사용되는 모니터링 구성입니다.
이 구성을 도입한 후 다음과 같은 효과를 얻을 수 있었습니다:
- 실시간 상태 파악: CPU, 메모리, JVM 힙, GC, HTTP 응답 시간을 대시보드에서 즉시 확인
- 다중 서버 비교: 여러 서버 인스턴스의 메트릭을 한 화면에서 비교하여 부하 불균형 감지
- 장애 선제 대응: Grafana 알림 기능으로 임계값 초과 시 사전 알림 가능
- 낮은 도입 비용: Actuator와 Micrometer는 별도 코드 추가 없이 주요 메트릭을 제공하며, Prometheus + Grafana는 오픈소스로 무료