Spring Web MVC 를 사용할 때 Front Controller 로 사용하는 Servlet WebApplicationContext 를 생성하는 DispatcherServlet 클래스가 사용하는 interface 에 대해 전체적으로 훑어보며 간략하게 정리한다.
DispatcherServlet 클래스는 이 servlet 이 초기화 되는 과정에서 자신이 사용 할 전략들을 등록 하는 작업을 한다.
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
MultipartResolver 를 제외하고 기본적으로 각자 interface 타입의 bean 을 찾아보고, 없을 경우 DispatcherServlet.properties 파일에 정의 되어 있는 기본 전략을 사용 한다.
# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
initMultipartResolver
: 요청을 분석하는 단계에서 사용
: 파일 업로드 요청 처리에 사용
: MultipartResolver interface 타입의 구현체가 bean 으로 등록 되어 있어야 사용 가능
/**
* Initialize the MultipartResolver used by this class.
* <p>If no bean is defined with the given name in the BeanFactory for this namespace,
* no multipart handling is provided.
*/
private void initMultipartResolver(ApplicationContext context) {
try {
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("Detected " + this.multipartResolver);
}
else if (logger.isDebugEnabled()) {
logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
}
}
catch (NoSuchBeanDefinitionException ex) {
// Default is no multipart resolver.
this.multipartResolver = null;
if (logger.isTraceEnabled()) {
logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared");
}
}
}
13라인에서 보면 다른 interface 와 달리 MultipartResolver 타입의 구현체를 찾지 못하면 기본 전략을 사용하지 않고 null 값을 할당 한다.
initLocaleResolver
: 요청을 분석하는 단계에서 사용
: 클라이언트의 locale 정보 확인할 때 사용 (지역정보)
: LocaleResolver interface 타입의 구현체가 bean 으로 등록 되어 있는지 확인
/**
* Initialize the LocaleResolver used by this class.
* <p>If no bean is defined with the given name in the BeanFactory for this namespace,
* we default to AcceptHeaderLocaleResolver.
*/
private void initLocaleResolver(ApplicationContext context) {
try {
this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("Detected " + this.localeResolver);
}
else if (logger.isDebugEnabled()) {
logger.debug("Detected " + this.localeResolver.getClass().getSimpleName());
}
}
catch (NoSuchBeanDefinitionException ex) {
// We need to use the default.
this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No LocaleResolver '" + LOCALE_RESOLVER_BEAN_NAME +
"': using default [" + this.localeResolver.getClass().getSimpleName() + "]");
}
}
}
LocaleResolver 타입의 구현체를 찾지 못하면 기본 전략을 사용 한다.
initThemeResolver
: 애플리케이션의 테마를 변경하고 싶을 때 사용
: theme 와 관련하여 CSS 와 같은 자원 중 어느 것을 적용할지 선택
: ThemeResolver interface 타입의 구현체가 bean 으로 등록 되어 있는지 확인
/**
* Initialize the ThemeResolver used by this class.
* <p>If no bean is defined with the given name in the BeanFactory for this namespace,
* we default to a FixedThemeResolver.
*/
private void initThemeResolver(ApplicationContext context) {
try {
this.themeResolver = context.getBean(THEME_RESOLVER_BEAN_NAME, ThemeResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("Detected " + this.themeResolver);
}
else if (logger.isDebugEnabled()) {
logger.debug("Detected " + this.themeResolver.getClass().getSimpleName());
}
}
catch (NoSuchBeanDefinitionException ex) {
// We need to use the default.
this.themeResolver = getDefaultStrategy(context, ThemeResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No ThemeResolver '" + THEME_RESOLVER_BEAN_NAME +
"': using default [" + this.themeResolver.getClass().getSimpleName() + "]");
}
}
}
initHandlerMappings
: 요청이 들어왔을 때 그 요청을 처리할 handler method 를 찾을 때 사용
: HandlerMapping interface 타입의 구현체가 bean 으로 등록 되어 있는지 확인
/**
* Initialize the HandlerMappings used by this class.
* <p>If no HandlerMapping beans are defined in the BeanFactory for this namespace,
* we default to BeanNameUrlHandlerMapping.
*/
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
일반적으로 사용하는 annotation 기반의 handler method 를 찾아주는 HandlerMapping 구현체로는 기본 적략으로 등록되어 있는 RequestMappingHandlerMapping 클래스 이다.
initHandlerAdapters
: HandlerMapping 구현체로 찾은 handler method 를 실제로 처리할 때 사용
: HandlerAdapter interface 타입의 구현체가 bean 으로 등록 되어 있는지 확인
/**
* Initialize the HandlerAdapters used by this class.
* <p>If no HandlerAdapter beans are defined in the BeanFactory for this namespace,
* we default to SimpleControllerHandlerAdapter.
*/
private void initHandlerAdapters(ApplicationContext context) {
this.handlerAdapters = null;
if (this.detectAllHandlerAdapters) {
// Find all HandlerAdapters in the ApplicationContext, including ancestor contexts.
Map<String, HandlerAdapter> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerAdapters = new ArrayList<>(matchingBeans.values());
// We keep HandlerAdapters in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerAdapters);
}
}
else {
try {
HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);
this.handlerAdapters = Collections.singletonList(ha);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerAdapter later.
}
}
// Ensure we have at least some HandlerAdapters, by registering
// default HandlerAdapters if no other adapters are found.
if (this.handlerAdapters == null) {
this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerAdapters declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
기본적으로 HandlerMapping 과 HandlerAdpater 는 어느정도 매칭이 되어 있어야 한다.
예를 들어 RequestMappingHandlerMapping 이 HandlerMapping 의 기본 전략으로 등록되어 있고, 이를 처리 할 HandlerAdapter 구현체로 RequestMappingHandlerAdapter 가 등록되어 있다.
initHandlerExceptionResolvers
: 요청을 처리하는 과정에서 예외가 발생할 경우 해당 예외를 처리하기 위해 사용
: HandlerExceptionResolver interface 타입의 구현체가 bean 으로 등록 되어 있는지 확인
private void initHandlerExceptionResolvers(ApplicationContext context) {
this.handlerExceptionResolvers = null;
if (this.detectAllHandlerExceptionResolvers) {
// Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
// We keep HandlerExceptionResolvers in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
}
}
else {
try {
HandlerExceptionResolver her =
context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
this.handlerExceptionResolvers = Collections.singletonList(her);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, no HandlerExceptionResolver is fine too.
}
}
// Ensure we have at least some HandlerExceptionResolvers, by registering
// default HandlerExceptionResolvers if no other resolvers are found.
if (this.handlerExceptionResolvers == null) {
this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
등록 되어있는 기본 전략 구현체 중 ExceptionHandlerExceptionResolver 클래스를 주로 사용한다.
initRequestToViewNameTranslator
: view 를 반환하는 handler method 가 명시적으로 view 의 이름을 반환하지 않은 경우 요청 정보로 view 이름을 판단할 때 사용
: RequestToViewNameTranslator interface 타입의 구현체가 bean 으로 등록 되어 있는지 확인
/**
* Initialize the RequestToViewNameTranslator used by this servlet instance.
* <p>If no implementation is configured then we default to DefaultRequestToViewNameTranslator.
*/
private void initRequestToViewNameTranslator(ApplicationContext context) {
try {
this.viewNameTranslator =
context.getBean(REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, RequestToViewNameTranslator.class);
if (logger.isTraceEnabled()) {
logger.trace("Detected " + this.viewNameTranslator.getClass().getSimpleName());
}
else if (logger.isDebugEnabled()) {
logger.debug("Detected " + this.viewNameTranslator);
}
}
catch (NoSuchBeanDefinitionException ex) {
// We need to use the default.
this.viewNameTranslator = getDefaultStrategy(context, RequestToViewNameTranslator.class);
if (logger.isTraceEnabled()) {
logger.trace("No RequestToViewNameTranslator '" + REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME +
"': using default [" + this.viewNameTranslator.getClass().getSimpleName() + "]");
}
}
}
/app/sample 요청을 처리하는 handler method 는 String 타입의 "sample" 문자열을 반환 했었다.
그래서 "/WEB-INF/sample.jsp" 파일을 view 로 사용 했는데, 다음과 같이 명시적으로 view 이름을 반환하지 않아도 요청 정보를 바탕으로 view 를 찾아 사용할 수 있다.
@GetMapping("/sample")
public void sample() { }
실제로 RequestToViewNameTranslator 의 기본 전략 객체인 DefaultRequestToViewNameTranslator 를 사용해서 요청 정보를 기반으로 view 이름을 찾는 것을 확인할 수 있다.
initViewResolvers
: 찾아낸 view 이름을 가지고 실제 view 를 찾아내는 interface
: ViewResolver interface 타입의 구현체가 bean 으로 등록 되어 있는지 확인
/**
* Initialize the ViewResolvers used by this class.
* <p>If no ViewResolver beans are defined in the BeanFactory for this
* namespace, we default to InternalResourceViewResolver.
*/
private void initViewResolvers(ApplicationContext context) {
this.viewResolvers = null;
if (this.detectAllViewResolvers) {
// Find all ViewResolvers in the ApplicationContext, including ancestor contexts.
Map<String, ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.viewResolvers = new ArrayList<>(matchingBeans.values());
// We keep ViewResolvers in sorted order.
AnnotationAwareOrderComparator.sort(this.viewResolvers);
}
}
else {
try {
ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);
this.viewResolvers = Collections.singletonList(vr);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default ViewResolver later.
}
}
// Ensure we have at least one ViewResolver, by registering
// a default ViewResolver if no other resolvers are found.
if (this.viewResolvers == null) {
this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No ViewResolvers declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
여러개의 구현체가 등록될 수 있는데 기본적으로 InternalResourceViewResolver 클래스 하나만 기본 적략으로 등록되어 있다.
initFlashMapManager
: FlashMap 객체를 가져오고 저장하는데 사용
: 주로 redirect 할 때 사용
: form submit 을 하고 난 다음 브라우저에서 refresh 할 경우 같은 데이터를 중복 처리를 방지하기 위한 일종의 패턴
: FlashMapManager interface 타입의 구현체가 bean 으로 등록 되어 있는지 확인
/**
* Initialize the {@link FlashMapManager} used by this servlet instance.
* <p>If no implementation is configured then we default to
* {@code org.springframework.web.servlet.support.DefaultFlashMapManager}.
*/
private void initFlashMapManager(ApplicationContext context) {
try {
this.flashMapManager = context.getBean(FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class);
if (logger.isTraceEnabled()) {
logger.trace("Detected " + this.flashMapManager.getClass().getSimpleName());
}
else if (logger.isDebugEnabled()) {
logger.debug("Detected " + this.flashMapManager);
}
}
catch (NoSuchBeanDefinitionException ex) {
// We need to use the default.
this.flashMapManager = getDefaultStrategy(context, FlashMapManager.class);
if (logger.isTraceEnabled()) {
logger.trace("No FlashMapManager '" + FLASH_MAP_MANAGER_BEAN_NAME +
"': using default [" + this.flashMapManager.getClass().getSimpleName() + "]");
}
}
}
기본 전략으로 SessionFlashMapManager 클래스를 사용 한다.
위에 정리한 DispatcherServlet 에 등록 되어있는 기본 전략 이외에도 각각의 interface 를 구현하고 있는 구현체들이 많이 있다.
기본 적략은 말 그대로 자주 사용하는 일반적인 경우에 대해 기본적으로 제공해주는 것이고, 그 외에 필요한 경우 다른 구현 체를 사용 하거나 또는 직접 구현체를 구현해서 사용하는 방법도 있다.
본 내용과 조금 다른 내용이지만 아주 관련이 없는 내용은 아니라 추가로 정리해 둔다.
나중에 Spring boot 프로젝트를 생성해보면 web.xml 파일이 없어진 것을 알 수 있다.
Servlet 3.0 스펙부터 web.xml 에 작성했던 정보를 java 코드로 작성할 수 있게 되었다.
모로 가도 서울만 가면 되는 마냥 모로 가도 서블릿만 등록하면 된다.
Front Controller 역할을 할 DispatcherServlet 클래스는 servlet 으로 등록 했지만 사실 생각해보면 @Controller 클래스에 @GetMapping, @PostMapping 등을 사용해서 web.xml 에 등록하던 servlet 을 등록했다.
어떤 히스토리가 있는지는 모르겠지만, servlet 을 포함해서 web.xml 에 등록했던 정보들을 굳이 xml 형식으로 작성하지 않고 java 코드로 작성하고 싶었던것 같다. Spring 이 처음 생겼을 때 xml 설정 파일로 bean 을 등록 했었지만 갈 수록 java 코드로 설정 파일을 작성하는 추세가 된 것 처럼 web.xml 도 Servlet 3.0 스펙으로 올라가면서 java 코드로 대체가 된 게 아닐까 싶은게 내 생각이다.
아무튼, 이제 더 이상 web.xml 이 아니어도 java 코드로 Servlet 을 등록해서 사용할 수 있다.
없어도 된다고 하니까 일단 지우고 보자.
web.xml 을 대체 할 임의의 클래스를 하나 생성 한다.
package me.nimkoes;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
public class WebApplication implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(MyWebConfig.class);
context.refresh();
// DispatcherServlet 을 만들 때 설정 정보를 가지고 있는 context 를 넘겨 준다.
DispatcherServlet dispatcherServlet = new DispatcherServlet(context);
// ServletContext 에 DispatcherServlet 을 등록 한다.
ServletRegistration.Dynamic app = servletContext.addServlet("app", dispatcherServlet);
app.addMapping("/app/*");
}
}
WebApplicationInitializer 인터페이스를 구현 하도록 하고, onStartup 메소드를 오버라이딩 한다.
그리고 그 안에서 web.xml 에서 했던 DispatcherServlet 을 만들고 ServletContext 에 등록하도록 한다.
이 상태로 서버를 실행 시키고 브라우저를 통해 요청을 보내면 정상적으로 동작하는 것을 확인할 수 있다.
이렇게 web.xml 없이 java 코드로 Servlet 을 등록해서 사용할 수 있는 것은 Spring 3.1 이후 버전, Servlet 3.0 이후 버전부터 가능하다.
눈여겨 봐야 할 부분은 WebApplicationInitializer 인터페이스를 구현한다는 것이다.
WebApplicationInitializer 인터페이스에 대한 api 문서를 보면 다음과 같은 내용이 있다.
Interface to be implemented in Servlet 3.0+ environments in order to configure the ServletContext programmatically -- as opposed to (or possibly in conjunction with) the traditional web.xml-based approach. Implementations of this SPI will be detected automatically by SpringServletContainerInitializer, which itself is bootstrapped automatically by any Servlet 3.0 container. See its Javadoc for details on this bootstrapping mechanism. |
WebApplicationInitializer 인터페이스를 구현하면 SpringServletContainerInitializer 가 자동으로 감지하고, Servlet 3.0 컨테이너에 의해 자동으로 부트스트랩 된다.
여기서 부트스트랩 된다는 말은 아무것도 없는 상태에서 무엇인가 시작하는 것을 뜻한다고 생각하면 된다.
벨 연구소에서 일했던 컴퓨터 과학자인 Brian W. Kernighan 교수가 쓴 'Hello, Digital World' 책 내용 중에 부트스트랩에 대해 다음과 같이 설명하고 있다.
CPU는 컴퓨터에 파워가 켜졌을 때 영구 기억 장치에 저장된 약간의 명령어를 실행해서 작동을 시작하도록 구성되어 있다. 다음에는 그 명령어들이 디스크상의 알려진 위치, USB 메모리, 또는 네트워크 연결로부터 더 많은 명령어를 읽기에 충분한 코드를 포함하고 있는 작은 플래시 메모리에서 명령어를 읽고, 그 명령어는 최종적으로 유용한 작업을 하기에 충분한 코드가 로딩될 때까지 더욱더 많은 명령어를 읽는다. 이렇게 시작하는 과정은 원래 "자력으로 해내다(pulling oneself up by one's bootstraps)"라는 오래된 표현에서 나온 "부트스트래핑(bootstrapping)"이라고 불렸는데, 지금은 그냥 부팅(booting)이라고 한다. |
또한 SpringServletContainerInitializer 클래스에 대한 설명은 다음과 같다.
public class SpringServletContainerInitializer extends Object implements ServletContainerInitializer Servlet 3.0 ServletContainerInitializer designed to support code-based configuration of the servlet container using Spring's WebApplicationInitializer SPI as opposed to (or possibly in combination with) the traditional web.xml-based approach. |
SPI 기술에 대해서 여유가 되면 조금 더 자세히 찾아보고 정리해둬야 겠다.
어쨌든 현재 중요한 것은 WebApplicationInitializer 인터페이스를 구현 하면 Servlet 3.0+ 컨테이너에 의해 자동으로 onStartup 메소드를 실행하기 때문에 이 곳에서 java 코드로 web.xml 에서 했던 설정들을 할 수 있다는 사실이다.
'Archive > Spring Web MVC' 카테고리의 다른 글
2.2 Spring Boot 의 Spring MVC 설정 (war 배포) (0) | 2021.07.05 |
---|---|
2.1 Spring Web MVC 설정 @EnableWebMvc, WebMvcConfigurer (0) | 2021.07.04 |
1.4 DispatcherServlet 기본 동작 원리 (0) | 2021.06.27 |
1.3 Spring MVC 연동 (Root & Servlet ApplicationContext) (0) | 2021.06.24 |
1.2 Spring IoC Container 연동 (0) | 2021.06.24 |