Servlet/Spring에서 web.xml은 어떻게 사라졌을까?
목차
- Servlet에서의 web.xml 대체
- Spring-Legacy에서의 web.xml 대체
- Spring-Boot에서의 web.xml 대체
- 결론
Servlet에서의 web.xml 대체
Java6(Servlet 3.0 API)에서 XML 설정을 Java Config와 Annoation으로 대체하고자 하는 시도가 보인다.
Servlet 3.0에서는 @WebServlet, @WebFilter, @WebListener를 통해 설정의 간소화를 도모하고 있다. 그러나 해당 어노테이션은 한계가 분명하게 보인다.
예를 들어, filter의 ordering은 어떻게 할 것인가?
session timeout은 어떻게 처리할 것인가?
security-role은 어떻게 처리할 것인가?
welcome-file-list는 어떻게 할 것인가?
annoation을 통해 해소하는데 한계가 있다.
위와 같은 문제점을 해결하기 위해 ServletContainerInitializer 인터페이스의 ServletContext를 통해 web.xml 설정들을 대체할 수 있다. 쉽게 얘기해서 Spring의 xml 설정을 Java Config로 대체하는 것과 같은 맥락이다.
ServletContainerInitilizer#onStartup은 서블릿 컨테이너에 의해 호출되어야 한다. 그러면 서블릿 컨테이너는 해당 메소드를 호출할 수 있을까? 그러니까 Tomcat은 ServletContainerInitilizer를 어떻게 호출할 수 있을까? ServletContainerInitilizer는 Service Provider Interface(SPI) 개념을 근간한다.
SPI(Service Provider Interface)는 플러그인/모듈 등의 기능을 확장할 때 사용하는 인터페이스이다. 예는 다음과 같다.
Framework "F"는 "A"라는 API를 제공한다. Framework "F"는 여러 모듈 또는 플러그인이 존재한다. "A" API의 결과는 플러그인 또는 모듈에 따라 결과 값이 다르다. Framework "F"는 해당 플러그인 또는 모듈을 상황에 따라 선택하여 배포할 수 있다.
SPI를 쓰기위해서는 META-INF/services에 정확한 패키지명과 클래스명을 다음과 같이 기재하며 호출할 수 있다.
SP 지정: META-INF/services/com.service.MyServiceImpl
SP 호출: ServiceLoader<MyService> services = ServiceLoader.load(MyService.class);
ServletContainerInitilizer인터페이스 및 사용방법은 아래와 같다.
package javax.servlet;
...
public interface ServletContainerInitializer {
public void onStartup(Set<Class<?>> c, ServletContext ctx)
throws ServletException;
}
@HandlesTypes({MyType.class})
public class AppInitializer implements ServletContainerInitializer{
..
}
Set<Class<?>>객체를 통해 javax.servlet.annotation.HandlesTypes에 지정한 클래스 객체를 갖고 올 수 있다. 만약에 해당 어노테이션에 인터페이스를 지정하면 해당 인터페이스를 구현한 모든 클래스를 조회하여 Set에 보관한다. 해당 클래스 객체들은 newInstance()를 통해 활용할 수 있다.
위와 같은 ServletContainerInitlizer를 통해 프레임워크에 IoC 패턴을 삽입할 수 있다.
Spring-Legacy에서의 web.xml 대체
Spring에서의 web.xml은 WebApplicationInitializer를 통해 해결할 수 있다. 사용자는 WebApplicationInitializer을 구현하여 web.xml을 사용할 수 있다. 또는 AbstractAnnotationConfigDispatcherServletInitializer 추상 클래스를 사용한다. 단순하게 WebApplicationInitializer를 확장하여 사용할 경우, 모든 설정을 일일 설정해야 한다. 그러나, 스프링에서 제공하는 기본 구현 셋(AbstractAnnotationConfigDispatcherServletInitializer) 사용을 통해 간소화할 수 있다. 해당 추상 클래스를 을 통해 Servlet 계층의 스프링 설정, Servivce 계층의 스프링 설정, DispatcherServlet의 경로 지정, 필터 등록을 간결하게 할 수 있다.
( WebApplicationInitializer, AbstractAnnotationConfigDispatcherServletInitializer의 사용 방법은 별도의 구글링을 통해 쉽게 확인할 수 있다.)
여기서 중요한 포인트는 SpringServletContainerInitilizer 클래스이다. 해당 클래스는 javax.servlet.ServletContainerInitializer 인터페이스를 구현한 클래스이다. 코드는 아래와 같다.
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer) waiClass.newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
- HandlesTypes어노테이션에 WebApplicationInitializer 클래스를 지정하였다.
- Set에 있는 WebApplicationInitilizer을 구현한 모든 클래스에 대해 newInstance()를 하여 객체를 생성한다.
- 생성한 객체에 onStartup 메소드를 실행한다.
SpringServletContainerInitilizer 클래스에서 위와 같은 작업을 해주기 때문에 우리는 Spring에서 AbstractAnnotationConfigDispatcherServletInitializer 또는 WebApplicationInitializer를 구현해서 Servlet Container설정을 간결하게 할 수 있다.
Spring-Boot 에서의 web.xml 대체
Spring-Boot에서는 설정들이 아래와 같이 퍼졌다.
- DispatcherServletAutoConfiguration을 통해 DispatcherServlet을 자동으로 설정한다.
- application.yaml을 통한 DispatcherServlet 경로 설정(default는 "/"이다.)
- FilterRegistrationBean, ServletListenerRegistrationBean을 통해 Servlet Filter와 Listener를 설정한다.
- welcome-file-list의 경우, Spring Container 설정으로 해소할 수 있다(ViewControllerRegister).
- error의 경우, Spring-Boot만의 error 핸들링 스트럭쳐가 존재한다.
Servlet Listener의 경우, Spring ApplicationListener<ContextRefreshedEvent>으로 변경해도 무방할 경우, 해당 클래스로 마이그레이션 하는 것을 권고한다. 어플리케이션 초기화 설정은 Spring Bean을 사용하는 경우 다반사이기 때문이다. 즉 Spring Container 내에서의 초기화 설정을 진행하는 것이 편리하다.
정리하면 Spring-Boot에서의 Servlet Container 설정은 이곳 저곳 퍼진 감이 있다. 그래도 Spring-Legacy와 비교하면 더 간편해졌다고 봐도 무방하다.
결론
본문을 통해 web.xml이 어떻게 사라졌는지 확인할 수 있다. 설정 방법을 보러온 독자의 경우 많은 실망을 했을 수 있다. 다른 많은 블로그에서 다루니까 잘 찾아보길 바란다.
- Servlet에서는 web.xml은 SPI 기술을 통해 xml을 없앨 수 있었고,
- Spring-Legacy에서는 HandlesTypes 어노테이션에 WebApplicationInitilizer을 지정하여 효율적으로 사용할 수 있었고,
- Spring-Boot에서는 Auto-Configuration, Spring Container 대체, 빈 설정 등으로 간편하게 사용할 수 있었다.
여기서도 느낄 수 있지만 Spring(Boot)의 이해에는 먼저 Servlet의 이해가 정말 필요하다. Servlet 책 1권 정도는 보면 좋을 것 같다(엄진영의 Java 웹 개발 워크북을 추천한다. 시대별 Servlet 프로그래밍을 맛볼 수 있다.).
참고문헌
www.logicbig.com/tutorials/core-java-tutorial/java-se-api/service-loader.html
docs.oracle.com/javaee/6/api/javax/servlet/annotation/HandlesTypes.html
www.logicbig.com/tutorials/java-ee-tutorial/java-servlet/servlet-container-initializer-example.html
www.baeldung.com/spring-xml-vs-java-config
www.baeldung.com/spring-boot-dispatcherservlet-web-xml
www.baeldung.com/java-web-app-without-web-xml