배포 장애를 줄이는 시작점

Spring Boot 서비스를 컨테이너나 Kubernetes에서 운영할 때 무중단 배포의 핵심은 새 버전이 준비되기 전에는 트래픽을 받지 않고, 기존 버전이 종료될 때는 처리 중인 요청을 충분히 마무리하게 하는 것입니다. 단순히 Pod가 실행 중인지 확인하는 것만으로는 부족합니다. 애플리케이션이 살아 있는지 보는 liveness와 지금 요청을 받아도 되는지 보는 readiness를 분리해야 배포 중 502, 503, 요청 중단을 줄일 수 있습니다.

liveness는 프로세스를 재시작해야 할 정도로 내부 상태가 깨졌는지 확인하는 신호입니다. readiness는 데이터베이스 연결, 필수 캐시, 메시지 큐처럼 업무 요청을 처리하기 위한 조건이 준비됐는지 확인하는 신호입니다. 외부 API가 잠시 느리다는 이유로 liveness를 실패시키면 재시작이 반복될 수 있으므로, 재시작 판단과 트래픽 차단 판단을 섞지 않는 것이 중요합니다.

Actuator와 graceful shutdown 기본 설정

운영 서비스에는 Spring Boot Actuator를 추가하고 health endpoint를 노출합니다. Kubernetes 환경에서는 liveness와 readiness가 health group으로 제공되며, 일반 서버나 로컬 검증 환경에서는 probe 설정을 명시적으로 켜면 됩니다. graceful shutdown은 종료 신호를 받았을 때 새 요청을 막고 진행 중인 요청이 끝날 시간을 주는 설정입니다.

<!-- pom.xml -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

# application.yml
server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      probes:
        enabled: true
      show-details: never

설정 후에는 /actuator/health/liveness/actuator/health/readiness를 직접 호출해 상태 코드와 응답을 확인합니다. 외부에 health 상세 정보가 노출되면 내부 구성이나 장애 원인이 드러날 수 있으므로, 공개망에서는 show-details: never처럼 제한하고 상세 원인은 로그와 내부 모니터링에서 확인하는 편이 안전합니다.

readiness에는 필수 조건만 포함합니다

readiness는 트래픽 수신 여부를 결정하는 기준이므로 너무 많은 항목을 넣으면 정상 요청까지 막힐 수 있습니다. 업무 요청에 반드시 필요한 데이터베이스, Redis, 필수 캐시 정도는 readiness에 포함하고, 이메일 발송이나 통계 수집처럼 핵심 처리와 분리된 기능은 별도 지표로 관찰하는 것이 좋습니다.

# application.yml
management:
  endpoint:
    health:
      group:
        liveness:
          include: livenessState
        readiness:
          include: readinessState,db,redis,requiredCache

업무 캐시처럼 Spring Boot가 기본으로 알 수 없는 준비 상태는 직접 HealthIndicator로 연결합니다. 아래 예시는 필수 캐시가 로딩되지 않았을 때 readiness를 DOWN으로 내려 배포 직후 트래픽 유입을 막는 방식입니다.

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component("requiredCache")
public class RequiredCacheHealthIndicator implements HealthIndicator {
  private final RequiredCacheService cacheService;

  public RequiredCacheHealthIndicator(RequiredCacheService cacheService) {
    this.cacheService = cacheService;
  }

  @Override
  public Health health() {
    if (cacheService.isLoaded()) {
      return Health.up()
          .withDetail("version", cacheService.currentVersion())
          .build();
    }
    return Health.down()
        .withDetail("reason", "required cache is not loaded")
        .build();
  }
}

Kubernetes probe와 종료 시간을 맞추기

Kubernetes probe 값은 실제 애플리케이션 부팅 시간과 요청 처리 시간을 기준으로 잡아야 합니다. 너무 짧으면 정상 기동 중인 Pod가 재시작되고, 너무 길면 장애 감지가 늦습니다. 특히 terminationGracePeriodSeconds가 Spring Boot의 종료 유예 시간보다 짧으면 graceful shutdown이 끝나기 전에 컨테이너가 강제 종료될 수 있습니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-api
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 45
      containers:
        - name: order-api
          image: registry.example.com/order-api:2026.07.04
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 10"]
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
            failureThreshold: 2

preStop의 짧은 대기는 Service endpoint나 외부 로드밸런서에서 Pod가 제외되는 시간을 벌기 위한 장치입니다. 무조건 길게 두기보다 실제 endpoint 전파 시간, 평균 요청 시간, 긴 요청의 타임아웃을 함께 보고 조정해야 합니다. 배포 후에는 readiness가 먼저 내려가고, 기존 요청이 빠진 뒤 프로세스가 종료되는지 로그로 확인합니다.

운영 점검 체크리스트

  • liveness는 재시작 판단, readiness는 트래픽 투입 판단으로 분리합니다.
  • readiness에는 데이터베이스, 필수 캐시, 필수 큐처럼 요청 처리에 필요한 조건만 넣습니다.
  • server.shutdown: graceful과 종료 유예 시간을 실제 긴 요청보다 넉넉하게 잡습니다.
  • Kubernetes의 terminationGracePeriodSeconds는 Spring Boot 종료 유예 시간보다 길게 설정합니다.
  • 배포 직후 probe URL, active request, 오류율, connection pool 사용률을 함께 확인합니다.
  • liveness 실패가 반복되면 재시작 설정이 아니라 메모리, deadlock, connection 고갈 원인을 먼저 추적합니다.