스프링 컨테이너와 서블릿 컨테이너

스프링 애플리케이션의 핵심인 스프링 컨테이너를 서블릿 컨테이너(WAS) 위에 수동으로 구축하는 과정은 스프링 MVC의 동작 원리를 이해하는 데 중요하다. 이 과정에는 스프링 컨테이너 생성, 컨트롤러 빈 등록, 그리고 서블릿 컨테이너에 DispatcherServlet을 등록하여 스프링 MVC와 연동하는 작업이 포함된다. 스프링 MVC는 이러한 복잡한 초기화 과정을 WebApplicationInitializer 인터페이스를 통해 단순화하여 개발자가 애플리케이션 로직에 집중할 수 있도록 돕는다

초기화 순서 흐름도

  1. 서블릿 컨테이너(WAS) 시작 (예: Tomcat 구동)
  2. ServletContainerInitializer 구현체 탐색 및 실행
    • META-INF/services 파일 읽기
    • SpringServletContainerInitializer 발견 및 실행
  3. @HandlesTypes를 통한 WebApplicationInitializer 탐색
    • 클래스패스 스캔
    • AppInitV3SpringMvc 등 구현 클래스 발견
  4. WebApplicationInitializer.onStartup() 호출
    • 스프링 컨테이너 생성
    • 설정 클래스 등록 (HelloConfig)
  5. DispatcherServlet 생성 및 서블릿 컨테이너 등록
    • DispatcherServlet에 스프링 컨테이너 연결
    • URL 매핑 설정 (/, /spring/* 등)
  6. DispatcherServlet 초기화
    • 첫 요청 시 또는 setLoadOnStartup(1) 설정 시
    • 스프링 컨테이너 refresh() 실행
    • 빈 생성 및 의존성 주입 (HelloController 등)
  7. 애플리케이션 준비 완료
    • HTTP 요청 처리 가능

스프링 컨테이너 등록의 기본 과정

  • 스프링 컨테이너 생성: AnnotationConfigWebApplicationContext와 같은 웹 환경에 특화된 스프링 컨테이너를 생성한다
  • 스프링 빈 등록: @RestController로 정의된 컨트롤러와 같은 스프링 컴포넌트들을 생성된 스프링 컨테이너에 빈으로 등록한다
  • DispatcherServlet 등록: 스프링 MVC의 핵심인 DispatcherServlet을 서블릿 컨테이너에 등록하고 생성한 스프링 컨테이너와 연결한다. DispatcherServlet은 클라이언트의 요청을 받아 적절한 스프링 컨트롤러로 라우팅하는 역할을 한다

수동으로 스프링 컨테이너와 DispatcherServlet 설정

ServletContainerInitializer와 @HandlesTypes를 활용하여 AppInitV2Spring 이라는 사용자 정의 초기화 구현체를 통해 위 과정을 수동으로 진행할 수 있다

HelloController(스프링 컨트롤러 예시)

@RestController
public class HelloController {
    @GetMapping("/hello-spring")
    public String hello() {
        System.out.println("HelloController.hello");
        return "hello-spring";
    }
}
  • @RestController: RESTful 웹 서비스의 컨트롤러임을 나타낸다
  • @GetMapping(“/hello-spring”): /hello-spring 경로로 GET 요청을 이 메서드가 처리하도록 매핑한다

HelloConfig(스프링 컨테이너 설정 예시)

@Configuration // 스프링 설정 클래스로 지정
public class HelloConfig {
    @Bean // 스프링 컨테이너가 관리할 빈으로 등록
    public HelloController helloController() {
        return new HelloController();
    }
}
  • 실무에서는 @ComponentScan을 활용하여 @RestController 애노테이션이 붙은 클래스들을 자동으로 빈으로 등록하는 방식이 더 일반적이다. 학습 목적으로 명시적인 빈 등록 방식을 사용하며, 빈 등록을 수동으로 하는 방식도 쓰인다

AppInitV2Spring (애플리케이션 초기화 구현체)

import hello.spring.HelloConfig;
import jakarta.servlet.ServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

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

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


        // 2. HelloConfig 설정 클래스를 사용하여 스프링 컨테이너에 빈 등록 (HelloController 포함)
        appContext.register(HelloConfig.class);
        // 참고: DispatcherServlet이 init() 메서드에서 자동으로 appContext.refresh() 호출하여
        // 스프링 컨테이너를 초기화하고 빈들을 생성한다

        // 3. DispatcherServlet 생성 및 스프링 컨테이너 연결
        DispatcherServlet dispatcher = new DispatcherServlet(appContext);

        // 4. DispatcherServlet을 서블릿 컨테이너에 등록하고 URL 매핑 지정
        //    "/spring/*" 경로로 들어오는 모든 요청을 이 DispatcherServlet이 처리
        servletContext.addServlet("dispatcherV2", dispatcher)
                .addMapping("/spring/*");
                // .setLoadOnStartup(1); // 선택사항: 서버 시작 시 즉시 초기화 (기본값은 첫 요청 시)
    }
}
  • AnnotationConfigWebApplicationContext: 애노테이션 기반 설정을 지원하는 웹 환경용 스프링 컨테이너
  • appContext.register(HelloConfig.class): HelloConfig 클래스에 정의된 빈 설정을 스프링 컨테이너에 등록한다
  • 스프링 컨테이너는 refrash() 호출을 통해 실제로 초기화된다
  • DispatcherServlet 생성자에 ApplicationContext를 전달하면, 서블릿 컨테이너가 DispatcherServlet의 init() 메서드를 호출할 때 자동으로 refrash()가 실행된다.
  • new DispatcherServlet(appContext): DispatcherServlet을 생성하면서 위에서 만든 스프링 컨테이너를 연결한다. 이제 DispatcherServlet은 이 컨테이너의 빈들을 알고 요청을 처리할 수 있다
  • servletContext.addServlet(“dispatcherV2”, dispatcher).addMapping(“/spring/*”): 서블릿 컨테이너에 dispatcherV2라는 이름으로 DispatcherServlet을 등록하고, /spring/* 패턴의 URL 요청을 이 서블릿이 처리하도록 매핑한다
  • loadOnStartup 미설정 시: 첫 번째 요청이 왔을 때 DispatcherServlet이 초기화되며, 이때 스프링 컨테이너도 함께 초기화된다
  • loadOnStartup(1) 설정 시: 서버 시작 시점에 즉시 초기화되어 첫 요청의 응답 속도를 개선할 수 있다
실핼과정
  • /spring/hello-spring 요청 시, 서블릿 컨테이너는 /spring/* 패턴에 매핑된 dispatcherV2(DispatcherServlet)를 실행한다. DispatcherServlet은 서블릿 매핑 패턴(/spring/*)을 제외한 나머지 경로 (/hello-spring)를 사용하여 내부 스프링 컨테이너에서 @GetMapping(“/hello-spring”)으로 매핑된 HelloController의 hello() 메서드를 찾아 호출한다

스프링 MVC의 초기화 지원: WebApplicationInitializer

스프링 MVC는 위와 같이 복잡하고 반복적인 서블릿 컨테이너 초기화 과정을 단순화하기 위해 WebApplicationInitializer 인터페이스를 제공한다. 개발자는 이 인터페이스만 구현하면 스프링이 미리 만들어준 SpringServletContainerInitializer 덕분에 복잡한 ServletContainerInitializer 등록 절차 없이 애플리케이션 초기화 코드를 작성할 수 있다

AppInitV3SpringMvc.java (스프링 MVC 지원 초기화 구현체)

import hello.spring.HelloConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class AppInitV3SpringMvc implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        System.out.println("AppInitV3SpringMvc.onStartup");

        // 1. 스프링 컨테이너 생성
        AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
        // 2. HelloConfig 설정 클래스를 사용하여 스프링 컨테이너에 빈 등록
        appContext.register(HelloConfig.class);

        // 3. DispatcherServlet 생성 및 스프링 컨테이너 연결
        DispatcherServlet dispatcher = new DispatcherServlet(appContext);

        // 4. DispatcherServlet을 서블릿 컨테이너에 등록하고 모든 요청 ("/")을 처리하도록 매핑
        servletContext.addServlet("dispatcherV3", dispatcher)
                .addMapping("/"); // 모든 요청을 처리하도록 설정
    }
}
  • WebApplicationInitializer: 스프링이 제공하는 애플리케이션 초기화 인터페이스이다. 이 인터페이스를 구현하는 것만으로 스프링의 초기화 로직에 자동으로 통합된다.
  • servletContext.addServlet(“dispatcherV3”, dispatcher).addMapping(“/”): 모든 URL 요청(/)을 이 dispatcherV3(DispatcherServlet)가 처리하도록 설정한다. 이는 일반적으로 단일 DispatcherServlet으로 모든 요청을 처리하는 현대 스프링 애플리케이션의 방식이다
WebApplicationInitializer의 실행 과정
  • 서블릿 컨테이너가 시작되면 ServletContainerInitializer 구현체들을 찾아 실행한다
  • 스프링 SpringServletContainerInitializer가 실행된다
  • SpringServletContainerInitializer는 @HandlesTypes(WebApplicationInitializer.class)를 통해 클래스패스 WebApplicationInitializer를 구현한 모든 클래스를 스캔한다
  • 찾은 클래스들의 인스턴스를 생성하고 각각의 onStartup() 메서드를 호출한다
  • 이로써 AppInitV3SpringMvc의 onStartup()이 자동으로 실행되어 스프링 컨테이너와 DispatcherServlet이 초기화된다
스프링 MVC의 내부 동작
  • 스프링 MVC의 spring-web 라이브러리 내부에는 org.springframework.web.SpringServletContainerInitializer가 META-INF/services/jakarta.servlet.ServletContainerInitializer 파일에 등록되어 있다. 이 SpringServletContainerInitializer는 @HandlesTypes(WebApplicationInitializer.class) 애노테이션을 통해 WebApplicationInitializer를 구현한 클래스들을 찾아 실행한다. 결과적으로 개발자는 WebApplicationInitializer만 구현하면 스프링 MVC가 자동으로 복잡한 서블릿 컨테이너 초기화 과정을 처리해주는 것이다

스프링 부트는 이러한 초기화 과정을 더 추상화하고 자동화하여 개발자가 서블릿 컨테이너 설정이나 서블릿 컨테이너 초기화에 대한 고민 없이 main 메서드 실행만으로 웹 애플리케이션을 구동할 수 있도록 했다. 하지만 그 밑단에는 ServletContainerInitializer와 WebApplicationInitializer를 통한 체계적인 서블릿 컨테이너와 스프링 컨테이너의 연동 로직이 숨어 있으며, 이를 이해하는 것은 스프링 애플리케이션의 깊은 동작 원리를 파악하는 데 도움이 된다. 일반적으로 하나의 스프링 컨테이너와 하나의 DispatcherServlet (/ 매핑)을 사용하여 모든 요청을 처리하는 방식이 선호된다

참고: 스프링 애플리케이션 컨텍스트 계층 구조

전통적인 스프링 MVC 애플리케이션에서 종종 두 단계의 스프링 컨테이너 계층을 사용했다

  • Root ApplicationContext (부모): 서비스 계층, DAO, 데이터 소스 등 비즈니스 로직 관련 빈들을 관리. 여러 DispatcherServlet이 공유할 수 있다
  • Servlet ApplicationContext (자식): 컨트롤러, 인터셉터, 뷰 리졸버 등 웹 계층 빈들을 관리, 각 DispatcherServlet마다 독립적으로 존재한다

이러한 계층 구조를 사용하는 이유는 비즈니스 로직과 웹 계층을 분리하고 여러 개의 DispatcherServlet(예: REST API용, 웹 페이지용)에서 동일한 서비스 계층을 공유하기 위함이다. 하지만 스프링 부트는 이러한 복잡성을 제거하고 단일 ApplicationContext를 사용하는 단순화된 구조를 채택했다. 대부분의 현대 애플리케이션에서는 하나의 DispatcherServlet으로 모든 요청을 처리하므로 계층 구조의 필요성이 줄어들었기 때문이다

출처 – 김영한 님의 강의 중 스프링 부트 – 핵심 원리와 활용