Spring Boot Actuator로 운영 모니터링 구축하기
Actuator 엔드포인트를 활용해 애플리케이션 상태를 실시간으로 파악하고, Prometheus와 Grafana를 연동해 운영 대시보드를 구성한 실전 경험을 정리했습니다.

이전에 Sentry를 도입해 에러 추적을 자동화하면서 장애 대응 속도가 확실히 빨라졌습니다. 그런데 에러가 터진 뒤에 알림을 받는 것과, 에러가 터지기 전에 징후를 감지하는 건 완전히 다른 이야기였습니다.
어느 날 오후, 갑자기 API 응답 속도가 느려졌다는 제보가 들어왔습니다. Sentry에는 에러가 잡히지 않았고, 로그를 뒤져봐도 예외는 없었습니다. 결국 서버에 SSH로 접속해 top 명령어를 치고 나서야 힙 메모리(Heap Memory)가 90%를 넘긴 상태라는 걸 알게 됐습니다. GC(Garbage Collection)가 폭발적으로 돌면서 응답이 밀리고 있었던 겁니다.
"에러가 없는데 느려지는 상황"을 사전에 감지할 수 없을까 고민하다가, Spring Boot Actuator를 본격적으로 파고들게 됐습니다.
Actuator, 처음엔 /health 하나로 시작했습니다
Spring Boot Actuator는 애플리케이션의 내부 상태를 HTTP 엔드포인트로 노출해주는 도구입니다. 의존성 하나만 추가하면 바로 사용할 수 있습니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}처음에는 배포 후 서버가 정상 기동됐는지 확인하는 용도로 /actuator/health만 썼습니다. 무중단 배포 파이프라인에서 헬스 체크(Health Check) 용도로 호출하는 정도였는데, Nginx와 GitHub Actions로 무중단 배포를 구축할 때 이 엔드포인트가 큰 역할을 했습니다.
management:
endpoints:
web:
exposure:
include: health, info, metrics, prometheus
endpoint:
health:
show-details: when-authorizedshow-details: when-authorized로 설정하면 인증된 요청에 대해서만 디스크, DB 연결, Redis 연결 등 세부 상태를 보여줍니다. 운영 환경에서 민감한 정보가 무방비로 노출되는 걸 막으면서도, 필요할 때는 확인할 수 있는 균형점이었습니다.
{
"status": "UP",
"components": {
"db": { "status": "UP", "details": { "database": "MySQL" } },
"diskSpace": { "status": "UP", "details": { "total": 53687091200, "free": 31254896640 } },
"redis": { "status": "UP" }
}
}이걸 보고 나서야 "서버가 살아 있다"와 "서버의 모든 구성 요소가 정상이다"가 다른 의미라는 걸 인식하게 됐습니다.
/metrics로 숫자를 보기 시작하면서 달라진 것
/actuator/metrics는 JVM 메모리, HTTP 요청 횟수, 응답 시간, 커넥션 풀(Connection Pool) 상태 같은 수치 데이터를 제공합니다. 처음 이 엔드포인트를 열어봤을 때는 항목이 수백 개여서 어디를 봐야 할지 막막했습니다.
실무에서 자주 들여다보게 된 지표는 결국 몇 가지로 좁혀졌습니다.
| 지표 | 의미 | 왜 중요한가 |
|---|---|---|
jvm.memory.used | JVM 힙 메모리 사용량 | 메모리 누수 조기 감지 |
http.server.requests | HTTP 요청 수 + 응답 시간 | 트래픽 패턴 파악, 느린 API 식별 |
hikaricp.connections.active | 활성 DB 커넥션 수 | 커넥션 풀 고갈 경고 |
system.cpu.usage | 시스템 CPU 사용률 | 부하 이상 징후 감지 |
logback.events | 로그 레벨별 발생 횟수 | ERROR 로그 급증 감지 |
특히 hikaricp.connections.active는 이전에 DB 쿼리 로그를 운영에서 관찰하면서 느린 쿼리가 커넥션을 오래 붙잡고 있는 문제를 발견한 적이 있어서, 이 지표를 대시보드에 반드시 포함시키게 됐습니다.
/info로 빌드 버전을 확인하는 습관
/actuator/info는 보통 간과하기 쉬운 엔드포인트인데, build-info를 활성화해두면 현재 배포된 빌드 버전과 시각을 확인할 수 있습니다.
springBoot {
buildInfo()
}{
"build": {
"artifact": "my-service",
"version": "1.2.3",
"time": "2026-04-15T14:30:00Z"
}
}장애 상황에서 "지금 서버에 어느 버전이 올라가 있는 거야?"라는 질문에 SSH 접속 없이 바로 답할 수 있게 된 것만으로도 가치가 충분했습니다.
Prometheus 연동: 숫자를 시계열로 바꾸다
Actuator가 제공하는 메트릭(Metric)은 "지금 이 순간"의 스냅샷입니다. 하지만 운영에서 진짜 필요한 건 "3시간 전부터 지금까지 메모리가 어떻게 변했는가" 같은 시계열(Time Series) 데이터였습니다.
Prometheus(프로메테우스)는 주기적으로 /actuator/prometheus 엔드포인트를 긁어가서(Scrape) 시계열 DB에 저장해주는 모니터링 도구입니다. Micrometer(마이크로미터) 의존성을 추가하면 Actuator 메트릭이 Prometheus 형식으로 자동 변환됩니다.
dependencies {
implementation 'io.micrometer:micrometer-registry-prometheus'
}scrape_configs:
- job_name: 'my-service'
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
static_configs:
- targets: ['app-server:8080']설정 자체는 간단했지만, 처음에 한 가지 실수를 했습니다. scrape_interval을 5초로 설정했더니 Prometheus 저장 용량이 눈에 띄게 빠르게 차올랐습니다. 운영 서버의 메트릭 변화가 초 단위로 유의미한 경우는 거의 없었고, 15초 간격이면 충분했습니다. 이렇게 작은 설정 하나가 디스크 비용에 직접 영향을 준다는 걸 경험으로 배웠습니다.
보안: Actuator 엔드포인트 접근 제한
Actuator 엔드포인트를 외부에 그대로 노출하면 서버의 내부 상태가 다 드러납니다. 운영 환경에서는 반드시 접근을 제한해야 했습니다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/actuator/prometheus").hasIpAddress("10.0.0.0/8")
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
return http.build();
}/health는 로드밸런서의 헬스 체크를 위해 열어두고, /prometheus는 내부 네트워크 대역에서만 접근 가능하게 제한했습니다. 나머지 엔드포인트는 관리자 권한이 있어야만 볼 수 있도록 설정했습니다. 또 하나, Actuator 포트를 분리하는 방법도 있습니다.
management:
server:
port: 9090이렇게 하면 애플리케이션은 8080, 관리 엔드포인트는 9090으로 분리되어 방화벽 단에서 외부 접근을 원천 차단할 수 있습니다. 저는 포트 분리 방식을 선택했는데, Security 설정에 Actuator 관련 룰을 섞지 않아도 되니 설정이 깔끔해지는 장점이 있었습니다.
Grafana 대시보드 구성: 숫자에 의미를 입히다
Prometheus에 데이터가 쌓이기 시작했는데, PromQL(Prometheus Query Language)로 터미널에서 확인하는 건 여전히 불편했습니다. 이 시계열 데이터를 시각적으로 보여주는 역할을 Grafana(그라파나)가 해줍니다.
Grafana에서 Prometheus를 데이터 소스로 연결하고, 패널(Panel)을 하나씩 추가해가며 대시보드를 만들었습니다. 처음부터 화려한 대시보드를 만들려고 하기보다는, 앞서 정리한 핵심 지표 5개부터 시작했습니다.
jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} * 100rate(http_server_requests_seconds_count{status=~"5.."}[5m])
/ rate(http_server_requests_seconds_count[5m]) * 100알림 규칙 설정
대시보드를 만들었지만, 사람이 계속 모니터링 화면을 쳐다보고 있을 수는 없었습니다. Grafana의 Alert 기능으로 임계치(Threshold)를 설정하고 슬랙 알림을 연동했습니다.
| 알림 조건 | 임계치 | 이유 |
|---|---|---|
| 힙 메모리 사용률 | 85% 이상 5분 지속 | OOM 발생 전 선제 대응 |
| HTTP 5xx 에러율 | 1% 이상 3분 지속 | 서비스 장애 조기 감지 |
| 활성 DB 커넥션 | 풀 크기의 80% 이상 | 커넥션 고갈 전 경고 |
| CPU 사용률 | 80% 이상 10분 지속 | 비정상 부하 감지 |
처음에는 임계치를 너무 민감하게 잡아서 알림이 하루에도 몇 번씩 울렸습니다. 알림이 많아지면 결국 무시하게 되더라는 걸 체감한 뒤, "정말 사람이 개입해야 하는 수준"으로 기준을 올렸습니다. 운영 로그를 정리했던 경험에서 "양보다 질"이라는 원칙을 세웠는데, 알림에도 같은 원칙이 그대로 적용됐습니다.
커스텀 메트릭 추가: 비즈니스 지표도 모니터링하기
Actuator 기본 메트릭은 인프라 수준의 지표입니다. 하지만 운영하다 보면 "지난 1시간 동안 주문이 몇 건 들어왔는가" 같은 비즈니스 지표도 실시간으로 보고 싶어집니다.
Micrometer의 MeterRegistry를 주입받아 커스텀 메트릭을 등록할 수 있습니다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final MeterRegistry meterRegistry;
public OrderResponse createOrder(OrderRequest request) {
// 주문 처리 로직
Order order = processOrder(request);
// 주문 완료 카운터 증가
meterRegistry.counter("order.completed",
"paymentMethod", request.getPaymentMethod()
).increment();
// 주문 금액 분포 기록
meterRegistry.summary("order.amount")
.record(order.getTotalAmount());
return OrderResponse.from(order);
}
}이렇게 등록한 메트릭은 /actuator/prometheus에 자동으로 포함되어 Grafana에서 바로 조회할 수 있습니다. 비즈니스 지표와 인프라 지표를 한 화면에서 볼 수 있게 되니, "주문량이 급증하면서 DB 커넥션이 함께 치솟는" 같은 상관관계를 직관적으로 파악할 수 있게 됐습니다.
도입 전후 비교
| 항목 | 도입 전 | 도입 후 |
|---|---|---|
| 서버 상태 확인 | SSH 접속 후 top, free -m 수동 확인 | Grafana 대시보드에서 실시간 확인 |
| 장애 인지 시점 | 유저 제보 후 (사후 대응) | 임계치 알림으로 선제 감지 |
| 배포 후 확인 | 로그 파일 직접 확인 | /health, /info로 즉시 확인 |
| 성능 추이 파악 | 불가능 (스냅샷 데이터 없음) | 시계열 그래프로 추이 분석 |
| 장애 원인 파악 시간 | 평균 30분~1시간 | 평균 5~10분 |
가장 크게 달라진 건 "체감"이었습니다. 예전에는 장애가 나면 긴장부터 했는데, 대시보드가 있으니 현재 상황을 객관적인 숫자로 볼 수 있어서 침착하게 대응할 수 있게 됐습니다.
마무리
처음 Actuator를 접했을 때는 /health 엔드포인트 하나로 "서버 살아 있네" 정도만 확인하는 도구라고 생각했습니다. 하지만 메트릭을 수집하고, 시계열로 저장하고, 시각화하고, 알림을 거는 과정을 하나씩 붙여나가면서 모니터링이라는 것의 진짜 의미를 이해하게 됐습니다.
모니터링은 장애를 막는 기술이 아니라, 장애가 왔을 때 당황하지 않을 준비를 해두는 것이었습니다. 그리고 그 준비란 결국, 서버가 지금 어떤 상태인지를 "숫자"로 말할 수 있게 만드는 것이었습니다.
과하게 많은 알림은 오히려 감각을 무디게 만들었고, 지표는 적더라도 의미 있는 것만 골라 꾸준히 보는 편이 훨씬 나았습니다. 모니터링 도구를 도입하는 것보다, 어떤 숫자를 볼 것인지 기준을 세우는 과정이 더 중요했다는 걸 이번 작업을 통해 배웠습니다.