• spring
  • servlet
  • spring-mvc

서블릿 컨테이너 초기화와 스프링 컨테이너 등록 과정

WAR 배포 구조부터 ServletContainerInitializer, 스프링 컨테이너와 DispatcherServlet 등록 과정을 코드로 따라간다.

시리즈 · Spring13 / 14
  1. SOLID 원칙과 OCP·DIP를 지키는 방법
  2. 의존성 주입(DI)과 스프링 컨테이너
  3. 쓰레드 풀과 싱글톤 서블릿의 동시성 문제
  4. FrontController 패턴과 핸들러 어댑터로 스프링 MVC 구조 이해하기
  5. @ModelAttribute와 PRG 패턴으로 폼 요청 처리하기
  6. System.out 대신 SLF4J로 로그 남기기
  7. 세션 직접 만들어 보기와 HttpSession
  8. 스프링 인터셉터와 예외 발생 시 오류 페이지 흐름
  9. 서블릿 Part와 MultipartFile로 파일 업로드 처리하기
  10. JDBC로 CRUD 구현하기와 커넥션 풀, DataSource
  11. JDBC 트랜잭션에서 스프링 트랜잭션 동기화와 트랜잭션 AOP까지
  12. JPA 변경 감지(dirty checking)와 병합(merge)
  13. 서블릿 컨테이너 초기화와 스프링 컨테이너 등록 과정
  14. 싱글톤 빈의 동시성 문제와 ThreadLocal

스프링 부트가 내장 톰캣으로 감춰 준 과정, 즉 WAS가 뜰 때 서블릿과 스프링 컨테이너가 어떻게 등록되는지를 직접 구현하며 따라가 본다.

JAR와 WAR

JAR (Java Archive)

  • 여러 클래스와 리소스를 묶은 압축 파일이다.
  • JVM 위에서 직접 실행되거나 다른 곳에서 사용하는 라이브러리로 제공된다.
  • 직접 실행하려면 main() 메서드가 필요하고, MANIFEST.MF 파일에 메인 메서드가 있는 클래스를 지정해야 한다.

WAR (Web Application Archive)

  • WAS에 배포할 때 사용하는 파일이다.
  • JAR가 JVM 위에서 실행된다면 WAR는 WAS 위에서 실행된다.

WAR 구조

WAR 파일의 디렉터리 구조

WEB-INF/
  classes/     실행 클래스 모음
  lib/         라이브러리 모음
  web.xml      웹 서버 배치 설정 파일 (생략 가능)
index.html     정적 리소스

WEB-INF 하위는 클래스, 라이브러리, 설정 정보가 들어가는 곳이고, 그 밖의 영역은 HTML, CSS 같은 정적 리소스가 위치한다.

톰캣에 직접 배포

  1. ./gradlew build로 WAR 파일을 빌드한다.
  2. 톰캣폴더/webapps 내부의 파일을 모두 삭제한다.
  3. 빌드한 WAR 파일을 ROOT.war라는 이름으로 webapps에 넣는다.

이 과정은 IDE에서 톰캣을 연동해 대신 처리할 수도 있다.

서블릿 컨테이너 초기화

WAS를 실행하는 시점에 서비스에 필요한 필터와 서블릿을 등록해야 한다. 스프링을 사용한다면 스프링 컨테이너를 만들고, 서블릿과 스프링을 연결하는 DispatcherServlet도 등록해야 한다.

서블릿은 이를 위해 ServletContainerInitializer라는 초기화 인터페이스를 제공한다. 서블릿 컨테이너는 실행 시점에 onStartup()을 호출하고, 여기서 애플리케이션에 필요한 기능을 초기화하거나 등록할 수 있다.

public interface ServletContainerInitializer {
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
  • Set<Class<?>> c: 더 유연한 초기화를 위한 파라미터로, @HandlesTypes 애노테이션과 함께 사용한다.
  • ServletContext ctx: 서블릿 컨테이너 자체의 기능을 제공한다. 이 객체로 필터나 서블릿을 등록할 수 있다.

구현 클래스를 만든 뒤에는 WAS에 알려 줘야 한다. 아래 경로의 파일에 초기화 클래스를 패키지 경로까지 포함해 적어 두면, WAS가 실행 시 해당 클래스를 초기화 클래스로 인식해 로딩 시점에 실행한다.

# resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
hello.container.MyContainerInitV1

서블릿을 등록하는 두 가지 방법

@WebServlet

@WebServlet(urlPatterns = "/test")
public class TestServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("TestServlet.service");
        resp.getWriter().println("test");
    }
}

프로그래밍 방식

먼저 등록할 서블릿을 만든다.

public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("HelloServlet.service");
        resp.getWriter().println("hello servlet!");
    }
}

애플리케이션 초기화를 담당할 인터페이스를 정의한다. 내용과 형식은 자유롭게 정하면 된다.

public interface AppInit {
    void onStartup(ServletContext servletContext);
}

구현체에서 ServletContext를 통해 서블릿을 직접 등록한다.

public class AppInitV1Servlet implements AppInit {

    @Override
    public void onStartup(ServletContext servletContext) {
        System.out.println("AppInitV1Servlet.onStartup");

        // 순수 서블릿 코드 등록: /hello-servlet을 호출하면 helloServlet이 실행된다.
        servletContext.addServlet("helloServlet", new HelloServlet())
                .addMapping("/hello-servlet");
    }
}

두 방식의 차이

@WebServlet은 애노테이션 하나로 편리하게 등록할 수 있지만 유연하게 변경하기 어렵다. 프로그래밍 방식은 코드가 더 필요한 대신 다음이 가능하다.

  • 특정 조건에 따라 if 문으로 분기해 서블릿을 등록하거나 뺄 수 있다.
  • 서블릿을 직접 생성하므로 생성자에 필요한 정보를 넘길 수 있다.

애플리케이션 초기화

AppInit 구현체는 누가 실행해 줄까? 서블릿 컨테이너 초기화 클래스에 @HandlesTypes로 인터페이스를 지정하면 된다.

@HandlesTypes(AppInit.class)
public class MyContainerInitV2 implements ServletContainerInitializer {

    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
        System.out.println("MyContainerInitV2.onStartup");
        System.out.println("c = " + c);
        System.out.println("ctx = " + ctx);

        // c: class hello.container.AppInitV1Servlet
        for (Class<?> appInitClass : c) {
            try {
                // 리플렉션으로 객체 생성
                AppInit appInit = (AppInit) appInitClass.getDeclaredConstructor().newInstance();
                appInit.onStartup(ctx);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
  • @HandlesTypes에 애플리케이션 초기화 인터페이스를 지정한다.
  • 서블릿 컨테이너는 그 인터페이스의 구현체를 모두 찾아 클래스 정보로 Set<Class<?>> c에 넘겨 준다.
  • 클래스 정보만 넘어오므로 리플렉션으로 객체를 생성한 뒤 초기화 코드를 직접 실행한다. 이때 서블릿 컨테이너 정보가 담긴 ctx도 함께 전달한다.

실행 로그를 보면 서블릿 컨테이너 초기화가 먼저 실행되고, 이어서 애플리케이션 초기화가 실행된다.

MyContainerInitV2.onStartup
c = [class hello.container.AppInitV1Servlet]
ctx = org.apache.catalina.core.ApplicationContextFacade@38dd0980
AppInitV1Servlet.onStartup

서블릿 컨테이너 초기화가 @HandlesTypes로 찾은 애플리케이션 초기화 구현체를 실행하는 흐름

서블릿 컨테이너 초기화만으로도 서블릿을 등록할 수 있는데 애플리케이션 초기화를 따로 두는 이유는 의존성 제거다. 애플리케이션 초기화 코드는 ServletContainerInitializer에 의존하지 않고, 원하는 모양의 인터페이스로 자유롭게 만들 수 있다.

스프링 컨테이너 등록

같은 방식으로 스프링 컨테이너를 만들고 DispatcherServlet을 서블릿 컨테이너에 등록할 수 있다.

서블릿 컨테이너에 등록된 DispatcherServlet이 스프링 컨테이너와 연결된 구조

public class AppInitV2Spring implements AppInit {

    @Override
    public void onStartup(ServletContext servletContext) {
        System.out.println("AppInitV2Spring.onStartup");

        // 스프링 컨테이너 생성
        AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
        appContext.register(HelloConfig.class);

        // 스프링 MVC DispatcherServlet 생성, 스프링 컨테이너와 연결
        DispatcherServlet dispatcher = new DispatcherServlet(appContext);

        // DispatcherServlet을 서블릿 컨테이너에 등록
        servletContext
                .addServlet("dispatcherV2", dispatcher)
                .addMapping("/spring/*");
    }
}

/spring/* 경로로 HTTP 요청이 오면 DispatcherServlet이 실행되고, 연결된 스프링 컨테이너에 들어 있는 컨트롤러 빈을 호출한다.

WebApplicationInitializer

지금까지 직접 만든 서블릿 컨테이너 초기화 과정을 스프링 MVC가 이미 제공한다. AppInit에 해당하는 애플리케이션 초기화 인터페이스가 WebApplicationInitializer다. AppInit 대신 WebApplicationInitializer를 구현하기만 하면 되고, 서블릿 컨테이너 초기화 클래스를 직접 만들거나 등록할 필요가 없다.

동작 방식은 위에서 구현한 것과 같다. spring-web 라이브러리를 열어 보면 META-INF/services/jakarta.servlet.ServletContainerInitializer 파일에 서블릿 컨테이너 초기화 클래스가 등록되어 있다.

org.springframework.web.SpringServletContainerInitializer

그리고 이 클래스는 @HandlesTypes로 WebApplicationInitializer를 지정해 애플리케이션 초기화를 실행한다.

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    // ...
}

전체 구조

서블릿 컨테이너 초기화부터 WebApplicationInitializer 구현체 실행, 스프링 컨테이너와 DispatcherServlet 등록까지의 전체 구조

참고

  • 김영한, 스프링 부트 - 핵심 원리와 활용 (인프런)
  • 전체 코드