Spring Boot 3 핵심 정리 시리즈 13편. Actuator로 /health·/metrics·/loggers 모니터링 엔드포인트를 띄우고, Kubernetes Liveness/Readiness 프로브를 설정하고, Micrometer + Prometheus로 메트릭을 수집하고, Zalando Logbook으로 HTTP 요청·응답을 구조화 JSON 로그로 남기는 흐름까지 — 사내 헬스 모니터링 시스템 비유로 친절하게 풀어쓴 13편.
이 글은 Spring Boot 3 핵심 정리 시리즈의 열세 번째 편입니다. 12편까지 API 명세·AI까지 풀었다면, 이번 13편은 운영 단계 — 띄운 앱이 잘 돌고 있는지 어떻게 확인하고, 문제가 생겼을 때 어떻게 빠르게 진단할지를 다뤄요. 키워드는 두 개입니다 — Actuator와 관측성(observability).
본문 흐름은 회사 비유를 따라 풀어 가요. Actuator는 "사내 헬스 모니터링 시스템" 이에요. 회사 건물에 곳곳에 센서를 박아서 — 각 부서가 정상 작동하는지(헬스 체크), 전기·공조·서버 사용량은 어떤지(메트릭), 누가 언제 출입했는지(로그) — 한 화면에서 다 볼 수 있는 통합 모니터링 대시보드. /health·/metrics·/loggers는 그 대시보드의 화면 메뉴들이고요.
왜 Actuator·관측성이 처음엔 어렵게 느껴질까요
이유는 네 가지예요.
첫째, 운영을 안 해 본 사람은 왜 이런 게 필요한지가 안 보입니다. 개발 환경에선 IDE 콘솔만 봐도 되니까요. 컨테이너로 띄운 앱이 새벽 2시에 멈췄을 때 로그·메트릭이 없으면 어디서 뭐가 잘못됐는지 영영 모릅니다.
둘째, Liveness·Readiness 프로브가 처음엔 추상적이에요. "컨테이너가 살아 있는지 확인"이라는 말이 같아 보이지만, 둘은 실패 시 동작이 완전히 다릅니다. Liveness 실패 = 컨테이너 재시작, Readiness 실패 = 트래픽 차단(재시작 X).
셋째, Logback·Logstash·Logbook 셋이 헷갈립니다. 이름이 비슷해서 처음엔 다 같은 라이브러리로 보이는데, 각자 다른 일을 합니다.
넷째, Micrometer·Prometheus·Grafana 단어가 줄지어 나옵니다. Spring 앱에서 어떤 게 어디 위치하는지가 한 번에 안 잡혀요.
해결법은 한 가지예요. Actuator = "사내 헬스 모니터링 시스템" 으로 잡으면 갑자기 명확해집니다. /health는 건강 검진 결과 화면, /metrics는 각 부서 통계 화면, /loggers는 로그 레벨 조정 패널, Liveness/Readiness 프로브는 회사 정문에 붙은 "사무실 운영 중/준비 중" 안내판 같은 거예요. 이 비유를 따라 풀어 갑니다.
Actuator 한 줄 — 의존성 하나로 모니터링 활성화
Spring Boot Actuator는 Spring에서 "운영 환경에 즉시 적용 가능한 기능"으로 명명한 도구 모음이에요. 의존성 하나만 박으면 다양한 모니터링 엔드포인트가 자동으로 활성화됩니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Prometheus 메트릭을 노출하려면 추가 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
기본 활성화는 /actuator/health 한 개뿐이에요. 다른 엔드포인트는 명시적으로 노출해야 합니다.
# 노출할 엔드포인트 — 프로덕션 권장 (* 사용 X)
management.endpoints.web.exposure.include=health,info,metrics,prometheus
# 헬스 상세 정보 (보안 고려해 인증된 사용자만 보고 싶으면 when-authorized)
management.endpoint.health.show-details=always
# Kubernetes 프로브 활성화
management.health.probes.enabled=true
# 정상 종료 (in-flight 요청 완료 후 종료)
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
주요 엔드포인트 11종
| 엔드포인트 | 역할 | 기본 활성화 | 보안 위험도 |
|---|---|---|---|
/health | 상태 확인 (UP/DOWN) | 예 | 낮음 |
/info | 앱 정보 | 아니오 | 낮음 |
/metrics | 메트릭 목록 | 아니오 | 중간 |
/metrics/{name} | 특정 메트릭 상세 | 아니오 | 중간 |
/beans | 스프링 빈 정보 | 아니오 | 중간 |
/env | 환경 프로퍼티 | 아니오 | 높음 (시크릿 노출 가능) |
/mappings | URL 매핑 | 아니오 | 낮음 |
/loggers | 로그 레벨 조회·변경 | 아니오 | 중간 |
/prometheus | Prometheus 메트릭 | 아니오 | 중간 |
/health/liveness | K8s Liveness | 옵션 | 낮음 |
/health/readiness | K8s Readiness | 옵션 | 낮음 |
/shutdown | 앱 종료 | 아니오 | 매우 높음 |
여기서 정말 중요한 시험 함정이 하나 있어요. **프로덕션에서 management.endpoints.web.exposure.include=*로 모두 노출하면** /env로 환경 변수(=시크릿)·/beans로 내부 구조·/shutdown으로 앱 강제 종료가 가능해집니다. 보안 사고로 직결되는 1순위 함정이에요. 프로덕션엔 health,info,metrics,prometheus 정도만.
> 한 줄 정리 — Actuator = spring-boot-starter-actuator 한 줄. 노출은 management.endpoints.web.exposure.include로 명시적 제어.
Spring Security와 Actuator 통합
Spring Security가 적용된 프로젝트는 기본 보안 정책이 Actuator 엔드포인트도 막아 버려요. Actuator 전용 SecurityFilterChain을 별도로 만드는 게 정석입니다.
EndpointRequest.toAnyEndpoint()는 활성화된 모든 Actuator 엔드포인트를 자동 매칭하는 유틸리티예요. 하드코딩된 경로 대신 이걸 쓰면 새 엔드포인트가 추가돼도 자동으로 보안 설정이 따라옵니다.
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
// Actuator 전용 SecurityFilterChain (공식 권장 방식)
@Bean
@Order(1) // Actuator 보안이 먼저 평가되도록
public SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(requests ->
requests.anyRequest().permitAll()
// 프로덕션은 hasRole("ADMIN") 등으로 제한 권장
);
return http.build();
}
// 애플리케이션 API 보안 체인
@Bean
@Order(2)
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
여기서 시험 함정 — 두 SecurityFilterChain의 평가 순서입니다. @Order(1)을 Actuator 체인에 박지 않으면 어떤 체인이 먼저 평가될지 예측 불가예요. 결과적으로 Actuator 요청이 일반 API 보안 체인에 잡혀 401을 받게 됩니다.
또 한 가지 — 클라우드 환경에선 Actuator 포트를 별도로 빼는 패턴도 흔해요.
management.server.port=8090 # Actuator는 다른 포트
management.server.address=127.0.0.1 # 로컬 바인딩만
# Kubernetes Service에서 8090은 클러스터 내부에만 노출
# 8080(앱)만 LoadBalancer/Ingress로 외부 노출
> 한 줄 정리 — Actuator 보안 = EndpointRequest.toAnyEndpoint() + @Order(1). 두 SecurityFilterChain 순서가 핵심.
Kubernetes 프로브 — 정문에 붙은 안내판
컨테이너 환경에서 Actuator의 가장 중요한 역할은 K8s 오케스트레이터에게 앱 상태를 알려 주는 거예요. 두 가지 프로브가 있고, 각자 실패 시 동작이 완전히 다릅니다.
| 프로브 | 의미 | 실패 시 동작 | 엔드포인트 |
|---|---|---|---|
| Liveness | 앱이 살아 있나? | 컨테이너 재시작 | /actuator/health/liveness |
| Readiness | 트래픽 받을 준비됐나? | 트래픽 라우팅 중단 (재시작 X) | /actuator/health/readiness |
회사 비유로 — 사옥 정문에 두 안내판이 붙어 있어요. "사무실 운영 중"(Liveness) 안내판이 꺼지면 본사가 "이 사무실 비상! 재가동!"을 합니다. "손님 받기 준비 중"(Readiness) 안내판이 꺼지면 본사가 "이 사무실은 잠깐 손님을 보내지 마세요"라고만 해요. 사무실은 그대로 유지됩니다.
프로브 시작 생명주기
1. 앱 시작 초기 → Connection Refused (서버 미구동)
2. 서버 구동 후, 초기화 중 → Readiness OUT_OF_SERVICE / Liveness UP
3. 완전 초기화 완료 → 둘 다 UP
Kubernetes 매니페스트 — 프로브 설정
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-boot-app
spec:
template:
spec:
containers:
- name: spring-boot-app
image: my-spring-app:latest
ports:
- containerPort: 8080
# Liveness — 앱이 살아 있나?
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60 # 시작 대기
periodSeconds: 10 # 확인 주기
failureThreshold: 3
# Readiness — 트래픽 받을 준비?
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
failureThreshold: 3
여기서 정말 중요한 시험 함정이 하나 있어요. initialDelaySeconds를 너무 짧게 잡으면 큰 Spring Boot 앱이 시작에 60초 이상 걸릴 수 있어서, 시작되기도 전에 K8s가 컨테이너를 종료(재시작)시키는 무한 루프에 빠집니다. 앱 크기에 맞춰 충분히 잡으세요.
또 하나 — management.health.probes.enabled=true 설정이 빠지면 /actuator/health/liveness·/actuator/health/readiness 엔드포인트 자체가 안 만들어져요.
커스텀 헬스 인디케이터
내부 의존성(외부 API·메시지 큐 등)도 헬스 체크에 포함할 수 있어요.
@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
private final ExternalServiceClient externalServiceClient;
public ExternalServiceHealthIndicator(ExternalServiceClient client) {
this.externalServiceClient = client;
}
@Override
public Health health() {
try {
boolean isAvailable = externalServiceClient.ping();
if (isAvailable) {
return Health.up()
.withDetail("service", "External Service")
.withDetail("status", "연결됨")
.build();
} else {
return Health.down()
.withDetail("service", "External Service")
.withDetail("status", "연결 불가")
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}
> 한 줄 정리 — Liveness 실패 = 재시작 / Readiness 실패 = 트래픽 차단(재시작 X). 둘은 완전히 다른 개념.
Micrometer & Prometheus — 메트릭 수집 파이프라인
Spring Boot Actuator는 Micrometer를 통해 메트릭을 수집해요. Micrometer는 다양한 모니터링 시스템(Prometheus·Datadog·New Relic 등)을 위한 추상화 레이어입니다 — SLF4J가 로깅 시스템들의 추상화이듯, Micrometer는 메트릭 시스템들의 추상화예요.
커스텀 비즈니스 메트릭 등록
@Service
public class ProductService {
private final MeterRegistry meterRegistry;
private final Counter productCreationCounter;
public ProductService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
// 카운터 — 한 방향으로만 증가하는 값 (생성 횟수 등)
this.productCreationCounter = Counter.builder("product.creation.count")
.description("생성된 상품 수")
.tag("type", "regular")
.register(meterRegistry);
}
public ProductDTO createProduct(ProductDTO dto) {
ProductDTO saved = productRepository.save(/* ... */);
productCreationCounter.increment();
return saved;
}
// 타이머 — 작업 시간을 측정
public List<ProductDTO> listProducts() {
return Timer.builder("product.list.timer")
.description("상품 목록 조회 시간")
.register(meterRegistry)
.record(() -> productRepository.findAll()
.stream()
.map(mapper::productToProductDTO)
.collect(Collectors.toList()));
}
}
Gauge — 현재 값 측정
@Configuration
public class MetricsConfig {
@Bean
public MeterBinder productMetrics(ProductRepository productRepository) {
return registry -> Gauge.builder("product.total.count",
productRepository,
ProductRepository::count)
.description("전체 상품 수")
.register(registry);
}
}
메트릭 종류 정리
| 메트릭 타입 | 의미 | 사용처 |
|---|---|---|
| Counter | 한 방향 증가 | 요청 수·에러 수·이벤트 수 |
| Timer | 작업 시간 + 횟수 | API 응답 시간·DB 쿼리 시간 |
| Gauge | 현재 값 (변동) | 메모리 사용량·큐 크기 |
| Distribution | 값의 분포 | 응답 시간 백분위수 |
Prometheus 통합
# prometheus.yml
scrape_configs:
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
static_configs:
- targets: ['localhost:8080']
Prometheus가 주기적으로 /actuator/prometheus에서 메트릭을 pull해 저장하고, Grafana 대시보드에서 시각화해요. 자세한 메트릭 모범 사례는 Spring Boot Actuator 공식 문서와 Micrometer 공식 문서에서 확인할 수 있어요.
> 한 줄 정리 — Micrometer = 메트릭 추상화 / Prometheus = 시계열 저장소·풀 수집기 / Grafana = 시각화 대시보드.
로그 레벨 동적 변경 — 운영 중 디버깅 무기
/actuator/loggers 엔드포인트는 재시작 없이 로그 레벨을 변경하게 해 줘요. 프로덕션에서 특정 패키지만 잠깐 DEBUG로 올려서 문제를 진단하고 다시 INFO로 내릴 수 있습니다.
# 현재 로그 레벨 조회
curl http://localhost:8080/actuator/loggers/com.example.product
# 응답 예시
# {
# "configuredLevel": "INFO",
# "effectiveLevel": "INFO"
# }
# TRACE로 변경
curl -X POST http://localhost:8080/actuator/loggers/com.example.product \
-H 'Content-Type: application/json' \
-d '{"configuredLevel": "TRACE"}'
# 원래대로 복구
curl -X POST http://localhost:8080/actuator/loggers/com.example.product \
-H 'Content-Type: application/json' \
-d '{"configuredLevel": "INFO"}'
운영 중 진단에 가장 강력한 무기예요. 다만 노출 위험도 있으니 보안 설정은 꼼꼼하게.
Zalando Logbook — HTTP 요청·응답 상세 로깅
Spring 내장 로깅은 HTTP 본문 로깅이 안 되고, 민감 정보(JWT 토큰 등) 마스킹 기능도 없어요. Zalando Logbook이 이 두 한계를 해결합니다.
<properties>
<logbook.version>3.8.0</logbook.version>
</properties>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>${logbook.version}</version>
</dependency>
<!-- Logstash 연동 (JSON 로깅) -->
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-logstash</artifactId>
<version>${logbook.version}</version>
</dependency>
<!-- Logstash Logback Encoder -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
# 반드시 TRACE 레벨이어야 HTTP 요청·응답이 출력됨!
logging.level.org.zalando.logbook=TRACE
여기서 정말 중요한 시험 함정 — 로그 레벨이 INFO·DEBUG면 Logbook은 아무것도 출력하지 않아요. TRACE 레벨이 필수.
Logbook 핵심 기능 5가지
- HTTP 요청·응답의 헤더와 본문 모두 기록
- Authorization 헤더 같은 민감 정보 자동 마스킹
- JSON 형식 구조화 로그로 ELK·Splunk 연동 용이
- Spring MVC와 WebFlux 둘 다 지원 (Spring 6 호환 완료)
- Spring Boot Starter로 자동 구성
Logback·Logstash·Logbook 셋의 정체
이름이 비슷해서 자주 헷갈리는데 각자 다른 일을 합니다.
| 라이브러리 | 정체 | 역할 |
|---|---|---|
| Logback | Spring Boot 기본 로깅 프레임워크 | 모든 로그 출력의 엔진 |
| Logstash | 로그 수집·처리 도구 | 여기선 Logback용 JSON 인코더 |
| Logbook | HTTP 요청·응답 로깅 라이브러리 | HTTP 페이로드 전용 |
Logbook + Logstash 통합 설정
같이 쓰면 HTTP 페이로드가 JSON 안에 이스케이프된 문자열로 박히는 문제가 생겨요.
// 잘못된 결과 — 페이로드가 이스케이프된 문자열
{
"message": "{\"method\":\"GET\",\"path\":\"/api/v1/product\",...}"
}
해결책은 LogbookConfig에 LogstashLogbackSink를 박는 것.
@Configuration
public class LogbookConfig {
@Bean
public Sink logbookLogstashSink() {
return new LogstashLogbackSink(new JsonHttpLogFormatter());
}
}
// 올바른 결과 — 중첩 JSON 객체 (검색 용이)
{
"message": {
"method": "GET",
"path": "/api/v1/product",
...
}
}
logback-spring.xml — JSON 로깅 설정
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app":"spring-boot-app","env":"production"}</customFields>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON_CONSOLE"/>
</root>
<!-- Logbook 전용 TRACE -->
<logger name="org.zalando.logbook" level="TRACE" additivity="false">
<appender-ref ref="JSON_CONSOLE"/>
</logger>
</configuration>
WebFlux 환경에서 추가 의존성
<!-- WebFlux 프로젝트에서 별도 자동 구성 모듈 필요 -->
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-webflux-autoconfigure</artifactId>
<version>${logbook.version}</version>
</dependency>
여기서 시험 함정 — WebFlux에서는 logbook-spring-boot-webflux-autoconfigure가 추가로 필요합니다. MVC 설정을 그대로 가져오면 동작 안 해요.
MDC로 요청 추적 — 모든 로그에 요청 ID 자동 포함
MDC(Mapped Diagnostic Context)는 한 요청을 처리하는 동안 로그에 자동으로 박힐 키-값 컨텍스트예요. 마이크로서비스 환경에서 한 요청이 여러 서비스를 거쳐 갈 때 — 모든 로그에 같은 요청 ID가 박혀 있어야 추적이 가능합니다.
@Component
public class RequestLoggingFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 헤더에서 추적 ID 추출 또는 새로 생성
String requestId = Optional.ofNullable(
httpRequest.getHeader("X-Request-ID"))
.orElse(UUID.randomUUID().toString());
// MDC에 등록 — 이 요청 동안 모든 로그에 자동 포함
MDC.put("requestId", requestId);
MDC.put("userId", extractUserId(httpRequest));
try {
// 응답 헤더에 추적 ID 추가
((HttpServletResponse) response)
.setHeader("X-Request-ID", requestId);
chain.doFilter(request, response);
} finally {
// 반드시 정리 — 메모리 누수·다음 요청 오염 방지
MDC.clear();
}
}
private String extractUserId(HttpServletRequest request) {
return "anonymous";
}
}
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean<RequestLoggingFilter> requestLoggingFilter() {
FilterRegistrationBean<RequestLoggingFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new RequestLoggingFilter());
bean.addUrlPatterns("/api/*");
bean.setOrder(1); // 가장 먼저 실행
return bean;
}
}
여기서 시험 함정 — MDC.clear()를 finally에서 빠뜨리면 ThreadLocal에 컨텍스트가 남아 다음 요청이 같은 스레드를 재사용할 때 이전 요청 ID가 그대로 박혀요. 보안·디버깅 양쪽으로 위험.
Spring RestClient — 운영 단계의 HTTP 클라이언트
12편에서도 짚었지만, 13편 노트에 다시 등장하니 한 번 더 핵심만 정리할게요. RestClient는 Spring Framework 6.1+에서 등장한 블로킹 + fluent HTTP 클라이언트예요. RestTemplate 위에 구축됐지만 API는 WebClient와 비슷한 체이닝 스타일.
@Service
public class ProductClientImpl {
private final RestClient restClient;
public ProductClientImpl(RestClient.Builder builder) {
this.restClient = builder
.baseUrl("http://localhost:8080")
.build();
}
public Optional<ProductDTO> getProductByIdSafe(UUID id) {
try {
return Optional.ofNullable(
restClient.get()
.uri("/api/v1/product/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
(req, res) -> {
if (res.getStatusCode() == HttpStatus.NOT_FOUND) {
throw new NotFoundException("Product not found");
}
throw new ClientErrorException("Client error");
})
.onStatus(HttpStatusCode::is5xxServerError,
(req, res) -> { throw new ServerErrorException("Server error"); })
.body(ProductDTO.class)
);
} catch (NotFoundException e) {
return Optional.empty();
}
}
}
운영 중 호출 시 가장 흔한 함정 두 가지 — .onStatus() 누락(4xx/5xx 처리 안 됨)과 매번 RestClient.create() 로 새 인스턴스 생성(빈으로 등록해 재사용 권장).
비교표 — 한 번 더 한눈에
RestTemplate vs RestClient vs WebClient
| 특성 | RestTemplate | RestClient (Spring 6.1) | WebClient |
|---|---|---|---|
| 실행 | 블로킹 | 블로킹 | 논블로킹 |
| API | 메서드 (구식) | Fluent (현대적) | Fluent |
| 반환 | 동기 T | 동기 T | Mono/Flux |
| Spring | 3+ | 6.1+ | 5+ |
| 적합 | 레거시 | 새 MVC 앱 | WebFlux 앱 |
Liveness vs Readiness
| 특성 | Liveness | Readiness |
|---|---|---|
| 역할 | 앱 생존 | 트래픽 수신 준비 |
| 실패 시 | 컨테이너 재시작 | 트래픽 라우팅 중단 (재시작 X) |
| 엔드포인트 | /actuator/health/liveness | /actuator/health/readiness |
| 시작 시 | DOWN → UP | OUT_OF_SERVICE → UP |
| 적합 감지 | 데드락·응답 불능 | DB 연결·초기화 미완료 |
Logbook vs Spring 내장 로깅
| 특성 | Spring 내장 로깅 | Zalando Logbook |
|---|---|---|
| HTTP 본문 로깅 | 불가 | 가능 (요청·응답 모두) |
| 민감 정보 마스킹 | 미지원 | 자동 |
| JSON 형식 | 추가 설정 | 기본 지원 |
| 로그 구조화 | 텍스트 | 구조화된 JSON |
| Spring 통합 | 네이티브 | Spring Boot Starter |
텍스트 로그 vs JSON 로그
| 특성 | 텍스트 로그 | JSON 로그 |
|---|---|---|
| 사람 가독성 | 높음 | 낮음 (원시) |
| 기계 처리 | 어려움 (파싱 필요) | 쉬움 (구조화) |
| ELK 통합 | 파싱 규칙 필요 | 즉시 인덱싱 |
| 필드 검색 | 정규식 | 필드명 직접 |
| 개발 환경 | 선호 | 덜 선호 |
| 프로덕션 | 덜 적합 | 권장 |
자주 만나는 함정 10가지
1. management.endpoints.web.exposure.include=*로 모두 노출
/env로 시크릿 노출 + /shutdown으로 강제 종료 가능. 프로덕션은 health,info,metrics,prometheus만.
2. SecurityFilterChain 순서 누락
@Order(1) Actuator + @Order(2) API. 안 박으면 평가 순서 예측 불가.
3. K8s initialDelaySeconds 미설정
큰 앱은 60초+ 걸림. 시작도 못 하고 재시작 무한 루프.
4. management.health.probes.enabled=true 누락
/actuator/health/liveness·/actuator/health/readiness 엔드포인트 자체가 안 만들어짐.
5. Logbook 로그 레벨이 INFO/DEBUG
TRACE만 출력. 가장 흔한 "Logbook이 안 동작해요" 원인.
6. LogbookConfig 없이 Logstash 통합
페이로드가 이스케이프된 문자열로 박힘. LogstashLogbackSink 빈 등록 필수.
7. WebFlux에서 webflux-autoconfigure 누락
MVC 설정 그대로 가져오면 동작 안 함. logbook-spring-boot-webflux-autoconfigure 추가.
8. MDC clear 누락
ThreadLocal에 남아 다음 요청 오염. try-finally에서 반드시 MDC.clear().
9. RestClient .onStatus() 누락
4xx/5xx도 정상 처리 시도하다 파싱 오류. 명시적 오류 매핑 필수.
10. RestClient를 매번 새로 생성
// 비효율 — 매번 생성
public ProductDTO getProduct(UUID id) {
RestClient restClient = RestClient.create();
return restClient.get().uri("...").retrieve().body(ProductDTO.class);
}
빈으로 등록해 재사용. 생성자 주입으로 RestClient.Builder 받아 한 번만 빌드.
핵심 압축 노트 — 시험 직전 한 번 더
여기까지가 13편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- Actuator =
spring-boot-starter-actuator한 줄로 활성화 - 기본 노출 =
/actuator/health한 개 - 노출 제어 =
management.endpoints.web.exposure.include=health,info,metrics,prometheus - **프로덕션에
*로 모두 노출 X** —/env·/shutdown보안 사고 위험 - Spring Security + Actuator =
EndpointRequest.toAnyEndpoint()+@Order(1) - 두 SecurityFilterChain 순서 명시 안 하면 평가 순서 예측 불가
- 클라우드는
management.server.port=8090으로 별도 포트 분리도 흔함 - Liveness ≠ Readiness
- Liveness 실패 = 컨테이너 재시작 / Readiness 실패 = 트래픽 차단(재시작 X)
management.health.probes.enabled=true없으면 프로브 엔드포인트 자체가 없음- K8s
initialDelaySeconds충분히 (큰 앱은 60초+) - 커스텀 헬스 =
HealthIndicator인터페이스 구현 +@Component - Micrometer = 메트릭 추상화 (SLF4J가 로깅 추상화이듯)
- 메트릭 4종 = Counter(증가) / Timer(시간) / Gauge(현재값) / Distribution(분포)
MeterRegistry주입 → 빌더 패턴으로 메트릭 등록- Prometheus =
/actuator/prometheuspull → 시계열 저장 → Grafana 시각화 /actuator/loggers= 재시작 없이 로그 레벨 동적 변경 (운영 진단 무기)- Logback / Logstash / Logbook 셋은 다른 라이브러리
- Logback = Spring Boot 기본 / Logstash = JSON 인코더 / Logbook = HTTP 페이로드 로거
- Logbook 레벨 = TRACE (INFO·DEBUG에선 출력 X)
- Logbook + Logstash 통합 →
LogstashLogbackSink빈 등록 필수 (이스케이프 문제) - WebFlux는
logbook-spring-boot-webflux-autoconfigure추가 필요 - MDC = ThreadLocal 컨텍스트, 모든 로그에 요청 ID 자동 포함
try-finally에서MDC.clear()반드시 (다음 요청 오염 방지)- RestClient (Spring 6.1+) = 블로킹 + fluent
.onStatus()4xx/5xx 명시 처리 필수- 매번
RestClient.create()X → 빈으로 재사용 - 정상 종료 =
server.shutdown=graceful+spring.lifecycle.timeout-per-shutdown-phase=30s
시리즈 다른 편
같은 시리즈의 다른 글들도 같은 친절 톤으로 묶어 정리되어 있어요.
- 1편 — Spring Boot 입문
- 2편 — Spring MVC REST · MockMVC
- 3편 — Spring Data JPA · 검증
- 4편 — MySQL · Flyway · TestContainers
- 5편 — CSV 업로드 · 페이징 · 동적 쿼리
- 6편 — JPA 관계 매핑 심화
- 7편 — Spring Security · OAuth 2.0 · JWT
- 8편 — RestTemplate · RestClient
- 9편 — Reactive Programming · WebFlux 입문
- 10편 — WebFlux 심화 · MongoDB · WebClient
- 11편 — Cloud Gateway · Maven/Gradle · Buildpack
- 12편 — OpenAPI · Spring AI
- 13편 — Actuator · 관측성 (현재 글)
- 14편 — Spring Cache · 이벤트
- 15편 — Docker · Compose · Kubernetes
- 16편 — 마이크로서비스 · Apache Kafka
- 17편 — Spring Professional · 베스트 프랙티스 (완)