Graceful Shutdown — 요청 처리 중에 서버가 종료되면 어떻게 될까
배포 중에 사용자가 결제를 진행하고 있었다면, 그 요청은 어떻게 되는 걸까요?
Kubernetes에서 롤링 업데이트를 하거나 kill 명령으로 서버를 멈출 때, 진행 중인 HTTP 요청이 있을 수 있습니다. Graceful Shutdown이 없으면 이 요청은 중간에 끊어집니다 — 결제, 데이터 저장, 파일 업로드 등이 반쯤 완료된 채로.
개념 정의
Graceful Shutdown 은 서버 종료 신호(SIGTERM)를 받았을 때, 새 요청은 거부 하면서 진행 중인 요청은 완료될 때까지 기다린 후 종료하는 방식입니다.
설정
# application.yml
server:
shutdown: graceful # 기본값은 immediate (즉시 종료)
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 최대 대기 시간 (기본 30초)
이 두 줄이면 됩니다. 설정하면 다음과 같이 동작합니다.
동작 흐름
- **SIGTERM 수신 **: JVM이 종료 신호를 받습니다 (Kubernetes의
preStop,kill명령 등). - ** 새 요청 거부 **: 서블릿 컨테이너가 새 커넥션을 받지 않습니다. 클라이언트는 503을 받습니다.
- ** 진행 중인 요청 대기 **: 이미 처리 중인 요청은 계속 진행합니다.
- ** 타임아웃 **:
timeout-per-shutdown-phase안에 끝나지 않으면 강제 종료됩니다.
Kubernetes와 함께 쓸 때
Kubernetes에서 Pod을 종료할 때는 SIGTERM → 유예 기간 → SIGKILL 순서입니다.
| 단계 | Kubernetes | Spring Boot |
|---|---|---|
| 1 | preStop 훅 실행 | — |
| 2 | SIGTERM 전송 | Graceful Shutdown 시작 |
| 3 | terminationGracePeriodSeconds 대기 | timeout-per-shutdown-phase 대기 |
| 4 | SIGKILL (강제 종료) | 프로세스 즉시 종료 |
Spring Boot의
timeout-per-shutdown-phase는 Kubernetes의terminationGracePeriodSeconds보다 ** 짧게** 설정해야 합니다. 그래야 Spring이 먼저 정리를 마치고, Kubernetes가 SIGKILL을 보내기 전에 정상 종료됩니다.
# 예: K8s 60초, Spring 30초
# Pod spec
terminationGracePeriodSeconds: 60
# application.yml
spring.lifecycle.timeout-per-shutdown-phase: 30s
주의할 점
1. Graceful Shutdown은 기본값이 꺼져 있다
Spring Boot의 기본 server.shutdown은 immediate입니다. 명시적으로 graceful로 설정하지 않으면 진행 중인 요청이 끊어집니다. 많은 프로젝트가 이 설정 없이 프로덕션에 올라가 있습니다.
2. @Async 작업은 Graceful Shutdown 대상이 아니다
Graceful Shutdown이 기다리는 건 HTTP 요청 뿐입니다. @Async로 실행된 비동기 작업이나 @Scheduled 작업은 별도로 처리해야 합니다.
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setWaitForTasksToCompleteOnShutdown(true); // ← 이 설정
executor.setAwaitTerminationSeconds(30);
return executor;
}
3. 로드밸런서가 즉시 트래픽을 끊지 않는다
Kubernetes에서 Pod이 종료 중이어도, Service의 엔드포인트에서 제거되기까지 수 초의 지연 이 있습니다. 이 사이에 새 요청이 종료 중인 Pod으로 들어올 수 있습니다. preStop 훅에 sleep 5를 넣어 엔드포인트 제거를 기다리는 패턴이 흔합니다.
lifecycle:
preStop:
exec:
command: ["sleep", "5"]
정리
| 항목 | 설정 안 함 (immediate) | 설정함 (graceful) |
|---|---|---|
| 진행 중 요청 | 즉시 끊김 | 완료까지 대기 |
| 새 요청 | 받다가 갑자기 끊김 | 503 거부 |
| 배포 중 에러 | 발생 가능 | 안전 |
| 설정 | 없음 | server.shutdown: graceful |
프로덕션에서
server.shutdown: graceful은 선택이 아니라 필수 입니다. 특히 Kubernetes 환경에서는terminationGracePeriodSeconds와timeout-per-shutdown-phase의 관계를 반드시 맞춰야 합니다.