Spring Boot Actuator란?
Spring Boot Actuator는 애플리케이션의 상태, 메트릭, 환경 정보를 HTTP 엔드포인트로 노출하는 프로덕션 준비 기능입니다. 단순한 헬스체크를 넘어 커스텀 메트릭 수집, 런타임 설정 변경, 스레드 덤프, HTTP 트레이스까지 운영에 필요한 모든 관측 기능을 제공합니다.
Kubernetes 환경에서 liveness/readiness probe로 Actuator 엔드포인트를 사용하는 것은 기본이고, Prometheus + Grafana 모니터링 스택과의 통합까지 이 글에서 심화 분석합니다.
의존성과 기본 설정
// build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
// Prometheus 메트릭 노출
implementation("io.micrometer:micrometer-registry-prometheus")
}
// application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,env,loggers,threaddump
base-path: /actuator # 기본 경로
endpoint:
health:
show-details: when_authorized # 인증된 사용자에게만 상세 정보
show-components: when_authorized
env:
show-values: when_authorized
server:
port: 8081 # 관리 포트 분리 (보안)
info:
env:
enabled: true
보안 핵심: Actuator 엔드포인트를 애플리케이션 포트(8080)와 분리하여 management.server.port: 8081로 설정하면, 외부 로드밸런서에서 관리 포트를 차단하면서 내부 모니터링 시스템만 접근할 수 있습니다.
커스텀 Health Indicator
기본 제공되는 DB, Redis, Disk 헬스체크 외에 비즈니스 로직 기반 커스텀 헬스체크를 구현합니다:
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
private final RestClient restClient;
private final CircuitBreaker circuitBreaker;
public ExternalApiHealthIndicator(RestClient.Builder builder,
CircuitBreakerRegistry registry) {
this.restClient = builder.baseUrl("https://api.payment.io").build();
this.circuitBreaker = registry.circuitBreaker("payment-health");
}
@Override
public Health health() {
try {
return circuitBreaker.executeSupplier(() -> {
var response = restClient.get()
.uri("/health")
.retrieve()
.toBodilessEntity();
if (response.getStatusCode().is2xxSuccessful()) {
return Health.up()
.withDetail("paymentApi", "reachable")
.withDetail("responseTime", "OK")
.build();
}
return Health.down()
.withDetail("paymentApi", "unhealthy")
.withDetail("status", response.getStatusCode())
.build();
});
} catch (Exception e) {
return Health.down()
.withDetail("paymentApi", "unreachable")
.withException(e)
.build();
}
}
}
여러 헬스 인디케이터를 그룹으로 묶어 K8s probe에 활용합니다:
# application.yml
management:
endpoint:
health:
group:
liveness:
include: livenessState
show-details: always
readiness:
include: readinessState,db,redis,externalApi
show-details: always
startup:
include: readinessState,db
show-details: always
K8s Deployment에서 probe 매핑:
containers:
- name: app
ports:
- containerPort: 8080 # 앱 포트
- containerPort: 8081 # 관리 포트
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 10
periodSeconds: 5
startupProbe:
httpGet:
path: /actuator/health/startup
port: 8081
failureThreshold: 30
periodSeconds: 2
커스텀 메트릭 수집
Micrometer를 활용한 비즈니스 메트릭 수집입니다. Counter, Gauge, Timer, Distribution Summary 4가지 핵심 메트릭 타입을 다룹니다:
@Service
@RequiredArgsConstructor
public class OrderService {
private final MeterRegistry meterRegistry;
private final OrderRepository orderRepository;
// Counter: 주문 건수 (성공/실패별)
public Order createOrder(OrderRequest req) {
try {
Order order = orderRepository.save(new Order(req));
meterRegistry.counter("orders.created",
"type", req.getType().name(),
"region", req.getRegion()
).increment();
meterRegistry.counter("orders.revenue",
"currency", "KRW"
).increment(order.getTotalAmount().doubleValue());
return order;
} catch (Exception e) {
meterRegistry.counter("orders.failed",
"reason", e.getClass().getSimpleName()
).increment();
throw e;
}
}
// Gauge: 현재 처리 대기 중인 주문 수
@PostConstruct
void registerGauges() {
Gauge.builder("orders.pending.count",
orderRepository,
repo -> repo.countByStatus(OrderStatus.PENDING)
).register(meterRegistry);
}
// Timer: 주문 처리 시간 측정
public Order processOrder(Long orderId) {
return meterRegistry.timer("orders.processing.duration",
"step", "full-process"
).record(() -> {
Order order = orderRepository.findById(orderId).orElseThrow();
order.process();
return orderRepository.save(order);
});
}
// Distribution Summary: 주문 금액 분포
public void recordOrderAmount(BigDecimal amount) {
DistributionSummary.builder("orders.amount.distribution")
.baseUnit("KRW")
.publishPercentiles(0.5, 0.75, 0.95, 0.99)
.publishPercentileHistogram()
.register(meterRegistry)
.record(amount.doubleValue());
}
}
@Timed와 AOP 기반 자동 측정
메서드 단위로 자동 타이머를 적용하는 AOP 방식입니다:
// 설정 클래스
@Configuration
public class MetricsConfig {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
// 서비스에서 사용
@Service
public class PaymentService {
@Timed(value = "payment.process",
description = "결제 처리 시간",
extraTags = {"layer", "service"},
percentiles = {0.5, 0.95, 0.99})
public PaymentResult processPayment(PaymentRequest req) {
// 이 메서드의 실행 시간이 자동으로 메트릭에 기록됨
return paymentGateway.charge(req);
}
}
런타임 로그 레벨 변경
/actuator/loggers 엔드포인트를 사용하면 재배포 없이 런타임에 로그 레벨을 변경할 수 있습니다:
# 현재 로그 레벨 조회
curl http://localhost:8081/actuator/loggers/com.example.payment
# 응답:
# {"configuredLevel":null,"effectiveLevel":"INFO"}
# 런타임에 DEBUG로 변경
curl -X POST http://localhost:8081/actuator/loggers/com.example.payment
-H 'Content-Type: application/json'
-d '{"configuredLevel":"DEBUG"}'
# 원래대로 복원
curl -X POST http://localhost:8081/actuator/loggers/com.example.payment
-H 'Content-Type: application/json'
-d '{"configuredLevel":null}'
프로덕션에서 특정 패키지의 디버그 로그를 일시적으로 켜서 문제를 진단하고, 즉시 복원할 수 있어 매우 유용합니다.
Prometheus + Grafana 통합
Actuator가 노출하는 Prometheus 메트릭을 수집하는 설정입니다:
# application.yml — Prometheus 메트릭 세부 설정
management:
prometheus:
metrics:
export:
enabled: true
metrics:
tags:
application: order-service
environment: production
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 50ms, 100ms, 200ms, 500ms, 1s
Prometheus scrape_config:
# prometheus.yml
scrape_configs:
- job_name: 'spring-boot-apps'
metrics_path: /actuator/prometheus
scrape_interval: 15s
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
target_label: __address__
regex: (.+)
replacement: ${1}:8081
K8s Pod annotation으로 자동 디스커버리:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8081"
prometheus.io/path: "/actuator/prometheus"
커스텀 Info Contributor
빌드 정보, Git 커밋, 배포 버전을 /actuator/info에 노출합니다:
@Component
public class DeploymentInfoContributor implements InfoContributor {
@Value("${app.version:unknown}")
private String appVersion;
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("deployment", Map.of(
"version", appVersion,
"timestamp", Instant.now().toString(),
"javaVersion", System.getProperty("java.version"),
"activeProfiles", String.join(",",
SpringApplication.run(App.class).getEnvironment()
.getActiveProfiles())
));
}
}
// build.gradle.kts — Git 정보 자동 포함
springBoot {
buildInfo()
}
plugins {
id("com.gorylenko.gradle-git-properties") version "2.4.1"
}
보안: Actuator 접근 제어
프로덕션에서 Actuator 엔드포인트 보안은 필수입니다:
@Configuration
@EnableWebSecurity
public class ActuatorSecurityConfig {
@Bean
@Order(1) // 관리 포트 보안 (8081)
public SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception {
return http
.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
// health는 인증 없이 허용 (K8s probe용)
.requestMatchers(EndpointRequest.to("health")).permitAll()
// prometheus는 내부 네트워크에서만
.requestMatchers(EndpointRequest.to("prometheus")).hasRole("MONITORING")
// 나머지는 ADMIN만
.anyRequest().hasRole("ADMIN")
)
.httpBasic(Customizer.withDefaults())
.build();
}
}
관련 글: Spring Boot Resilience4j에서 서킷브레이커 패턴을, K8s ConfigMap·Secret 관리 전략에서 환경 설정 관리를 함께 확인하세요.
마무리
Spring Boot Actuator는 단순한 헬스체크 도구가 아닙니다. 커스텀 Health Indicator로 비즈니스 상태 모니터링, Micrometer 메트릭으로 Prometheus/Grafana 통합, 런타임 로그 레벨 변경으로 무중단 디버깅 — 이 세 가지를 마스터하면 프로덕션 운영의 관측 가능성(Observability)이 크게 향상됩니다. 관리 포트 분리와 접근 제어는 반드시 설정하세요.