새벽 4시 7분. 전화벨이 울렸다. '서버 죽었어요.' 로그인해서 확인하니 CPU 100%, 메모리 풀. 원인 파악에 2시간 걸렸다. 모니터링 시스템이 있었다면? 아마 30분 전에 알림을 받고 잠을 깨지 않았을 것이다. 이건 그 새벽 이후에 구축한 모니터링 시스템의 이야기다.
아키텍처 개요
모니터링 시스템의 데이터 흐름은 다음과 같습니다:
Spring Boot App (Actuator + Micrometer)
↓ /actuator/prometheus
Prometheus (메트릭 수집 & 저장)
↓
Grafana (시각화 & 알림)
Spring Boot 설정
의존성 추가
// build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")
}
application.yml 설정
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
endpoint:
health:
show-details: always
prometheus:
enabled: true
metrics:
tags:
application: ${spring.application.name}
environment: ${SPRING_PROFILES_ACTIVE:local}
여기서 중요한 포인트는 metrics.tags입니다. 환경별(dev, staging, prod)로 메트릭을 구분하기 위해 환경변수를 통해 태그를 주입합니다.
Standalone Tomcat 환경 설정
WAR로 배포하는 경우, Tomcat의 setenv.sh에서 환경변수를 설정합니다:
# $CATALINA_BASE/bin/setenv.sh
export SPRING_PROFILES_ACTIVE=production
export JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=production"
이렇게 하면 Grafana에서 환경별로 메트릭을 필터링할 수 있습니다.
Prometheus 설정
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['app-server:8080']
relabel_configs:
- source_labels: [__address__]
target_label: instance
regex: '(.+):\d+'
replacement: '${1}'
Grafana 대시보드 구성
Grafana 대시보드 ID 19004를 추천합니다. Spring Boot 3.x에 최적화된 대시보드입니다.
주요 모니터링 패널
- JVM 메모리: Heap/Non-Heap 사용량
- GC 현황: GC 횟수, 소요 시간
- HTTP 요청: 요청 수, 응답 시간, 에러율
- 스레드: 활성 스레드 수, 스레드 풀 상태
- DB 커넥션: HikariCP 풀 상태
환경별 필터링 쿼리
# Production 환경의 HTTP 요청 수
sum(rate(http_server_requests_seconds_count{environment="production"}[5m]))
커스텀 메트릭 추가
비즈니스 로직에 맞는 커스텀 메트릭을 추가할 수 있습니다:
@Component
public class OrderMetrics {
private final Counter orderCounter;
private final Timer orderProcessingTimer;
public OrderMetrics(MeterRegistry registry) {
this.orderCounter = Counter.builder("orders.created")
.description("Number of orders created")
.register(registry);
this.orderProcessingTimer = Timer.builder("orders.processing.time")
.description("Order processing time")
.register(registry);
}
public void recordOrder() {
orderCounter.increment();
}
public void recordProcessingTime(Runnable task) {
orderProcessingTimer.record(task);
}
}
알림 설정
Grafana에서 알림 규칙을 설정합니다:
- 에러율 5% 초과 시 Slack 알림
- 응답시간 P99 > 3초 시 이메일 알림
- Heap 사용률 85% 초과 시 경고
트러블슈팅
메트릭이 수집되지 않는 경우
- Actuator 엔드포인트 노출 확인:
/actuator접근 가능 여부 - Prometheus 타겟 상태 확인: Prometheus UI의 Targets 메뉴
- 방화벽 설정 확인: Prometheus → App 서버 간 통신
환경 태그가 적용되지 않는 경우
WAR 배포 환경에서 환경변수가 제대로 전달되지 않을 수 있습니다. CATALINA_OPTS 대신 JAVA_OPTS를 사용하거나, catalina.properties에 직접 설정합니다.
모니터링 시스템이 장애를 막아준 실제 사례
위에서 공유한 경험 이후로 팀에서는 모니터링 알림 규칙을 대폭 강화했습니다. 단순히 시스템 메트릭만 감시하는 것이 아니라, 배포 직후 30분간은 알림 임계값을 더 낮게 설정하는 "배포 감시 모드"를 도입했습니다. 이 덕분에 비슷한 유형의 문제를 3건 더 사전에 탐지할 수 있었습니다.
모니터링 시스템 구축은 선택이 아닌 필수입니다. 대시보드를 만드는 것 자체보다, 어떤 메트릭을 감시하고 어떤 임계값에서 알림을 받을지를 설계하는 것이 핵심입니다. 특히 DB 커넥션 풀, JVM 메모리, 에러율 이 세 가지는 반드시 알림을 설정해두시길 강력히 추천합니다.
10년차 풀스택 개발자. Spring Boot, Flutter, AI 등 실무 경험을 기록합니다.
GitHub →
💬 댓글