Spring Boot로 만든 서버를 실제 사용자에게 열어 두면(이걸 운영 환경이라고 부른다), 나는 그 서버가 지금 잘 돌고 있는지를 계속 지켜봐야 한다. 이 일을 모니터링이라고 하고, 요청 하나하나가 어디서 얼마나 걸렸는지까지 들여다보는 도구를 APM이라고 한다. 모니터링은 도구 하나로 끝나지 않고 「숫자를 꺼낸다 → 모아 둔다 → 그래프로 본다 → 이상하면 알린다」처럼 여러 도구가 줄줄이 이어져 돈다. 제목의 「체인」은 이 줄을 말한다.
그 줄의 맨 앞, 서버 안에서 숫자를 꺼내 주는 자리에 있는 것이 Actuator다. Actuator는 Spring Boot가 붙여 주는 부가 기능인데, 서버에 /actuator/health 같은 관리용 주소를 만들어 두고, 누가 그 주소로 HTTP 요청을 보내면 「이 서버는 지금 살아 있나」, 「메모리를 얼마나 쓰고 있나」 같은 내부 상태를 답으로 돌려준다.
하지만 Actuator 하나가 모니터링·알림·대시보드(여러 그래프를 한 화면에 모은 감시 화면)까지 다 해주는 것은 아니다. 이 글은 Actuator가 무엇을 제공하고 무엇을 제공하지 않는지를 분리해 정리한다.
토론 요약
- Actuator는 Spring Boot 앱 내부 상태를 HTTP 엔드포인트로 내보내는 진단 모듈이다. 여기서 「모듈」은 내가 의존성으로 추가하는(빌드 설정 파일에 한 줄 적는) 라이브러리를 말하고, 「진단」은 앱이 살아 있는지, 메모리와 요청 처리 시간이 어떤지 같은 상태를 바깥에서 들여다볼 수 있게 한다는 뜻이다.
- 메트릭(요청 수 · 메모리 사용량 같은 숫자)과 헬스(살아 있나, 괜찮나), 앱 정보를 내보낼 뿐, 그 숫자를 오래 저장하고, 골라 꺼내 보고(질의), 그래프로 그리고, 이상할 때 알리는 일은 하지 않는다. 그것은 Prometheus와 Grafana의 역할이다.
- 운영 환경에서는 보안이 중요하다. 헬스 상세에는 DB 종류나 디스크 경로 같은 서버 속사정이 나오고, 그런 정보는 공격하려는 사람에게 단서가 된다. 그래서 이런 주소는 인증(누가 보냈는지 확인)을 거치게 하거나, 아예 회사 내부 네트워크에서만 닿게 막거나, 보여 주는 양 자체를 줄여야 한다.
심화 설명: 제공하는 것 vs. 못 하는 것
Actuator가 제공하는 것 (제공)
아래 주소들은 브라우저로 직접 열어 볼 수도 있지만, 주로 Prometheus나 쿠버네티스 같은 다른 프로그램이 정해진 주기로 불러서 읽는다.
- 헬스:
/actuator/health가 서버 상태를 JSON으로 돌려준다. 맨 위에는 전체가 괜찮은지("status" : "UP")가 나오고, 그 아래에 DB 연결, 디스크 남은 자리 같은 항목별 검사 결과가 붙는다. 이 항목별 검사 하나하나를 HealthIndicator(인디케이터)라고 부른다. 쿠버네티스가 묻는 liveness(살아 있나)·readiness(요청 받을 준비가 됐나) 상태도 여기서 나온다(프로브 참고). 참고: Spring Boot 3.5.9 공식 문서의 health 엔드포인트 예시 링크. - 메트릭 목록/단건 조회:
/actuator/metrics는 내보낼 수 있는 메트릭 이름 목록을,/actuator/metrics/{name}은 그중 하나의 값을 보여 준다.{name}자리는 그대로 치는 것이 아니라 메트릭 이름으로 바꿔 넣는다. 예를 들어/actuator/metrics/jvm.memory.used는 JVM(자바 프로그램을 실행하는 프로그램)이 지금 쓰는 메모리 양이고,/actuator/metrics/http.server.requests는 이 서버가 받은 HTTP 요청의 횟수와 처리 시간이다. -
Prometheus 스크레이프 포맷: Prometheus는 정해 둔 주기마다 서버에 찾아와 숫자를 가져가는데, 이 일을 스크레이프라고 한다.
/actuator/prometheus는 모든 메트릭을 Prometheus가 읽는 글자 형식으로 한꺼번에 내놓는다. 공식 문서의 응답 예시는 이렇게 생겼다.# HELP jvm_memory_used_bytes The amount of used memory # TYPE jvm_memory_used_bytes gauge jvm_memory_used_bytes{area="heap",id="G1 Old Gen"} 8.7872E7앞에서 본
jvm.memory.used가 여기서는jvm_memory_used_bytes로 나온다. Prometheus로 내보낼 때는 이름이 Prometheus가 권하는 모양으로 바뀌기 때문이다. 공식 문서의 다른 예시에서도http.server.requests가 Prometheus에서는http_server_requests_duration_seconds로 나온다 — 점이 밑줄이 되고 단위가 뒤에 붙는다.# HELP줄은 이 메트릭의 설명,# TYPE줄은 종류다. 여기서gauge는 게이지, 곧 올라가기도 내려가기도 하는 「지금 값」이라는 뜻이다. 셋째 줄이 실제 값인데, 이름 뒤 중괄호 안이 라벨이다.area="heap"은 힙(객체가 들어가는 메모리 영역)을,id="G1 Old Gen"은 그 힙 안의 한 구역을 가리킨다. 그리고 맨 뒤 숫자(약 8,700만 바이트)가 그 순간의 값이다. 필요한 메트릭만 받고 싶으면 주소 뒤에?includedNames=jvm_memory_used_bytes처럼 쿼리 파라미터includedNames를 붙여 이름으로 거른다 링크. - 애플리케이션 정보:
/actuator/info는 빌드 정보, 버전, 그리고 application.yml 같은 설정 파일에info.app.encoding처럼info.로 시작하는 이름으로 내가 적어 둔 값들을 보여 준다. 다만 Spring Boot 3.5 공식 문서에 따르면 이info.값들은 기본으로 꺼져 있어서, 설정 파일에management.info.env.enabled를true로 적어야 나온다. - 선택적 진단 엔드포인트: 문제가 생겼을 때 쓰는 주소들도 있다.
/actuator/threaddump는 스레드 덤프(지금 각 스레드가 어디를 실행 중인지)를,/actuator/loggers는 로거 설정을 보여 주고 바꾸게 하고,/actuator/heapdump는 힙 덤프 파일을 내려 준다. 이런 주소는 만들어져 있어도 밖에서 부를 수 있게 열려 있지 않다. 밖에서 부를 수 있게 여는 것을 「노출」이라고 하고, 바로 아래 항목의 설정으로 한다. - 노출 범위 제어: 설정 파일에
management.endpoints.web.exposure.include(열 것)와exclude(닫을 것)로 HTTP로 열 엔드포인트를 정한다. 점(.)으로 이어 쓴 이 이름은 YAML에서는 들여쓰기 계층으로 적는다. 아래 「실무 적용 예제 1」의 YAML이 바로 이 설정이다. Spring Boot 3.5 공식 문서에 따르면 아무것도 적지 않으면 HTTP로는health하나만 열린다(문서는 HTTP와 함께 JMX라는 자바 관리용 통로도 말하는데, 이 글은 HTTP만 다룬다). 전체 공개 예시 링크.
Actuator가 제공하지 않는 것 (미제공)
Actuator는 부르는 그 순간의 값을 내놓는 곳이지, 시간마다 값을 기록해 쌓아 두는 저장소가 아니다. 그래서 「어제 오후 3시에 메모리가 얼마였나」는 Actuator에 물어볼 수 없다. 아래 첫 세 항목(보관·시각화·알림)이 이 한계에서 나온다.
- 장기 보관·질의: 시계열(시간 순서대로 쌓인 숫자) 저장소와, 거기서 원하는 구간을 골라 꺼내는 쿼리는 하지 않는다 → Prometheus, VictoriaMetrics 같은 시계열 저장소가 필요하다.
- 시각화·대시보드: 대시보드/그래프/탐색은 제공하지 않는다 → Grafana 등이 필요하다.
- 알림: 「메모리가 90%를 넘으면」 같은 임계치(정해 둔 경계값)를 넘었을 때 알람을 보내지 않는다 → Prometheus Alertmanager나 Grafana Alerting(Grafana가 가진 알림 기능) 이 필요하다.
- 강제 인증/인가: Actuator 자체에는 보안 계층이 없다. 인증(누구인가)과 인가(그 사람이 이걸 봐도 되는가)는 Spring Security나, 앱 바깥의 네트워크 설정(방화벽 등)으로 막아야 한다. 참고로 Spring Boot 3.5 공식 문서에 따르면 Spring Security가 함께 들어 있고 내가 따로 보안 설정을 만들지 않았다면, Spring Boot가
/health를 뺀 나머지 Actuator 주소를 자동으로 막는다. 내가 보안 설정(아래 예제 2의SecurityFilterChain)을 만들면 그 자동 설정은 물러나고 내 설정이 전부를 정한다. - 애플리케이션 코드 수정 없이 모든 진단을 자동 제공: 「오늘 들어온 주문 수」 같은 우리 서비스만의 숫자(커스텀 비즈니스 메트릭)나, 「결제 회사 서버에 연결되나」 같은 우리 서비스만의 헬스 검사(도메인 헬스)는 Actuator가 알 수 없으니 내가 직접 코드로 등록해야 한다. 헬스 쪽은 아래 「예제 4」에 코드가 있다.
왜 이런 분리가 중요한가?
- 운영하는 사람 관점: Actuator는 숫자가 나오는 출발점(데이터 소스)일 뿐이다. 숫자가 「수집 → 저장 → 시각화 → 알림」 순서로 흘러가는 길(파이프라인)을 단계별로 다른 도구에 맡겨야 어디가 고장 났는지 바로 짚인다. 그래프가 비었을 때 Actuator 주소를 열어 숫자가 나오면 문제는 그 뒤 단계라는 식이다.
- 보안 관점: 헬스 상세나 환경 정보(설정값을 보여 주는
/actuator/env엔드포인트가 따로 있다)는 공격 표면(공격자가 건드려 볼 수 있는 곳)이 될 수 있다. 운영 환경에서는show-details: when-authorized(인증된 사람에게만 상세를 보여 준다, 예제 1 참고)와 네트워크 제어를 권장한다. - 성능 관점: Prometheus는 풀 방식이다. 앱이 숫자를 보내는 게 아니라 Prometheus가 주기마다
/actuator/prometheus를 부르러 온다. 그래서 그 주기를 짧게 잡을수록 앱이 응답을 만드는 횟수가 늘어 서버에 드는 일(부하)이 늘 수 있다. 한 번 응답 자체는 가볍지만, 라벨 값이 끝없이 많아지면 메트릭 한 개가 수많은 숫자 줄로 갈라진다(카디널리티 폭발). 예를 들어 요청 수 메트릭에 사용자 ID를 라벨로 달면 사용자 수만큼 숫자 줄이 생긴다. Spring 쪽에서는 메트릭을 만들 때 이름과 함께 「이름=값」 꼬리표를 붙이는데 이것을 태그라고 부르고, Prometheus로 나가면 그대로 라벨이 된다(위 예시의area="heap"이 그렇게 나온 것이다). 그래서 태그는 값의 종류가 많지 않게 설계해야 한다.
실무 적용 예제
1) 최소 노출 + Prometheus 스크레이프만 허용
아래는 Spring Boot 앱의 설정 파일(application.yml)에 적는 내용이다. YAML은 들여쓰기로 계층을 나타내므로, 이 블록은 management.endpoints.web.exposure.include와 management.endpoint.health.show-details 두 설정을 적은 것이다.
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
endpoint:
health:
show-details: when-authorized
include: health,metrics,prometheus— HTTP로 열 엔드포인트를 쉼표로 나열한다. 여기 없는info,threaddump,heapdump등은 열리지 않는다.show-details: when-authorized— 헬스 응답의 항목별 상세를 인증된 사용자에게만 보여 준다. 공식 문서에 적힌 값은never(기본값, 상세를 보여 주지 않는다),when-authorized,always(누구에게나 보여 준다) 셋이다. 상세를 못 보는 사람에게는 전체 상태(UP/DOWN)만 간다.
활용: 쿠버네티스는 /actuator/health를 프로브로 주기적으로 불러, 앱이 죽었으면 다시 띄우고 준비가 안 됐으면 요청을 보내지 않는다. Prometheus는 /actuator/prometheus만 스크레이프한다. 앞에서 본 /actuator/metrics는 사람이 직접 들여다보는 용도로만 열어 두고, 인증을 붙인다. 이 YAML만으로는 인증이 붙지 않는다. 인증은 다음 예제 2의 코드가 붙인다.
2) 필수 보안 스니펫 (Spring Security 예시)
스니펫은 그대로 가져다 붙일 수 있는 짧은 코드 조각이다. 아래 코드는 Spring Security를 쓰는 앱에서, /actuator/ 아래 주소 중 헬스와 프로메테우스만 누구나 열게 하고 나머지는 로그인을 요구한다.
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class ActuatorSecurity {
@Bean
SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/actuator/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health", "/actuator/prometheus").permitAll()
.anyRequest().authenticated()
)
.httpBasic(withDefaults());
return http.build();
}
}
import static ...Customizer.withDefaults;— 보통import는 클래스를 가져오지만,import static은 클래스 안의 static 메서드를 가져와서 클래스 이름 없이withDefaults()라고만 부르게 해 준다.withDefaults()는 「아무것도 바꾸지 말고 기본값 그대로 써라」는 뜻의 빈 설정 묶음을 돌려준다.@Configuration— 이 클래스가 Spring 설정 클래스라는 표시다. Spring이 켜질 때 이 클래스를 찾아 안의@Bean메서드를 부른다.@Bean— 이 메서드가 돌려준 객체를 Spring이 빈으로 들고 있게 한다(@Bean). 그래서 이 메서드는 내가 부르지 않고 Spring이 부르고, 매개변수HttpSecurity http도 Spring이 만들어 넘겨준다.HttpSecurity는 보안 규칙을 하나씩 쌓아 가는 설정 도구이고, 돌려주는SecurityFilterChain은 그 규칙으로 만든 필터 체인(요청이 Controller(요청을 제일 먼저 받는 창구 클래스)에 닿기 전에 차례로 거치는 검사 줄)이다.http다음 줄부터.으로 시작하는 줄이 이어지는 모양 —http.securityMatcher(...)가http자신을 다시 돌려주기 때문에, 그 결과에 바로.authorizeHttpRequests(...)를 붙여 부를 수 있다. 메서드 세 개를 한 줄로 이어 부른 것(메서드 체이닝)을 읽기 좋게 줄만 바꿔 적은 것이다..securityMatcher("/actuator/**")— 이 규칙 묶음을/actuator/로 시작하는 주소에만 적용한다.**는 「그 아래 어떤 경로든」이라는 뜻이다..authorizeHttpRequests(auth -> auth ...)— 주소별로 누가 들어올 수 있는지 정한다.auth -> auth...는 람다다. Spring Security가 규칙을 적을 객체를auth로 넘겨주면, 그 위에 규칙을 이어 적는다..requestMatchers("/actuator/health", "/actuator/prometheus").permitAll()— 이 두 주소는 로그인 없이 누구나 들어온다(permitAll)..anyRequest().authenticated()— 위에서 말하지 않은 나머지/actuator/주소는 인증된 사람만 들어온다..httpBasic(withDefaults())— 인증 방법으로 HTTP Basic 인증(요청마다 아이디·비밀번호를 헤더에 담아 보내는 방식)을 기본 설정으로 켠다. 인자 없는.httpBasic()은 Spring Security 6.5 API 문서에 7.0에서 지울 예정(deprecated)이라고 적혀 있어서, 공식 문서 예시처럼withDefaults()를 넘겼다.return http.build();— 지금까지 쌓은 규칙으로 필터 체인을 만들어 Spring에 돌려준다.
활용: 헬스/프로메테우스는 쿠버네티스 프로브와 Prometheus가 로그인 없이 불러야 하므로 열어 두고, 나머지는 인증을 요구한다. 앞에서 「보호하라」고 한 것과 어긋나 보이지만, 예제 1에서 헬스의 상세는 인증된 사람에게만 보이게 했으므로 로그인 없이 부르는 쪽은 UP/DOWN만 받는다. /actuator/prometheus까지 열어 두는 이 선택은 그 주소가 바깥 인터넷에서는 닿지 않고, Prometheus가 있는 회사 내부 네트워크에서만 닿게 막는다는 전제에 기대고 있다. 그래야 Prometheus는 로그인 없이 가져가고 바깥 사람은 아예 닿지 못한다.
3) Prometheus 설정 예시 (pull 대상 등록)
이 파일은 Spring 앱 쪽이 아니라 Prometheus 서버 쪽 설정 파일(보통 prometheus.yml)이다. 「어느 서버에, 몇 초마다, 어느 주소로 숫자를 가지러 갈지」를 Prometheus에게 알려 준다.
scrape_configs:
- job_name: 'kamis-app'
scrape_interval: 15s
metrics_path: /actuator/prometheus
static_configs:
- targets: ['kamis.example.com:8080']
labels:
application: 'kamis'
YAML에서 줄 앞의 - (빼기와 빈칸)는 목록의 한 칸을 뜻하고, ['...']처럼 대괄호 안에 쉼표로 나열한 것도 목록이다. 그래서 scrape_configs 아래에는 job을 여러 개, targets에는 서버 주소를 여러 개 적을 수 있다.
scrape_configs— 스크레이프할 대상 목록이 여기 들어간다.job_name: 'kamis-app'— 이 대상 묶음의 이름이다(job).kamis는 이 예시에 쓴 서비스 이름일 뿐 특별한 뜻은 없다.scrape_interval: 15s— 15초마다 한 번씩 숫자를 가지러 간다.metrics_path: /actuator/prometheus— 가지러 갈 주소다. 앞에서 본 Actuator의 Prometheus 엔드포인트다.targets: ['kamis.example.com:8080']— 숫자를 가져올 서버의 주소다. 뒤의:8080은 그 서버 안에서 Spring 앱이 듣고 있는 포트 번호다.labels: application: 'kamis'— 이 서버에서 가져온 모든 숫자에application="kamis"라벨을 붙인다.
활용: 이렇게 붙인 라벨로 대시보드와 알람에서 「kamis 서비스의 숫자만」 골라 볼 수 있다. 앞의 「성능 관점」에서 말했듯 Spring 쪽 태그도 Prometheus로 넘어오면 라벨이 되니, 태그와 라벨은 같은 꼬리표를 Spring과 Prometheus가 각각 부르는 이름이다.
4) 헬스 인디케이터 커스터마이징 개요
- 기본: DB 연결이 되는지(
db), 디스크 남은 자리가 적지 않은지(diskSpace), 늘UP을 답하는ping같은 검사는 Spring Boot가 앱이 켜질 때 알아서 붙인다. -
커스텀: HealthIndicator 인터페이스를 구현한 클래스를 빈으로 등록하면 내 검사가 하나 더 붙는다. 아래는 Spring Boot 3.5 공식 문서의 예시다.
@Component public class MyHealthIndicator implements HealthIndicator { @Override public Health health() { int errorCode = check(); if (errorCode != 0) { return Health.down().withDetail("Error Code", errorCode).build(); } return Health.up().build(); } private int check() { // perform some specific health check return ... } }@Component— 이 클래스를 통째로 빈으로 만들어 달라는 표시다. 예제 2의@Bean이 「메서드가 만든 객체」를 빈으로 등록했다면, 이것은 「이 클래스」를 Spring이 직접 객체로 만들어 빈으로 등록한다. 둘 다 빈이 된다는 결과는 같다.implements HealthIndicator— Actuator가 정해 둔 인터페이스를 구현한다. 이 인터페이스의 약속은health()메서드 하나다. Actuator는 이 인터페이스를 구현한 빈을 모두 찾아/actuator/health를 만들 때 하나씩 부른다.Health health()— 검사 결과를 돌려준다.Health는 상태(UP/DOWN)와 덧붙일 상세 정보를 담는 결과 객체다.int errorCode = check();— 실제 검사는check()에 맡긴다. 예시라서 본문은 비워 두었고(return ...는 그대로는 컴파일되지 않는 자리 표시다), 실제로는 여기에 「결제 회사 서버에 연결되나」 같은 내 검사를 적는다.Health.down().withDetail("Error Code", errorCode).build()— 문제가 있으면 상태를DOWN으로 하고, 상세에 오류 코드를 붙여 결과를 만든다.Health.up().build()— 문제가 없으면UP을 돌려준다.
공식 문서에 따르면 이 검사는 빈 이름에서
HealthIndicator를 뗀 이름, 즉my라는 항목으로 헬스 응답에 붙는다. - 보안:
show-details를never나when-authorized로 두어 아무나 상세를 보지 못하게 한다.
흐름 한눈에 보기
아래 그림은 지금까지 본 도구들이 어떻게 이어지는지를 보여 준다. Actuator가 숫자를 내놓으면 Prometheus가 가져가 쌓고 알림 규칙을 검사하고, Grafana가 그 숫자를 그려 보인다. 따로 쿠버네티스(그림의 K8s는 쿠버네티스를 줄여 쓴 이름이다)나 로드밸런서(요청을 여러 서버에 나눠 주는 장치)는 헬스 주소를 불러 이 서버로 요청을 보내도 되는지 판단한다.
「알림 룰」은 앞의 「알림」 항목에서 말한 「이 숫자가 이 경계를 넘으면 알린다」는 조건이다. Prometheus가 그 조건을 검사하고, 걸리면 Alertmanager가 사람에게 알린다.
결론/팁
- Actuator는 데이터 제공자다. 저장·질의·시각화·알림은 별도 도구(프로메테우스/그라파나)가 담당한다.
- 운영에서는 보안 설정이 최우선:
show-details최소화, 인증/네트워크로 보호. - 라벨(태그) 설계를 신중히:
application처럼 공통 라벨을 넣어 대시보드·알림 필터를 쉽게 만든다. - 스크레이프 주기와 엔드포인트 부담을 균형 있게 잡고, 필요 시
includedNames로 한 번에 주고받는 양(트래픽)을 줄인다. 쓰는 법은 위 「Prometheus 스크레이프 포맷」 항목에 있다.
여기까지 Actuator 편이다. Prometheus/Grafana 편에서 “그다음 단계”를 이어 정리할 예정인데, 추가로 궁금한 포인트나 강조하고 싶은 사례가 있다면 알려 달라.
자신만의 철학을 만들어가는 중입니다.
댓글남기기