본문 바로가기

Spring Framework/Spring Boot

[Spring Boot] 스프링 부트 애플리케이션 종료

개요 

실전 스프링 부트 책을 읽던 중 shutdown hook 개념과 안전 종료 개념에 대해 배울 수 있었다.

 

관련 개념에 대해 알아보고,

스프링 부트의 종료 로직을 좀 더 자세히 알아보자!

 

 

관련 개념

1. Shutdown Hook

- 개념: JVM이 종료되기 전에 실행되는 스레드

- 이용: 스프링 부트에서는 자동으로 shutdown hook을 등록하여 애플리케이션 컨텍스트를 안전하게 종료한다.

 

 

2. 안전 종료(Graceful Shutdown)

- 개념: 진행 중인 요청을 완료한 후 서버를 종료하는 방식

- 지원 버전: 스프링 부트 2.3부터 지원.

- 설정 방법

# application.yml
server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s  # 종료 대기 시간 (기본값: 30초)

 

 

종료 로직

1. 종료 신호 수신

종료 트리거

// 1-1. 외부 종료 신호
- SIGTERM (kill 명령어)
- SIGINT (Ctrl+C)
- System.exit() 호출
- IDE에서 애플리케이션 중지
- 도커 컨테이너 stop

// 1-2. 애플리케이션 내부 종료
- /actuator/shutdown 엔드포인트 호출 (활성화된 경우)
- SpringApplication.exit() 호출
- ConfigurableApplicationContext.close() 호출

 

2. Shutdown Hook 실행

JVM Shutdown Hook 등록 및 실행

// 2-1. 스프링 부트가 자동으로 등록한 Shutdown Hook 실행
public class SpringBootShutdownHook extends Thread {
    @Override
    public void run() {
        // 애플리케이션 컨텍스트 종료 시작
        applicationContext.close();
    }
}

// 2-2. 커스텀 Shutdown Hook들도 동시에 실행됨
Runtime.getRuntime().addShutdownHook(customHook);

 

3. 안정 종료(Graceful Shutdown) 시작

웹 서버 종료 과정

// 3-1. 새로운 요청 수락 중단
server.shutdown(); // 더 이상 새로운 연결 수락 안함

// 3-2. 진행 중인 요청 완료 대기
// - 활성 연결들의 요청 처리 완료까지 대기
// - spring.lifecycle.timeout-per-shutdown-phase 시간만큼 대기

// 3-3. 타임아웃 후 강제 종료
if (timeout exceeded) {
    server.shutdownNow(); // 강제 종료
}

 

4. ApplicationContext 종료 과정

컨텍스트 종료 이벤트 발행

// 4-1. ContextClosedEvent 발행
@EventListener
public void handleContextClosed(ContextClosedEvent event) {
    System.out.println("컨텍스트 종료 이벤트 수신");
}

 

5. SmartLifecycle Bean 종료

Phase별 순차 종료

// 5-1. SmartLifecycle 구현체들을 Phase 순서로 종료
@Component
public class CustomLifecycle implements SmartLifecycle {
    
    @Override
    public int getPhase() {
        return 1000; // 낮은 숫자부터 먼저 종료
    }
    
    @Override
    public void stop() {
        System.out.println("Phase " + getPhase() + " 종료");
    }
}

// 종료 순서: Phase 값이 낮은 것부터 높은 순으로
// Phase -1000 → Phase 0 → Phase 1000 → Phase Integer.MAX_VALUE

 

6. 일반 Bean 종료

6-1. @PreDestroy 메서드 실행

// 6-1. 모든 Bean의 @PreDestroy 메서드 실행
@Component
public class ServiceBean {
    
    @PreDestroy
    public void cleanup() {
        System.out.println("@PreDestroy 실행 - 리소스 정리");
        // 캐시 정리, 연결 종료 등
    }
}

 

6-2. DisposableBean 인터페이스 실행

// 6-2. DisposableBean.destroy() 메서드 실행
@Component
public class ResourceBean implements DisposableBean {
    
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean.destroy() 실행");
        // 리소스 해제 로직
    }
}

 

6-3. @Bean destroyMethod 실행

// 6-3. @Bean 어노테이션의 destroyMethod 실행
@Configuration
public class BeanConfig {
    
    @Bean(destroyMethod = "shutdown")
    public CustomService customService() {
        return new CustomService();
    }
}

public class CustomService {
    public void shutdown() {
        System.out.println("destroyMethod 실행");
    }
}

 

7. 인프라스트럭처 Bean 종료

데이터소스 및 연결 풀 종료

// 7-1. 데이터베이스 연결 풀 종료
HikariDataSource → 연결 풀 종료
JPA EntityManagerFactory → 엔티티 매니저 팩토리 종료

// 7-2. 메시지 브로커 연결 종료
RabbitMQ ConnectionFactory → 연결 종료
Kafka Producer/Consumer → 연결 종료

 

8. 스레드 풀 종료

ExecutorService 종료

// 8-1. 애플리케이션의 모든 스레드 풀 종료
@Async 스레드 풀 종료
@Scheduled 스레드 풀 종료
커스텀 ExecutorService 종료

// 8-2. 스레드 풀 종료 로직
executorService.shutdown();
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
    executorService.shutdownNow();
}

 

9. 최종 정리

JVM 리소스 정리

// 9-1. 남은 리소스 정리
- 열린 파일 디스크립터 정리
- 네트워크 소켓 정리
- 임시 파일 정리

// 9-2. JVM 종료
System.exit(exitCode);