Graceful Shutdown이란?

송현진·2025년 7월 7일
0

Spring Boot

목록 보기
22/23

서비스를 운영하다 보면 서버를 재시작하거나 종료해야 하는 순간이 오기 마련이다. 이때 진행 중이던 요청을 중단하지 않고 마무리까지 처리한 뒤 안전하게 종료하는 방식이 바로 Graceful Shutdown이다.

왜 필요한가?

Graceful Shutdown이 구현되지 않으면 아래와 같은 문제가 발생할 수 있다.

  • 클라이언트는 응답을 받지 못하고 오류 발생
  • 서버는 트랜잭션, 작업 로그, 파일 저장 중단 -> 데이터 손실 가능성
  • 운영 환경에서 장애나 요청 누락 발생 가능

따라서 서비스 중단이 발생하더라도 안정적으로 종료되도록 처리하는 방식은 필수적이다.

Graceful Shutdown의 주요 흐름

Spring Boot 애플리케이션에서 Graceful Shutdown은 아래 순서로 진행된다.

  1. SIGTERM 시그널 수신 (예: kill -15)
  2. Web Server(Tomcat, Jetty)의 Connector 중단 -> 새 요청 차단
  3. 기존 요청 처리 완료까지 대기
  4. SpringApplicationContext 종료
  5. Bean 소멸 단계 (@PreDestroy, DisposableBean.destroy(), SmartLifecycle.stop())
  6. JVM 종료 (System.exit(0))

사용 방법

서버 종료 방식 (linux kill)

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

1. TomcatWebServer에서 GracefulShutdown 객체 생성

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에 연결해주는 역할을 한다.

2. Connector 중단 및 요청 완료 대기

// 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() 메서드는 새로운 요청을 더 이상 받지 않도록 설정하지만 이미 처리 중인 요청은 그대로 유지된다.

3. 종료 시 GracefulShutdown 트리거

@Override
public void shutDownGracefully(GracefulShutdownCallback callback) {
	if (this.gracefulShutdown == null) {
		callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
		return;
	}
	this.gracefulShutdown.shutDownGracefully(callback);
}

TomcatWebServer 또는 JettyWebServer에서 shutDownGracefully()가 호출되면 실제 종료 작업이 시작된다

4. Jetty 내부 구현 예시

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 루프로 대기
  • 100ms 단위로 반복 체크
  • 모든 요청 완료 -> IDLE 결과 리턴
  • 일부 남은 경우 -> REQUESTS_ACTIVE 상태로 종료

Graceful Shutdown의 Timeout 처리

무한 루프나 외부 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();
    }
}

테스트 시나리오는 아래와 같다.

  • 2번의 요청을 보낸다.
  • 2번째 요청을 보낸 후 10초가 지나기 전(요청이 완료되기전)에 서버를 종료시킨다.
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이 서비스 신뢰성을 높이는 핵심 기술임을 체감할 수 있었다.

profile
개발자가 되고 싶은 취준생

0개의 댓글