서비스를 운영하다 보면 서버를 재시작하거나 종료해야 하는 순간이 오기 마련이다. 이때 진행 중이던 요청을 중단하지 않고 마무리까지 처리한 뒤 안전하게 종료하는 방식이 바로 Graceful Shutdown이다.
Graceful Shutdown이 구현되지 않으면 아래와 같은 문제가 발생할 수 있다.
따라서 서비스 중단이 발생하더라도 안정적으로 종료되도록 처리하는 방식은 필수적이다.
Spring Boot 애플리케이션에서 Graceful Shutdown은 아래 순서로 진행된다.
SIGTERM
시그널 수신 (예: kill -15
)SpringApplicationContext
종료@PreDestroy
, DisposableBean.destroy()
, SmartLifecycle.stop()
)System.exit(0)
)Linux에서 process를 종료할 때 kill
명령어를 사용한다. kill
명령어의 옵션은 아래와 같이 다양하다.
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGEMT 8) SIGFPE 9) SIGKILL 10) SIGBUS
11) SIGSEGV 12) SIGSYS 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGURG 17) SIGSTOP 18) SIGTSTP 19) SIGCONT 20) SIGCHLD
21) SIGTTIN 22) SIGTTOU 23) SIGIO 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGPWR 30) SIGUSR1
31) SIGUSR2 32) SIGRTMIN 33) SIGRTMIN+1 34) SIGRTMIN+2 35) SIGRTMIN+3
36) SIGRTMIN+4 37) SIGRTMIN+5 38) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8
41) SIGRTMIN+9 42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13
46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMIN+16 49) SIGRTMAX-15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9
56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4
61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
많이 사용하는 옵션은 -9와 -15이다. 두 옵션은 큰 차이가 있다.
kill -9 (SIGKILL)
: 프로세스를 즉시 종료한다. 따라서 처리중이던 작업들의 유무와 관계 없이 즉시 종료된다.kill -15 (SIGTERM)
: 프로세스를 정상적으로 종료한다. 소프트웨어 프로세스에게 종료하라는 시그널을 준다고 생각하면 된다.따라서 프로세스를 정상적으로 종료시켜줘야 Graceful Shutdown을 사용할 수 있다. kill -15
명령어가 실행되면 Spring의 SpringApplicationShutdownHook
이라는 객체를 통해 Spring을 종료시키기 시작한다.
server:
shutdown: graceful
package org.springframework.boot.web.embedded.tomcat;
public class TomcatWebServer implements WebServer {
// ...
private final GracefulShutdown gracefulShutdown;
// ...
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
initialize();
}
}
이 코드는 application.yml
에서 server.shutdown=graceful
설정이 있으면 GracefulShutdown
객체를 생성하여 Tomcat에 연결해주는 역할을 한다.
// GracefulShutdown.java (Spring Boot 내부 구현)
public class GracefulShutdown {
public GracefulShutdown(Tomcat tomcat) {
this.tomcat = tomcat;
}
public void shutDown() {
// 톰캣의 Connector를 꺼서 더 이상 새로운 요청을 받지 않게 함
this.tomcat.getConnector().pause();
// 요청 중이던 Thread들은 계속 작업 수행 중
// -> 이후 shutdown timeout 내에 완료되면 종료
}
}
Connector의 pause()
메서드는 새로운 요청을 더 이상 받지 않도록 설정하지만 이미 처리 중인 요청은 그대로 유지된다.
@Override
public void shutDownGracefully(GracefulShutdownCallback callback) {
if (this.gracefulShutdown == null) {
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
return;
}
this.gracefulShutdown.shutDownGracefully(callback);
}
TomcatWebServer
또는 JettyWebServer
에서 shutDownGracefully()
가 호출되면 실제 종료 작업이 시작된다
final class GracefulShutdown {
private final Server server;
private final Supplier<Integer> activeRequests;
private volatile boolean shuttingDown = false;
void shutDownGracefully(GracefulShutdownCallback callback) {
for (Connector connector : this.server.getConnectors()) {
shutdown(connector, true);
}
this.shuttingDown = true;
new Thread(() -> awaitShutdown(callback), "jetty-shutdown").start();
}
private void awaitShutdown(GracefulShutdownCallback callback) {
while (this.shuttingDown && this.activeRequests.get() > 0) {
sleep(100); // 100ms 대기하며 요청 종료 감시
}
this.shuttingDown = false;
long remaining = this.activeRequests.get();
if (remaining == 0) {
logger.info("Graceful shutdown complete");
callback.shutdownComplete(GracefulShutdownResult.IDLE);
} else {
logger.info("Graceful shutdown aborted with " + remaining + " request(s) still active");
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
}
}
}
activeRequests.get()
값을 확인하여 요청이 모두 끝날 때까지 while
루프로 대기IDLE
결과 리턴REQUESTS_ACTIVE
상태로 종료무한 루프나 외부 API 지연 등으로 요청이 끝나지 않을 경우를 방지하기 위해 shutdown phase당 최대 대기 시간을 설정할 수 있다.
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
GracefulShutdown의 대기 시간이 30초를 초과하면 Spring은 더 이상 기다리지 않고 다음 종료 단계로 넘어간다. 이때 GracefulShutdown의 abort()
메서드가 호출되어 종료 루프를 빠져나간다.
final class GracefulShutdown {
// ...
void abort() {
this.aborted = false;
}
}
이로써 awaitShutdown()
루프는 멈추고 shutdownComplete()
콜백을 강제로 호출한다.
아래와 같이 요청을 처리하는데 10s가 소요되는 Controller로 테스트를 해봤다.
@RestController
public class GracefulShutdownController {
private final Logger logger = LoggerFactory.getLogger(GracefulShutdownController.class);
@PostMapping("/graceful-test")
public ResponseEntity<Void> checkGracefulShutdown(@RequestBody GracefulShutDownRequest request) {
logger.info("요청 수신 seq : ({})", request.getSequence());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 인터럽트 플래그 재설정
throw new RuntimeException("스레드가 인터럽트됨", e);
}
logger.info("요청 처리 완료 seq : ({})", request.getSequence());
return ResponseEntity.noContent().build();
}
}
테스트 시나리오는 아래와 같다.
2025-07-08 00:47:54.111 [http-nio-8080-exec-1] INFO org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet' traceId=
2025-07-08 00:47:54.112 [http-nio-8080-exec-1] INFO org.springframework.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet' traceId=
2025-07-08 00:47:54.114 [http-nio-8080-exec-1] INFO org.springframework.web.servlet.DispatcherServlet - Completed initialization in 2 ms traceId=
2025-07-08 00:47:54.278 [http-nio-8080-exec-1] INFO com.example.giftrecommender.controller.GracefulShutdownController - 요청 수신 seq : (1) traceId=b58d4324-0183-4107-9174-780cf2f8727a
2025-07-08 00:47:56.285 [http-nio-8080-exec-2] INFO com.example.giftrecommender.controller.GracefulShutdownController - 요청 수신 seq : (2) traceId=9cb8d050-2fd9-4722-b264-22ee0203a910
2025-07-08 00:47:58.387 [SpringApplicationShutdownHook] INFO org.springframework.boot.web.embedded.tomcat.GracefulShutdown - Commencing graceful shutdown. Waiting for active requests to complete traceId=
로그를 통해 Connector가 pause되고 요청이 정상 처리된 뒤 종료되는 과정을 직접 확인할 수 있어 Graceful Shutdown이 실제 운영 환경에서 어떻게 안정적으로 작동하는지를 이해할 수 있었다.
Graceful Shutdown은 서버를 재시작하거나 종료할 때 진행 중인 요청을 안전하게 마무리하기 위한 필수 개념임을 실습을 통해 확인할 수 있었다. 단순히 서버를 끄는 것이 아니라 SIGTERM
수신 후 Web 서버의 Connector를 중단하고 요청 처리가 끝날 때까지 대기한 뒤 Spring Context를 종료하는 일련의 과정은 안정적인 운영 환경 구축에 있어 매우 중요하다. 특히 timeout-per-shutdown-phase
설정을 통해 무한 대기나 데드락 상황을 방지할 수 있고 테스트를 통해 실제로 요청이 종료될 때까지 응답을 보장해주는 과정을 확인하며 Graceful Shutdown이 서비스 신뢰성을 높이는 핵심 기술임을 체감할 수 있었다.