티스토리 뷰

개요

  • 이번 글에서는 Spring Boot, 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는 종료 시그널 발생시 현재 들어온 요청을 모두 처리하지 않은 채 도중에 애플리케이션 컨텍스트를 모두 제거하기 때문에 익셉션이 발생한다. 개발자가 별도의 로직 처리를 해주어야 한다.

Undertow 빈 작성

package com.jsonobject.example.config;

import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;

public class UndertowShutdownHandlerWrapper implements HandlerWrapper {

    private GracefulShutdownHandler gracefulShutdownHandler;

    public HttpHandler wrap(final HttpHandler handler) {

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

        return gracefulShutdownHandler;
    }

    public GracefulShutdownHandler getGracefulShutdownHandler() {

        return gracefulShutdownHandler;
    }
}
package com.jsonobject.example.config;

import io.undertow.servlet.api.DeploymentInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.undertow.UndertowDeploymentInfoCustomizer;
import org.springframework.boot.context.embedded.undertow.UndertowEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
public class UndertowConfig {

    @Bean
    public EmbeddedServletContainerCustomizer undertowCustomizer() {

        return new EmbeddedServletContainerCustomizer() {

            public void customize(ConfigurableEmbeddedServletContainer container) {
                if (container instanceof UndertowEmbeddedServletContainerFactory) {
                    ((UndertowEmbeddedServletContainerFactory) container).addDeploymentInfoCustomizers(undertowDeploymentInfoCustomizer());
                }
            }
        };
    }

    @Bean
    public UndertowDeploymentInfoCustomizer undertowDeploymentInfoCustomizer() {

        return new UndertowDeploymentInfoCustomizer() {

            public void customize(DeploymentInfo deploymentInfo) {
                deploymentInfo.addOuterHandlerChainWrapper(undertowShutdownHandlerWrapper());
            }
        };
    }

    @Bean
    public UndertowShutdownHandlerWrapper undertowShutdownHandlerWrapper() {

        return new UndertowShutdownHandlerWrapper();
    }
}

ContextClosedEvent 리스너 작성

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

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class ApplicationClosedEventListener {

    @Autowired
    private UndertowShutdownHandlerWrapper undertowShutdownHandlerWrapper;

    @EventListener
    public void processContextClosedEvent(ContextClosedEvent event) {

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

참고 글

댓글
댓글쓰기 폼