들어가기 전에 포스팅 계기
오랜만에 글 작성 해보려고 합니다. 최근에 사내에 SSE(Server-Sent Events)를 기반으로 개발한 기능이 있어서 Graceful shutdown 이 어떻게 동작하는지 추상적으로만 알고 있었고 동작 원리를 명확히 알고자 포스팅하게 됐습니다.
본론
Spring Boot 애플리케이션이 종료될 때, Graceful Shutdown은 중요한 역할을 합니다. 이는 처리 중인 요청을 안전하게 종료하고, 리소스를 정리하며, 종료 상태를 클라이언트에 알리는 메커니즘입니다. 본 글에서는 Spring Boot의 Graceful Shutdown이 어떻게 등록되고 실행되는지, 그리고 kill -15 신호를 받을 때 어떤 단계로 동작하는지 상세히 살펴보겠습니다.
1. Graceful Shutdown이란?
Graceful Shutdown은 애플리케이션 종료 시 요청 처리와 리소스 정리를 안전하게 수행하도록 설계된 메커니즘입니다. Spring Boot는 이를 통해 다음을 보장합니다:
- 요청 처리 완료
- 종료 중에도 진행 중인 HTTP 요청을 끝까지 처리한 뒤 연결을 종료합니다.
- 리소스 정리
- DB 커넥션, 메시지 큐 컨슈머, 스레드 풀 등 시스템 리소스를 안전하게 정리합니다.
- 장기 연결 종료
- WebSocket, SSE(Server-Sent Events), HTTP Keep-Alive와 같은 장기 연결을 클라이언트에게 적절히 알리고 안전하게 종료합니다.
장기 연결이란?
장기 연결은 클라이언트와 서버가 일정 시간 동안 지속적으로 데이터를 주고받는 네트워크 연결을 의미합니다. Graceful Shutdown에서 이를 안전하게 닫는 것이 중요한 이유는 클라이언트의 재연결 또는 정상적인 서비스 종료를 보장하기 위해서입니다. 주요 예는 다음과 같습니다:
- WebSocket
- 서버와 클라이언트 간 양방향 실시간 통신을 지원.
- 종료 시 연결을 명시적으로 닫아 클라이언트가 재연결을 시도할 수 있도록 준비해야 합니다.
- SSE(Server-Sent Events)
- 서버가 클라이언트로 데이터를 스트리밍하는 단방향 연결.
- 서버 종료 시 클라이언트가 이를 감지하고 적절히 처리할 수 있도록 해야 합니다.
- HTTP Keep-Alive
- 클라이언트와 서버 간 다수의 요청/응답을 단일 연결에서 처리.
- 연결을 닫기 전에 모든 요청이 안전히 완료되었는지 확인해야 합니다.
Spring Boot의 Graceful Shutdown은 이러한 장기 연결을 안전하게 닫고, 클라이언트가 서비스 중단을 최소화할 수 있도록 지원합니다.
2. Graceful Shutdown 등록 과정
Spring Boot는 SpringApplication.run() 실행 시 Graceful Shutdown을 등록합니다. 이 과정은 JVM의 Shutdown Hook을 통해 구현되며, 종료 시 실행할 핸들러와 리스너가 설정됩니다.
2.1 prepareEnvironment 호출
애플리케이션 실행 초기, prepareEnvironment 메서드에서 Graceful Shutdown 관련 핸들러가 등록됩니다.
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
내부 동작
- SpringApplicationRunListeners.environmentPrepared() 호출
- environmentPrepared 이벤트를 통해 각종 리스너가 초기화됩니다.
- Graceful Shutdown 핸들러가 등록됩니다.
- LoggingApplicationListener 초기화
- LoggingApplicationListener는 로그 시스템 초기화와 함께 종료 시 로그 정리를 위한 Shutdown Hook을 추가합니다.
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
- Shutdown Handlers 관리 Graceful Shutdown 중 실행할 추가 작업(Runnable)을 관리하기 위해 SpringApplicationShutdownHandlers가 초기화됩니다.
private class Handlers implements SpringApplicationShutdownHandlers {
private final Set<Runnable> actions = Collections.newSetFromMap(new IdentityHashMap<>());
@Override
public void add(Runnable action) {
synchronized (SpringApplicationShutdownHook.class) {
this.actions.add(action);
}
}
}
2.2 컨텍스트 초기화 및 Shutdown Hook 등록
Spring Boot는 애플리케이션의 컨텍스트를 초기화하며, JVM 종료 신호를 처리할 Shutdown Hook을 등록합니다.
private void refreshContext(ConfigurableApplicationContext context) {
if (this.registerShutdownHook) {
shutdownHook.registerApplicationContext(context);
}
refresh(context);
}
내부 동작
- registerApplicationContext 호출
- 애플리케이션 컨텍스트가 등록되며, ContextClosedEvent를 처리할 리스너가 추가됩니다.
void registerApplicationContext(ConfigurableApplicationContext context) {
addRuntimeShutdownHookIfNecessary();
synchronized (SpringApplicationShutdownHook.class) {
context.addApplicationListener(this.contextCloseListener);
this.contexts.add(context);
}
}
- Shutdown Hook 등록
- JVM의 Shutdown Hook에 SpringApplicationShutdownHook이 추가됩니다.
void addRuntimeShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(this, "SpringApplicationShutdownHook"));
}
3. Shutdown 발생 후 동작 과정
kill -15(SIGTERM) 신호를 받으면 JVM의 Shutdown Hook이 실행되며, Graceful Shutdown이 시작됩니다.
3.1 JVM의 Shutdown Hook 실행
운영체제에서 SIGTERM 신호가 전달되면, Spring Boot에서 등록한 SpringApplicationShutdownHook이 실행됩니다. 이는 JVM의 Shutdown Hook 메커니즘을 통해 트리거됩니다.
3.2 SpringApplicationShutdownHook.run 호출
SpringApplicationShutdownHook.run()은 Spring Boot 애플리케이션 종료 작업의 시작점입니다.
실행 코드
@Override
public void run() {
Set<ConfigurableApplicationContext> contexts;
Set<ConfigurableApplicationContext> closedContexts;
Set<Runnable> actions;
synchronized (SpringApplicationShutdownHook.class) {
this.inProgress = true;
contexts = new LinkedHashSet<>(this.contexts);
closedContexts = new LinkedHashSet<>(this.closedContexts);
actions = new LinkedHashSet<>(this.handlers.getActions());
}
contexts.forEach(this::closeAndWait); // 컨텍스트 종료
closedContexts.forEach(this::closeAndWait);
actions.forEach(Runnable::run);
}
3.3 애플리케이션 컨텍스트 종료
애플리케이션 컨텍스트는 closeAndWait 메서드를 통해 종료됩니다.
private void closeAndWait(ConfigurableApplicationContext context) {
if (!context.isActive()) {
return; // 이미 비활성화된 컨텍스트는 스킵
}
context.close(); // 컨텍스트 닫기
}
컨텍스트 종료의 주요 로직은 AbstractApplicationContext.close() 메서드에서 처리되며, 종료 이벤트(ContextClosedEvent)가 발행되고, 빈 소멸 및 리소스 정리가 이루어집니다.
리소스 정리는 아래 스크린샷 처럼 DB, Kafka, MQ 등 Thread Pool 등 정리됩니다.
3.4 Tomcat Graceful Shutdown
Tomcat과 같은 내장 웹 서버는 요청 처리를 완료한 후 종료됩니다. 새 요청을 차단하고, 진행 중인 요청이 모두 완료될 때까지 대기 후 연결을 닫습니다.
void shutDownGracefully(GracefulShutdownCallback callback) {
logger.info("Commencing graceful shutdown...");
new Thread(() -> doShutdown(callback), "tomcat-shutdown").start();
}
4. 사용자 정의 Graceful Shutdown 작업
Spring Boot는 종료 시 커스텀 로직을 추가할 수 있는 메커니즘을 제공합니다.
4.1 @EventListener를 통한 종료 작업
@Component
public class CustomShutdownListener {
@EventListener(ContextClosedEvent.class)
public void onShutdown() {
System.out.println("Graceful shutdown logic executed...");
}
}
4.2 DisposableBean을 통한 리소스 정리
@Component
public class ResourceCleanup implements DisposableBean {
@Override
public void destroy() {
System.out.println("Releasing resources...");
}
}
5. 정리
Spring Boot에서 Graceful Shutdown은 애플리케이션 종료 시 요청 처리 완료와 리소스 정리를 보장하며, 이를 통해 안정적이고 신뢰할 수 있는 서비스를 제공합니다. 다음은 전체적인 과정과 각 단계에서 수행되는 작업입니다:
5.1 등록 과정
- 애플리케이션 초기화 중 Graceful Shutdown 관련 핸들러와 리스너가 등록됩니다.
- JVM Shutdown Hook을 통해 종료 작업이 실행될 준비를 합니다.
5.2 Shutdown 발생 후
- Shutdown Hook 실행 JVM의 Shutdown Hook을 통해 **SpringApplicationShutdownHook.run()*이 호출됩니다.
- 컨텍스트 종료 종료 이벤트(ContextClosedEvent)가 발행되며, 빈 소멸 및 리소스 정리가 이루어집니다.
- Tomcat Graceful Shutdown 현재 요청을 모두 처리한 뒤, 서버 연결이 안전히 종료됩니다.
5.3 사용자 정의 작업
- @EventListener 또는 DisposableBean을 통해 종료 시 추가 작업을 설정할 수 있습니다.
5.4 주요 이점
- 데이터 손실 방지: 요청 처리가 중단되지 않고 완료됩니다.
- 리소스 누수 방지: DB 커넥션, 스레드 풀 등 시스템 자원이 안전하게 정리됩니다.
- 서비스 안정성 강화: 장기 연결을 안전하게 닫아 클라이언트와 서버 간 신뢰를 유지합니다.
'Spring > Spring Boot' 카테고리의 다른 글
Spring Boot의 SSE(Server-Sent Events) Graceful Shutdown 동작 원리 (0) | 2025.01.05 |
---|---|
@Log4j2 vs @Slf4j with log4j2 (0) | 2021.01.22 |
Spring Boot AutoConfiguration 자동 설정, 원리 (0) | 2021.01.03 |