Contents
see List왜 헬스 체크만으로는 무중단 배포가 되지 않을까
Spring Boot 애플리케이션을 운영하다 보면 “서버는 살아 있는데 사용자는 502 또는 타임아웃을 본다”는 문제가 자주 발생합니다. 원인은 대부분 배포 전후의 짧은 경계 구간입니다. 새 인스턴스가 아직 DB 커넥션 풀, 캐시, 외부 API 클라이언트를 준비하지 못했는데 로드밸런서가 트래픽을 보내거나, 반대로 종료 중인 인스턴스가 요청을 처리하는 도중 컨테이너가 먼저 내려가면 장애가 됩니다. 단순히 /actuator/health가 200을 반환하는지만 보는 방식은 운영 준비 상태와 프로세스 생존 상태를 구분하지 못합니다.
실전에서는 liveness, readiness, graceful shutdown을 분리해야 합니다. liveness는 “프로세스를 재시작해야 할 정도로 망가졌는가”를 판단하고, readiness는 “지금 새 요청을 받아도 되는가”를 판단합니다. graceful shutdown은 이미 받은 요청을 가능한 한 끝까지 처리하고, 새 요청 유입을 막은 뒤 안전하게 종료하는 절차입니다. 이 세 가지를 함께 설계하면 롤링 배포, 오토스케일링, 장애 복구 시 사용자에게 보이는 오류를 크게 줄일 수 있습니다.
Spring Boot Actuator 설정 기본값 잡기
먼저 Actuator를 추가하고 헬스 엔드포인트를 세분화합니다. 운영 환경에서는 모든 상세 정보를 외부에 공개하지 않고, 내부망 또는 인증된 모니터링 시스템에서만 확인하도록 제한해야 합니다. 아래 설정은 readiness와 liveness 그룹을 분리하고, 종료 시점에 서버가 요청을 기다릴 수 있도록 graceful shutdown을 켜는 예입니다.
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
probes:
enabled: true
show-details: never
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
이렇게 설정하면 Kubernetes 같은 플랫폼에서 /actuator/health/liveness와 /actuator/health/readiness를 별도로 호출할 수 있습니다. 중요한 점은 readiness에 너무 많은 외부 의존성을 넣지 않는 것입니다. 예를 들어 결제 외부 API가 순간적으로 느리다고 해서 모든 웹 서버를 준비 불가로 바꾸면 오히려 전체 서비스가 빠르게 빠져나가며 장애가 커질 수 있습니다. 반대로 DB가 완전히 끊겨 핵심 요청을 처리할 수 없다면 readiness를 실패로 바꾸는 것이 맞습니다.
Kubernetes probe를 운영 기준으로 작성하기
컨테이너 환경에서는 probe 주기가 곧 장애 감지 속도이자 오탐 가능성입니다. 너무 짧으면 일시적인 GC, 디스크 지연, 네트워크 흔들림에 재시작이 반복되고, 너무 길면 죽은 인스턴스가 오래 트래픽을 받습니다. 다음 예시는 Spring Boot 애플리케이션의 일반적인 웹 API 서버에 적용할 수 있는 기준값입니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-api
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
spec:
terminationGracePeriodSeconds: 45
containers:
- name: app
image: registry.example.com/order-api:2026.05.17
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readiness는 배포 직후 트래픽 투입을 늦추는 역할이므로 liveness보다 빠르게 확인해도 됩니다. liveness는 재시작이라는 강한 조치를 유발하므로 더 보수적으로 설정해야 합니다. 특히 JVM 애플리케이션은 시작 직후 클래스 로딩, JIT, 커넥션 풀 준비 때문에 짧은 지연이 생길 수 있으므로 initialDelaySeconds를 너무 낮게 잡지 않는 편이 안전합니다.
종료 직전 새 요청을 막는 애플리케이션 이벤트 처리
Spring Boot는 애플리케이션 가용성 상태를 이벤트로 관리합니다. 기본 기능만으로도 충분한 경우가 많지만, 메시지 컨슈머, 배치 워커, 내부 큐처럼 HTTP 서버 외부에서 일을 받는 컴포넌트는 종료 전 직접 멈춰야 합니다. 아래 예시는 종료 이벤트를 받으면 내부 작업 수신을 중단하고, 이미 진행 중인 작업만 마무리하도록 플래그를 전환하는 패턴입니다.
@Component
public class ShutdownGuard {
private final AtomicBoolean acceptingJobs = new AtomicBoolean(true);
@EventListener
public void onReadinessChange(AvailabilityChangeEvent<ReadinessState> event) {
if (event.getState() == ReadinessState.REFUSING_TRAFFIC) {
acceptingJobs.set(false);
}
}
public boolean canAcceptJob() {
return acceptingJobs.get();
}
}
이 방식은 HTTP 요청뿐 아니라 Kafka, RabbitMQ, 스케줄러, 사내 큐 처리기에도 유용합니다. 종료 신호를 받은 뒤에도 컨슈머가 계속 새 메시지를 가져오면 graceful shutdown이 의미가 없어집니다. 종료 단계에서는 새 작업을 받지 않고, 처리 중인 작업의 최대 대기 시간을 명확히 둔 뒤, 실패한 작업은 재처리 큐나 보상 트랜잭션으로 넘기는 구조가 안정적입니다.
운영에서 자주 하는 실수
- readiness와 liveness를 같은 URL로 둔다. 준비 불가 상황이 곧 재시작으로 이어져 장애가 확대될 수 있습니다.
- 헬스 체크에서 외부 API를 매번 동기 호출한다. 모니터링 트래픽이 외부 장애를 증폭시키고 애플리케이션 스레드를 점유할 수 있습니다.
- terminationGracePeriodSeconds가 Spring shutdown timeout보다 짧다. 애플리케이션이 기다리기도 전에 컨테이너가 강제 종료됩니다.
- 커넥션 풀 준비 전 readiness가 성공한다. 첫 요청에서 지연과 오류가 집중됩니다.
- Actuator 상세 정보를 인터넷에 공개한다. 빌드 정보, 디스크 상태, 내부 컴포넌트명이 노출될 수 있습니다.
적용 체크리스트
첫째, /actuator/health/liveness와 /actuator/health/readiness를 분리합니다. 둘째, readiness는 핵심 처리 가능 여부만 판단하고 외부 의존성은 신중히 포함합니다. 셋째, graceful shutdown 시간과 컨테이너 종료 대기 시간을 맞춥니다. 넷째, HTTP 외부의 메시지 컨슈머와 스케줄러도 종료 이벤트에 반응하게 만듭니다. 다섯째, probe 설정 변경 후에는 실제 롤링 배포를 수행하며 4xx, 5xx, 응답 시간, 재시작 횟수를 함께 확인합니다. 이 구성이 갖춰지면 Spring Boot 서비스는 배포와 장애 복구 과정에서 훨씬 예측 가능하게 동작합니다.