티스토리 뷰

개요

  • 이번 글에서는 Spring Boot 2.x, JBoss Undertow 조합의 웹 애플리케이션에서 Graceful Shutdown을 구현하는 방법을 설명하고 한다.

Spring Boot와 Graceful Shutdown

  • Spring Boot는 전형적인 멀티 쓰레드 애플리케이션이다. 클라이언트로부터의 단일 요청을 처리하기 위해 다양한 쓰레드가 유기적으로 작동하여 응답을 반환한다. 한편, 엔터프라이즈 레벨의 애플리케이션은 HA 보장이 필수이며 서비스 중인 1개 노드가 Shutdown될 경우 동일한 역할을 하는 다른 노드가 제 기능을 할 수 있어야 한다.
  • HA 환경에서 애플리케이션 Shutdown시의 Graceful Shutdown 보장은 모든 언어와 플랫폼을 불문하고 가장 중요한 요소이다. Graceful Shutdown이란 무엇일까? 외부에서 종료 시그널이 발생했을 때 애플리케이션은 즉시 새로운 요청을 거부해야 한다. 그래야 로드 밸런서가 인지하여 해당 노드에 대한 요청 전달을 멈출 수 있다. 동시에 이미 처리 중인 요청에는 정상적으로 응답을 완료해야 한다. 모든 요청에 대한 처리가 끝나면 비로소 애플리케이션 종료 행위가 일어나야 한다.
  • 그렇다면 Spring Boot는 기본적으로 Graceful Shutdown을 지원할까? 정답부터 말하자면 지원하지 않는다. Spring Boot는 종료 시그널 발생시 현재 들어온 요청을 모두 처리하지 않은 채 도중에 애플리케이션 컨텍스트를 모두 제거하기 때문에 익셉션이 발생한다. 개발자가 별도의 로직 처리를 해주어야 한다.

build.gradle 작성

  • 프로젝트의 루트의 build.gradle 파일에 아래 내용을 추가한다.
dependencies {
    implementation('org.springframework.boot:spring-boot-starter-web') {
        exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
    }
    implementation 'org.springframework.boot:spring-boot-starter-undertow'
}
  • Spring Boot 2는 기본 서블릿 컨테이너로 Apache Tomcat를 내장하고 있다. 위와 같이 spring-boot-starter-tomcat 아티팩트를 제외하고 spring-boot-starter-undertow 아티팩트를 추가하면 JBoss Undertow를 기본 서블릿 컨테이너로 변경할 수 있다.

GracefulShutdownHandlerWrapper 작성

package com.jsonobject.example.example;

import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import org.springframework.stereotype.Component;

@Component
public class GracefulShutdownHandlerWrapper implements HandlerWrapper {

    private GracefulShutdownHandler gracefulShutdownHandler;

    @Override
    public HttpHandler wrap(HttpHandler handler) {

        if (gracefulShutdownHandler == null) {
            this.gracefulShutdownHandler = new GracefulShutdownHandler(handler);
        }

        return gracefulShutdownHandler;
    }

    public GracefulShutdownHandler getGracefulShutdownHandler() {

        return gracefulShutdownHandler;
    }
}

GracefulShutdownEventListener 작성

  • ContextClosedEvent은 스프링의 애플리케이션 컨텍스트가 종료될 때 발생하는 이벤트이다. 애플리케이션을 구동 중인 JVM에 종료 시그널(kill 명령)이 전달되었을 때가 바로 ContextClosedEvent에 해당한다. ContextClosedEvent이 특히 중요한 것은 Spirng Boot에서의 Graceful Shutdown 처리에 있어 중요한 이벤트 발생 지점이기 때문이다.
package com.jsonobject.example.example;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class GracefulShutdownEventListener implements ApplicationListener<ContextClosedEvent> {

    @Autowired
    private GracefulShutdownHandlerWrapper gracefulShutdownHandlerWrapper;

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {

        log.info("GRACEFUL_SHUTDOWN_STARTED");

        // 이 시점부터 새로운 요청이 거부된다. 클라이언트는 503 Service Unavailable 응답을 수신한다.
        gracefulShutdownHandlerWrapper.getGracefulShutdownHandler().shutdown();

        try {
            // 이 시점에 기존 처리 중인 요청에 대한 응답을 완료한다.
            gracefulShutdownHandlerWrapper.getGracefulShutdownHandler().awaitShutdown();
        } catch (Exception ex) {
            ex.printStackTrace();
            log.error("GRACEFUL_SHUTDOWN_FAILED");
        }

        log.info("GRACEFUL_SHUTDOWN_FINISHED");
    }
}
  • 일반적인 스프링 빈을 작성하고 ContextClosedEvent 오브젝트를 아규먼트로 받는 임의의 메써드에 @EventListener 어노테이션을 부여하면 이벤트 리스너 작성 준비가 끝난다. 애플리케이션이 종료되기 직전 필요한 행위를 작성할 수 있다.
  • 실제 ContextClosedEvent 이벤트가 발생하면 해당 메써드가 별도의 쓰레드로 실행된다. 위 예제에서는 Undertow가 제공하는 GracefulShutdownHandler를 이용하여 애플리케이션 컨텍스트 제거 전 요청에 대한 차단 작업을 수행하도록 작성하였다.

GracefulShutdownConfig 작성

@Configuration
public class GracefulShutdownConfig {

    @Autowired
    private GracefulShutdownHandlerWrapper gracefulShutdownHandlerWrapper;

    @Bean
    public UndertowServletWebServerFactory servletWebServerFactory() {

        UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
        factory.addDeploymentInfoCustomizers(deploymentInfo -> deploymentInfo.addOuterHandlerChainWrapper(gracefulShutdownHandlerWrapper));

        return factory;
    }
}

참고 글

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/03   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함