백엔드 애플리케이션을 개발하고 운영하다 보면 '배포'는 피할 수 없는 일상적인 작업입니다. 새로운 기능을 추가하거나 버그를 수정하기 위해 기존 프로세스를 종료하고 새로운 프로세스를 실행합니다. 이때 중요한 것은 "어떻게 종료하느냐" 입니다.
애플리케이션이 정상적으로 시작되는 것만큼이나 정상적으로 종료되는 과정 또한 서비스의 안정성에 큰 영향을 미칩니다. 이번 글에서는 Graceful Shutdown(우아한 종료)의 필요성과 OS 시그널의 차이, Spring Boot 환경에서의 구체적인 동작 원리, 그리고 실무 운영 환경(로드밸런서, 쿠버네티스)에서의 고려사항까지 알아보겠습니다.
Graceful Shutdown(우아한 종료)이란 애플리케이션이 종료 신호를 받았을 때 즉시 전원을 끄듯 멈추는 것이 아니라, 현재 처리 중인 작업들을 모두 안전하게 마무리하고 리소스를 정리한 뒤 종료하는 방식을 의미합니다.
서버 애플리케이션 관점에서 보면 다음과 같은 절차를 따릅니다.
만약 요청을 처리하는 도중에 프로세스가 즉각적으로(Hard Shutdown) 종료된다면 다음과 같은 문제가 발생할 수 있습니다.
Connection Reset 오류나 500 Internal Server Error를 받게 됩니다.리눅스 및 유닉스 환경에서 프로세스를 종료할 때 사용하는 kill 명령어는 프로세스에 특정 시그널(Signal)을 보냅니다. Graceful Shutdown을 위해서는 SIGTERM과 SIGKILL의 차이를 명확히 알아야 합니다.
kill -9 {PID}
kill 명령어를 사용하면 기본적으로 이 시그널이 전송됩니다.kill {PID} # 기본값 -15 (SIGTERM)
Spring Boot 2.3 버전부터는 설정을 통해 매우 간단하게 Graceful Shutdown을 적용할 수 있습니다.
# Graceful Shutdown 활성화 (기본값: immediate)
server.shutdown=graceful
# 종료 대기 타임아웃 설정 (기본값: 30s)
spring.lifecycle.timeout-per-shutdown-phase=20s
server.shutdown을 graceful로 설정하면 Spring Boot의 내장 웹 서버는 종료 시그널을 받았을 때 새로운 요청을 받지 않고 기존 요청을 처리하기 위해 대기합니다.
설정에서 spring.lifecycle.timeout-per-shutdown-phase는 Spring 컨테이너의 라이프사이클 빈(Bean)들이 종료되는 데 기다려주는 전체 시간을 의미합니다. 하지만 실제 운영 환경에서는 Tomcat 자체의 내부 Graceful Shutdown 타임아웃과 구분해서 이해할 필요가 있습니다. 일반적으로 Spring의 설정이 우선순위를 가지며 전체 종료 과정을 제어하지만, 아주 정밀한 튜닝이 필요한 경우 톰캣 레벨의 설정이 별도로 존재함을 인지해야 합니다.
Spring Boot는 여러 내장 웹 서버를 지원하며, 서버마다 Graceful Shutdown 구현에 차이가 있습니다.
Graceful Shutdown 설정만으로는 '무중단 배포'를 완벽하게 보장하기 어렵습니다. 실제 서비스는 로드밸런서(LB) 뒤에 존재하기 때문입니다.
Spring Boot가 종료를 시작(SIGTERM 수신)하면 새로운 요청을 거부하지만, 로드밸런서가 이를 인지하고 트래픽을 차단하기까지 시차가 발생할 수 있습니다.
Deregistration Delay (기본 300초) 설정이 있습니다.preStop hook을 사용하여 로드밸런서(Service) 갱신 시간을 벌어주어야 합니다.따라서 이상적인 종료 시나리오는 다음과 같습니다.
preStop 등을 이용해 잠시 대기 (기존 요청 처리 + LB 갱신 대기)SIGTERM 수신 -> Spring Boot Graceful Shutdown 시작이때, Kubernetes의 terminationGracePeriodSeconds는 Spring Boot의 timeout-per-shutdown-phase보다 넉넉하게 설정해야 프로세스가 강제 종료(SIGKILL) 당하는 것을 방지할 수 있습니다.
Spring Boot와 내장 Tomcat은 어떻게 이 기능을 구현했을까요? 내부 코드를 통해 동작 원리를 살펴보겠습니다.
요청 처리에 15초가 걸리는 API를 만들고, 요청 중에 서버를 종료하면 다음과 같은 로그를 확인할 수 있습니다.
INFO ... GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
INFO ... GracefulShutdownController : 요청 처리 완료 seq : (1)
INFO ... GracefulShutdown : Graceful shutdown complete
서버 종료 신호가 들어왔음에도 Waiting for active requests to complete 메시지와 함께 기존 요청 처리가 완료될 때까지 기다렸다가 종료됩니다.
Spring Boot가 구동될 때 server.shutdown=graceful 설정이 되어 있다면, TomcatWebServer는 GracefulShutdown 객체를 생성하여 할당합니다.
final class GracefulShutdown {
// ...
private void doShutdown(GracefulShutdownCallback callback) {
List<Connector> connectors = getConnectors();
// 1. 커넥터 종료 (새로운 요청 차단)
connectors.forEach(this::close);
try {
for (Container host : this.tomcat.getEngine().findChildren()) {
for (Container context : host.findChildren()) {
// 2. 활성화된 요청이 있는지 지속적으로 확인
while (isActive(context)) {
if (this.aborted) {
logger.info("Graceful shutdown aborted...");
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
return;
}
Thread.sleep(50); // 50ms 주기로 체크
}
}
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
// ...
}
}
connectors.forEach(this::close)가 실행되면 Tomcat은 새로운 TCP 연결 수립을 거부합니다. 하지만 중요한 점은 기존 Keep-Alive 상태의 연결입니다. Tomcat은 새로운 요청은 막지만, 이미 맺어진 Keep-Alive 연결을 통해 들어와 처리 중인 요청은 끊지 않고 유지합니다. 이후 해당 요청 처리가 끝나면 연결을 자연스럽게 종료합니다.
isActive(context) 메서드는 단순히 "무언가 돌고 있다"를 추측하는 것이 아닙니다. 구체적으로는 Tomcat 내부의 StandardWrapperValve 클래스가 관리하는 processingCount 값을 확인합니다.
processingCount > 0: 현재 서블릿이 요청을 처리 중임processingCount == 0: 처리 중인 요청 없음 (종료 가능)즉, Spring Boot의 Graceful Shutdown은 이 카운트가 0이 될 때까지(혹은 타임아웃이 될 때까지) 루프를 돌며 대기하는 구조입니다.
Graceful Shutdown은 단순한 코드 설정 한 줄(server.shutdown=graceful)로 시작하지만, 그 뒤에는 OS 시그널, 웹 서버의 커넥션 관리, 그리고 인프라 레이어의 트래픽 제어까지 연결된 깊이 있는 기술이 숨어 있습니다.
안정적인 서비스를 운영하기 위해서는 코드 레벨의 설정뿐만 아니라, 로드밸런서와 배포 환경(K8s 등)의 종료 정책을 함께 고려하여 "진정한 우아한 종료"를 설계해야 합니다.
참고)
https://velog.io/@byeongju/SpringBoot%EC%9D%98-Graceful-Shutdown
https://effectivesquid.tistory.com/entry/JVM%EC%9D%98-%EC%A2%85%EB%A3%8C%EC%99%80-Graceful-Shutdown
https://www.baeldung.com/spring-boot-web-server-shutdown