Spring/Spring Boot

Spring Boot의 SSE(Server-Sent Events) Graceful Shutdown 동작 원리

TheWing 2025. 1. 5. 18:21

들어가기 전에 포스팅 계기

앞전에 Graceful Shutdown의 동작원리에 이어서, SSE Graceful shutdown 이 어떻게 동작하는지 동작 원리를 명확히 알고자 포스팅하게 됐습니다.


1. 일반적인 Graceful Shutdown vs SSE Graceful Shutdown

일반적인 HTTP 요청과 SSE 연결의 가장 큰 차이점은 "연결 지속 시간"입니다.

일반 HTTP 요청

  • 요청-응답 후 즉시 연결 종료
  • 대부분 짧은 시간 내 처리 완료
  • Graceful Shutdown 시 진행 중인 요청만 완료하면 됨

SSE 연결

  • 클라이언트와 서버 간 장기 연결 유지
  • 서버에서 클라이언트로 지속적인 이벤트 스트리밍
  • 명시적인 연결 종료 처리 필요

2. SSE Graceful Shutdown 동작 과정

2.1 Shutdown 시그널 수신 (Kill -15)

// SpringApplicationShutdownHook.run()이 실행됨
@Override
public void run() {
    Set<ConfigurableApplicationContext> contexts;
    synchronized (SpringApplicationShutdownHook.class) {
        contexts = new LinkedHashSet<>(this.contexts);
    }
    contexts.forEach(this::closeAndWait);
}

2.2 새로운 요청 거부

  • 서버는 새로운 SSE 연결 요청을 거부
  • 기존 SSE 연결은 grace period 동안 유지 (Default = 30초)

2.3 DefaultLifecycleProcessor에서의 처리

2.4 SSE 연결 종료 처리 (@PreDestroy 설정 시)

@Component
public class SseEmitterManager {
    private final Set<SseEmitter> emitters = ConcurrentHashMap.newKeySet();

    @PreDestroy
    public void shutdown() {
        for (SseEmitter emitter : emitters) {
            try {
								// 종료 이벤트 전송
                emitter.send(SseEmitter.event()
                    .name("shutdown")
                    .data("Server is shutting down"));
								// 연결 종료
                emitter.complete();
            } catch (IOException e) {
                emitter.completeWithError(e);
            } finally {
                emitters.remove(emitter);
            }
        }
    }
}

3. Graceful Shutdown 실패 시나리오

SSE 연결이 정상적으로 종료되지 않을 경우 다음과 같은 로그가 순차적으로 발생합니다:

1. 웹 서버 종료 실패 알림
Shutdown phase 2147482623 ends with 1 bean still running after timeout of 30000ms: [webServerGracefulShutdown]

2. 비동기 요청 타임아웃
DefaultHandlerExceptionResolver : AsyncRequestTimeoutException

3. Graceful Shutdown 중단
Graceful shutdown aborted with one or more requests still active

실패 시나리오 상세 분석

3.1. 실패 과정의 상세 흐름

3.1.1. Phase 종료 타임아웃 (2147482623)

Shutdown phase 2147482623 ends with 1 bean still running after timeout of 30000ms: [webServerGracefulShutdown]
  • 원인: DefaultLifecycleProcessor에서 webServerGracefulShutdown 빈이 30초 내에 종료되지 않음
  • phase에서 2147482623 값의 의미
    • 이 phase는 Tomcat의 GracefulShutdown이 실행되는 phase
    • webServerGracefulShutdown의 phase 값(2147482623)은 Integer.MAX_VALUE(2147483647)보다 약간 작은 값으로 설정되어 있습니다. 이는 가장 마지막에 종료되어야 하는 컴포넌트들의 phase 값입니다.
    • 다른 빈들보다 늦게 종료되어야 하므로 높은 값 사용
    • 실제 Tomcat이 완전히 종료되기 전에 타임아웃이 발생하면 이 로그가 출력됨
  • 상세 동작:

3.1.2. 비동기 요청 타임아웃

DefaultHandlerExceptionResolver : AsyncRequestTimeoutException
  • 원인: SSE 연결이 비동기 요청으로 처리되는 중 타임아웃 발생
  • 상세 동작:
  • @GetMapping("/sse") public SseEmitter subscribe() { SseEmitter emitter = new SseEmitter(timeout);// timeout 이후 AsyncRequestTimeoutException 발생 emitter.onTimeout(() -> emitter.complete()); return emitter; }

3.1.3. Graceful Shutdown 강제 중단

Graceful shutdown aborted with one or more requests still active
  • 원인: 활성 상태의 SSE 연결이 남아있는 상태에서 강제 종료
  • 상세 동작:

4. 안전한 SSE Graceful Shutdown을 위한 Best Practices

4.1 적절한 타임아웃 설정

server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 60s# 기본값 30초

4.2 클라이언트 재연결 로직 구현

function connectSSE() {
    const eventSource = new EventSource('/api/stream');

    eventSource.addEventListener('shutdown', (event) => {
        console.log('Server is shutting down:', event.data);
        eventSource.close();
    });

    eventSource.onerror = (error) => {
        eventSource.close();
        setTimeout(connectSSE, 5000);
    };
}

4.3 SSE 연결 관리 컴포넌트

@Configuration
public class WebServerConfig {
    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown() {
            @Override
            protected void onShutdown() {
                logger.info("SSE connections cleanup started");
// SSE 연결 정리 로직
            }
        };
    }
}

5. 결론

SSE Graceful Shutdown은 일반적인 HTTP 요청의 Graceful Shutdown보다 더 복잡한 처리가 필요합니다. 장기 연결의 특성상 클라이언트에게 적절한 종료 신호를 보내고, 연결을 정상적으로 종료하는 과정이 중요합니다. 이를 위해 적절한 타임아웃 설정, 연결 관리, 클라이언트 측 재연결 로직 구현 등이 필요합니다.