
웹 애플리케이션 개발에서 서블릿 컨테이너 초기화는 필수적인 과정이다.
서블릿 컨테이너는 애플리케이션 실행 시점에 초기화 작업을 수행하여 서블릿, 필터, 리스너 등을 등록한다.
이 글에서는 서블릿 컨테이너 초기화의 전반적인 구조와 이를 기반으로 스프링 레거시와 스프링 부트에서 각각 어떻게 처리하는지 살펴보겠다.
1. 서블릿 컨테이너 초기화의 기본 방법
1.1 ServletContainerInitializer
1.1.1 정의
서블릿 3.0 스펙에서 제공하는 ServletContainerInitializer
는 서블릿 컨테이너 초기화를 담당하는 표준 인터페이스이다.
웹 애플리케이션이 시작될 때 서블릿 컨테이너(Tomcat, Jetty 등)는 이 인터페이스를 구현한 클래스를 자동으로 탐색하고 실행한다.
public interface ServletContainerInitializer {
void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
- 파라미터
c
:@HandlesTypes
로 지정된 클래스들의 집합이다.ctx
: 컨테이너 환경에 접근할 수 있는 객체(서블릿, 필터, 리스너 등록 가능)
1.2 @HandlesTypes
1.2.1 정의
@HandlesTypes
는ServletContainerInitializer
가 어떤 타입을 다룰 것인지를 지정하기 위한 어노테이션이다.- 예를 들어
@HandlesTypes(AppInit.class)
라고 선언하면 서블릿 컨테이너는AppInit
을 상속(또는 구현)한 클래스들을 자동으로 찾아onStartup
메서드의Set<Class<?>>
에 담는다.
1.2.2 왜 사용하는가?
- 특정 인터페이스(또는 추상 클래스)를 구현한 클래스만 자동으로 검색 및 초기화하고 싶을 때 사용한다.
- 뒤에 스프링에서는
HandlesTypes
를 통해DispatcherServlet
등의 컨테이너 환경을 초기화 하는데 초기화에 필요한 추상체를 지원해줘서 비교적 빠르게 환경을 초기화할 수 있다.
1.3 사용 예제
@HandlesTypes(AppInit.class)
public class MyInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
if (c != null) {
for (Class<?> clazz : c) {
try {
AppInit instance = (AppInit) clazz.getDeclaredConstructor().newInstance();
instance.registerServlet(ctx);
} catch (Exception e) {
throw new ServletException("Failed to instantiate: " + clazz.getName(), e);
}
}
}
}
}
public interface AppInit {
void onStartup(ServletContext servletContext);
}
public class AppInitServlet implements AppInit {
@Override
public void onStartup(ServletContext servletContext) {
System.out.println("AppInitV1Servlet.onStartup");
//순수 서블릿 코드 등록
ServletRegistration.Dynamic helloServlet =
servletContext.addServlet("helloServlet", new HelloServlet());
helloServlet.addMapping("/hello-servlet");
}
}
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("HelloServlet.service");
resp.getWriter().println("hello servlet!");
}
}
@HandlesTypes(AppInit.class)
public class MyContainerInit implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
System.out.println("MyContainerInit.onStartup");
System.out.println("MyContainerInit c = " + c);
System.out.println("MyContainerInit ctx = " + ctx);
for (Class<?> appInitClass : c) {
try {
AppInit appInit = (AppInit) appInitClass.getDeclaredConstructor().newInstance();
appInit.onStartup(ctx);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
// 경로
resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
// 내용
hello.container.MyContainerInit
동작원리
- WAS는
resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
에 있는 클래스들을 읽는다. - 파일 안에서
ServletContainerInitializer
를 구현하는 지 확인하고 서블릿 컨텍스트를 초기화한다. - 클래스가
@HandlesTypes(SomeType.class)
을 가지고 있다면SomeType.class
와 그 자식 클래스를 조회하여onStartup
메서드의Set
에 넣어준다.
실제로 Tomcat의 ContextConfig
클래스를 보면 processServletContainerInitializers
메서드가 있다.
이 메서드에서 서블릿컨테이너 초기화를 진행하고, @HandlesTypes
을 읽고 처리하는 일을 진행한다.
또한 이 메서드에서 호출하는 WebappServiceLoader
를 열어보면 private static final String SERVICES = "META-INF/services/"
을 필드로 가지고 있고, 해당 부분을 조회한다는 것을 알 수 있다.
2. 서블릿 등록 방법
서블릿을 등록하는 방법은 크게 두 가지로 나뉜다.
2.1 @WebServlet 애노테이션 사용
서블릿 클래스에 @WebServlet
어노테이션을 추가하여 간단하게 등록할 수 있다.
예제
@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("HelloServlet.service");
resp.getWriter().println("hello servlet!");
}
}
2.2 프로그래밍 방식 사용
프로그래밍 방식은 ServletContext
를 활용하여 서블릿을 등록하는 방법이다.
위에서 @HandlesTypes
와 서블릿 컨테이너 초기화하는 과정이 이 방식이다.
3. 스프링 레거시에서의 초기화 방식
스프링 레거시는 주로 WAR 형태로 외부 WAS(Tomcat, Jetty 등)에 배포한다.
이 과정에서 스프링은 내부적으로 ServletContainerInitializer
를 활용한다.
3.1 SpringServletContainerInitializer
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) {
// WebApplicationInitializer 구현체들을 찾아서 초기화 작업 수행
}
}
스프링은 SpringServletContainerInitializer
라는 ServletContainerInitializer
의 구현체를 가지고 있고, 여기에 @HandlesTypes(WebApplicationInitializer.class)
를 지정해두었다.
그래서 WebApplicationInitializer
의 구현체만 구현하면 돼서 서블릿에 의존이 적어졌다.
3.2 WebApplicationInitializer
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
WebApplicationInitializer
는 스프링 애플리케이션 초기화를 담당하는 인터페이스이다.- 이 인터페이스를 구현하여
DispatcherServlet
등 필요한 서블릿을 컨텍스트에 등록한다. - 스프링이 이 인터페이스를 상속받아 여러 클래스를 만들어두었고, 대표적으로는
AbstractAnnotationConfigDispatcherServletInitializer
이 있다.
이것을 활용하면 비교적 쉽게 스프링 컨테이너 초기화를 할 수 있다.
3.3 동작 과정
- WAS 초기화: 컨테이너가
SpringServletContainerInitializer
를 호출 - 클래스 스캐닝:
@HandlesTypes(WebApplicationInitializer.class)
를 통해 구현체 검색 onStartup
실행: 각 구현체에서DispatcherServlet
등록 등 초기화 작업 수행
4. 스프링 부트에서의 초기화 방식
스프링 부트는 내장 톰캣을 사용하며 전통적인 WAR 배포와 다른 초기화 메커니즘을 사용한다.
4.1 TomcatStarter
스프링 부트는 ServletContainerInitializer
를 구현한 TomcatStarter
라는 클래스가 있다.
그러나 여기서는 @HandlesTypes
기반의 클래스 스캐닝을 사용하지 않는다.
왜 @HandlesTypes를 쓰지 않을까?
- 스프링 부트는
SpringApplication
안에서 자동 구성(Auto Configuration)과 컴포넌트 스캔을 활용한다. - 서블릿 레벨에서 특정 인터페이스 구현체를 찾는 것보다 스프링 컨텍스트에서 Bean으로 등록하는 방식이 선택했기 때문이다.
- 따라서 굳이
@HandlesTypes
를 사용할 필요 없이 스프링 빈 등록 과정을 통해 필요한 서블릿(DispatcherServlet 등)을 자동으로 설정한다.
5. 정리
- 서블릿 컨테이너 초기화: ServletContainerInitializer
- 서블릿 3.0에서 제공하는 표준 인터페이스로, WAS 구동 시 자동으로 호출된다.
@HandlesTypes
를 통해 특정 인터페이스 구현체들을 자동으로 스캔하고 초기화할 수 있다.
- 스프링 레거시
SpringServletContainerInitializer
을 통해 서블릿 컨테이너 초기화는 내부적으로 처리한다.SpringServletContainerInitializer
는@HandlesTypes(WebApplicationInitializer.class)
을 가지고 있어서 서블릿에 의존성을 최소화하면서WebApplicationInitializer
의 구현체를 통해 서블릿 컨테이너를 초기화할 수 있다.- 스프링은 자체적으로
WebApplicationInitializer
의 구현체 및 추상체들을 만들어둬서 서블릿 컨테이너 설정을 비교적 편하게 할 수 있다.
- 스프링 부트
- 내장 톰캣과
TomcatStarter
를 사용하지만, 실제 서블릿 등록/검색은 스프링 빈 등록 과정을 통해 처리 @HandlesTypes
를 사용하지 않고, 자동 구성(Auto Configuration)과 Bean 등록을 활용하여 더 단순한 초기화 로직 제공
- 내장 톰캣과
스프링 레거시는 서블릿 표준과 좀 더 맞닿아 있는 구조이고, 스프링 부트는 이를 더 단순화하여 해당 과정을 스프링 컨텍스트 내부에서 처리한다.
이에 따라, 스프링 부트 환경에서는 사용자가 굳이 서블릿 레벨에서 코드를 제어할 일이 거의 없어졌다. 조금 더 서비스 로직에 집중할 수 있게 만들어줬다고 보면 된다.
'Backend > Spring' 카테고리의 다른 글
Bean 등록할 때 @Configuration을 사용해야 하는 이유 (0) | 2024.12.29 |
---|---|
BeanFactory와 ApplicationContext (0) | 2024.12.28 |
4부: @ConfigurationProperties으로 타입 안전하게 외부 설정 관리 (0) | 2024.12.23 |
3부: 스프링 레거시와 스프링부트의 외부 설정 (1) | 2024.12.21 |
2부: 스프링 외부 설정 통합 관리 : Environment와 PropertySource의 동작 구조 (1) | 2024.12.19 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!