Graceful Shutdown, 서버를 그냥 끄면 안 되는 이유

백엔드 서버를 운영하다 보면 배포, 재시작, 스케일 다운 등 다양한 이유로 서버를 종료해야 하는 순간이 생긴다.
이때 "서버를 어떻게 종료하느냐"는 생각보다 중요하다.
갑자기 서버가 꺼진다면?
쇼핑몰 서버를 운영 중이라고 가정해보자.
사용자가 결제 버튼을 누른 직후, 서버가 배포를 위해 갑자기 종료되게 된다.
이 순간 어떤 일이 벌어질까?
- 결제 트랜잭션이 중간에 끊겨 데이터가 불일치할 수 있다.
- 주문 정보가 DB에 저장되지 못해 데이터가 유실될 수 있다.
- 사용자는 결제는 됐는데 주문이 안 된 최악의 경험을 하게 된다.
이런 문제를 방지하기 위한 개념이 바로 Graceful Shutdown(우아한 종료) 이다.
Graceful Shutdown이란?
Graceful Shutdown은 애플리케이션이 종료 신호를 받았을 때, 즉시 꺼지는 것이 아니라 아래 순서로 안전하게 종료하는 방식이다.
- 새로운 요청 수신을 차단한다
- 현재 처리 중인 요청을 모두 완료한다
- DB 커넥션, 파일 핸들 등 리소스를 정리한다
- 프로세스를 종료한다
반대 개념인 Immediate Shutdown(즉시 종료) 은 처리 중인 요청이 있든 없든 바로 프로세스를 죽인다.
| 구분 | 즉시 종료 | Graceful Shutdown |
|---|---|---|
| 처리 중인 요청 | ❌ 강제 중단 | ✅ 완료 후 종료 |
| 데이터 안전성 | ❌ 손실 위험 | ✅ 보장 |
| 리소스 정리 | ❌ 미정리 | ✅ 정상 반납 |
SIGTERM vs SIGKILL
리눅스/유닉스 환경에서는 프로세스에 시그널(Signal) 을 보내서 종료를 요청한다.
대표적으로 두 가지가 있다.
SIGTERM (Signal Terminate)
"종료해주세요" 라고 정중히 요청하는 신호다.
프로세스는 이 신호를 핸들링(처리) 할 수 있다.
즉, 신호를 받은 뒤 현재 작업을 마무리하고 종료할 수 있다.
Graceful Shutdown은 이 신호를 기반으로 동작한다.
SIGKILL (Signal Kill)
"지금 당장 죽어" 라고 강제하는 신호다.
프로세스가 이 신호를 핸들링할 수 없다.
운영체제가 직접 프로세스를 즉시 강제 종료하고, 어떤 정리 작업도 실행되지 않는다.
배포나 재시작 시에는 SIGTERM → Graceful Shutdown 순서로 진행하는 것이 이상적이다.
Spring Boot에서 Graceful Shutdown 설정하기
Spring Boot는 2.3 버전부터 Graceful Shutdown을 공식 지원한다.
설정은 application.properties 또는 application.yml에 두 줄이면 된다.
동작 방식
- SIGTERM 수신 → 새 요청 차단 시작
- 기존 처리 중인 요청 완료 대기
- 20초 이내 완료되면 정상 종료
- 20초 초과 시 강제 종료 (데드락, 무한 루프 방지)
왜 타임아웃이 필요한가?
처리 중인 요청이 데드락이나 무한 루프에 빠지면, 아무리 기다려도 완료되지 않는다.
타임아웃 없이 무한정 기다리면 서버가 영원히 종료되지 않는 상황이 발생할 수 있어서, 적절한 타임아웃 설정은 필수다.
NestJS에서 Graceful Shutdown 설정하기
Spring Boot처럼 설정 한 줄로 끝나지 않는다.
enableShutdownHooks()를 켜고, 각 모듈에서 직접 정리 로직을 작성해야 한다.
이후 각 서비스에서 OnModuleDestroy를 구현하면 종료 시 자동으로 호출된다.
DB 커넥션 정리도 여기서 하면 된다.
타임아웃은 Spring Boot와 달리 직접 처리해야 한다.
Promise.race()로 걸어두지 않으면 서버가 영원히 안 꺼지는 상황이 생길 수 있다.
정리
Graceful Shutdown은 단순히 "서버를 잘 끄는 것"이 아니다.
운영 환경에서 데이터 무결성을 지키고, 사용자 경험을 보호하는 중요한 운영 전략이다.
배포 파이프라인 구성할 때 꼭 챙겨두자.
참고 자료