스프링 애플리케이션의 핵심인 스프링 컨테이너를 서블릿 컨테이너(WAS) 위에 수동으로 구축하는 과정은 스프링 MVC의 동작 원리를 이해하는 데 중요하다. 이 과정에는 스프링 컨테이너 생성, 컨트롤러 빈 등록, 그리고 서블릿 컨테이너에 DispatcherServlet을 등록하여 스프링 MVC와 연동하는 작업이 포함된다. 스프링 MVC는 이러한 복잡한 초기화 과정을 WebApplicationInitializer 인터페이스를 통해 단순화하여 개발자가 애플리케이션 로직에 집중할 수 있도록 돕는다
초기화 순서 흐름도
- 서블릿 컨테이너(WAS) 시작 (예: Tomcat 구동)
- ServletContainerInitializer 구현체 탐색 및 실행
- META-INF/services 파일 읽기
- SpringServletContainerInitializer 발견 및 실행
- @HandlesTypes를 통한 WebApplicationInitializer 탐색
- 클래스패스 스캔
- AppInitV3SpringMvc 등 구현 클래스 발견
- WebApplicationInitializer.onStartup() 호출
- 스프링 컨테이너 생성
- 설정 클래스 등록 (HelloConfig)
- DispatcherServlet 생성 및 서블릿 컨테이너 등록
- DispatcherServlet에 스프링 컨테이너 연결
- URL 매핑 설정 (/, /spring/* 등)
- DispatcherServlet 초기화
- 첫 요청 시 또는 setLoadOnStartup(1) 설정 시
- 스프링 컨테이너 refresh() 실행
- 빈 생성 및 의존성 주입 (HelloController 등)
- 애플리케이션 준비 완료
- 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으로 모든 요청을 처리하므로 계층 구조의 필요성이 줄어들었기 때문이다