Graceful Shutdown이란?
Graceful Shutdown은 애플리케이션이 종료될 때 현재 처리 중인 작업을 안전하게 완료하고, 리소스를 정리한 후 종료하는 방식입니다. 갑작스러운 종료(Abrupt Shutdown)와 달리, 진행 중인 요청을 처리하고 데이터 손실을 방지합니다.
왜 필요한가?
일반적인 종료 문제점
- 처리 중인 HTTP 요청이 중단됨 - 데이터베이스 트랜잭션이 롤백됨 - 파일 쓰기 작업이 중단되어 데이터 손실 - 외부 API 호출이 타임아웃 - 메시지 큐의 메시지가 손실됨
|
Graceful Shutdown의 이점
- 데이터 무결성: 진행 중인 트랜잭션을 완료
- 사용자 경험: 진행 중인 요청에 대한 정상 응답
- 리소스 정리: 연결, 파일, 스레드 등 정리
- 무중단 배포: 롤링 업데이트 시 서비스 중단 최소화
JVM Shutdown Hook
JVM은 종료 시 등록된 Shutdown Hook을 실행합니다.
기본 사용법
public class ShutdownHookExample { public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("Shutdown Hook 실행 중...");
closeConnections(); saveState();
System.out.println("정리 작업 완료"); }));
System.out.println("애플리케이션 실행 중...");
}
private static void closeConnections() { }
private static void saveState() { } }
|
Shutdown Hook 실행 시점
다음과 같은 경우에 Shutdown Hook이 실행됩니다:
- 정상 종료 (System.exit())
- SIGTERM 시그널 수신
- Ctrl+C (SIGINT)
- OutOfMemoryError 등 치명적 오류
실행되지 않는 경우:
- SIGKILL (kill -9)
- JVM 크래시
- 운영체제 강제 종료
Spring Boot에서의 Graceful Shutdown
설정 방법
Spring Boot 2.3 이상에서는 내장 지원합니다.
server: shutdown: graceful
spring: lifecycle: timeout-per-shutdown-phase: 30s
|
또는 properties 파일:
server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=30s
|
동작 방식
1. 종료 신호 수신 (SIGTERM) ↓ 2. 새로운 요청 거부 (503 Service Unavailable) ↓ 3. 진행 중인 요청 처리 완료 대기 ↓ 4. 타임아웃 또는 모든 요청 완료 ↓ 5. Bean 소멸 (PreDestroy) ↓ 6. 애플리케이션 종료
|
PreDestroy를 활용한 정리 작업
@Component public class GracefulShutdownHandler {
private final ExecutorService executorService; private final DataSource dataSource;
@PreDestroy public void onShutdown() { log.info("Graceful Shutdown 시작");
shutdownExecutor();
saveCache();
closeConnections();
log.info("Graceful Shutdown 완료"); }
private void shutdownExecutor() { executorService.shutdown(); try { if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) { executorService.shutdownNow(); } } catch (InterruptedException e) { executorService.shutdownNow(); Thread.currentThread().interrupt(); } }
private void saveCache() { }
private void closeConnections() { } }
|
SmartLifecycle 인터페이스
더 세밀한 제어가 필요한 경우 SmartLifecycle을 구현합니다.
@Component public class GracefulShutdownLifecycle implements SmartLifecycle {
private volatile boolean running = false;
@Override public void start() { running = true; log.info("애플리케이션 시작"); }
@Override public void stop() { log.info("Graceful Shutdown 시작");
waitForTasksToComplete();
running = false; log.info("Graceful Shutdown 완료"); }
@Override public boolean isRunning() { return running; }
@Override public int getPhase() { return Integer.MAX_VALUE; }
private void waitForTasksToComplete() { } }
|
Kubernetes 환경에서의 Graceful Shutdown
Pod 종료 프로세스
1. kubectl delete pod 실행 ↓ 2. Pod 상태를 Terminating으로 변경 ↓ 3. SIGTERM 신호를 컨테이너에 전송 ↓ 4. terminationGracePeriodSeconds 대기 (기본 30초) ↓ 5. 타임아웃 시 SIGKILL 전송
|
설정 예시
apiVersion: v1 kind: Pod metadata: name: my-app spec: terminationGracePeriodSeconds: 60 containers: - name: app image: my-app:latest lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 15"]
|
Spring Boot + Kubernetes 베스트 프랙티스
spec: terminationGracePeriodSeconds: 60 containers: - name: app lifecycle: preStop: exec: command: ["sh", "-c", "sleep 10"]
|
server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 45s
|
실전 예제
HTTP 요청 처리 중 종료
@RestController public class LongRunningController {
@GetMapping("/long-task") public String longTask() throws InterruptedException { log.info("Long task 시작");
Thread.sleep(30000);
log.info("Long task 완료"); return "작업 완료"; } }
|
Graceful Shutdown 설정 시:
- 종료 신호를 받아도 이미 시작된 요청은 완료됨
- 새로운 요청은 503 응답
일반 종료 시:
- 요청이 중간에 끊김
- 클라이언트는 연결 오류 발생
비동기 작업 처리
@Configuration public class AsyncConfig {
@Bean public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(60); executor.setThreadNamePrefix("async-"); executor.initialize(); return executor; } }
|
메시지 큐 리스너
@Component public class MessageQueueListener {
private final AtomicInteger processingCount = new AtomicInteger(0);
@RabbitListener(queues = "my-queue") public void handleMessage(String message) { processingCount.incrementAndGet(); try { processMessage(message); } finally { processingCount.decrementAndGet(); } }
@PreDestroy public void onShutdown() throws InterruptedException { log.info("메시지 처리 대기 중: {}", processingCount.get());
int waitTime = 0; while (processingCount.get() > 0 && waitTime < 30000) { Thread.sleep(100); waitTime += 100; }
log.info("모든 메시지 처리 완료"); }
private void processMessage(String message) { } }
|
모니터링 및 로깅
Shutdown 이벤트 처리
@Component public class ShutdownEventListener {
@EventListener public void handleContextClose(ContextClosedEvent event) { log.info("애플리케이션 컨텍스트 종료 시작"); }
@EventListener public void handleContextStopped(ContextStoppedEvent event) { log.info("애플리케이션 컨텍스트 정지됨"); } }
|
액추에이터를 통한 모니터링
management: endpoint: shutdown: enabled: true endpoints: web: exposure: include: health,info,shutdown
|
@RestController public class HealthController {
private volatile boolean shuttingDown = false;
@PreDestroy public void onShutdown() { shuttingDown = true; }
@GetMapping("/health/ready") public ResponseEntity<String> readiness() { if (shuttingDown) { return ResponseEntity.status(503).body("Shutting down"); } return ResponseEntity.ok("Ready"); } }
|
주의사항
1. 타임아웃 설정
K8s terminationGracePeriodSeconds (60s) > Spring timeout-per-shutdown-phase (45s) > 실제 작업 완료 예상 시간 (30s)
|
여유 시간을 두어 강제 종료(SIGKILL)를 방지합니다.
2. 데드락 방지
@PreDestroy public void cleanup() { if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) { log.warn("일부 작업이 타임아웃됨"); executorService.shutdownNow(); } }
|
3. 멱등성 보장
Shutdown Hook은 여러 번 실행될 수 있으므로 멱등성을 보장해야 합니다.
private volatile boolean shutdownCompleted = false;
@PreDestroy public synchronized void onShutdown() { if (shutdownCompleted) { return; }
try { } finally { shutdownCompleted = true; } }
|
테스트
로컬 테스트
./gradlew bootRun
kill -15 <PID>
|
통합 테스트
@SpringBootTest class GracefulShutdownTest {
@Autowired private ConfigurableApplicationContext context;
@Test void gracefulShutdown() throws Exception { Future<String> future = executeLongTask();
context.close();
assertThat(future.isDone()).isTrue(); assertThat(future.get()).isEqualTo("완료"); } }
|
정리
Graceful Shutdown은 프로덕션 환경에서 필수적인 기능입니다:
- Spring Boot 설정:
server.shutdown=graceful
- 타임아웃 설정: 적절한 대기 시간 설정
- 리소스 정리: @PreDestroy, SmartLifecycle 활용
- K8s 고려: terminationGracePeriodSeconds와 조화
- 모니터링: 로깅 및 헬스체크 구현
적절한 Graceful Shutdown 구현으로 무중단 배포와 안정적인 서비스 운영이 가능합니다.