이전 Web Server, Web Application Server 포스트에서 Spring MVC 대한 모든 요청과 응답은 DispatcherServlet에서 관리한다고 말했었다.
DispatcherServlet
은 스프링 MVC 구조를 구축할 수 있도록 도와주는 프론트 컨트롤러
이다.
즉, DispatcherServlet
은 모든 요청에 대한 실행 흐름을 책임을 가지고 있고, 사용자 요청 처리/뷰 렌더링/로직 등은 Controller, Model, View에 위임한다.
아래 그림을 통해서 MVC의 각각의 역할을 요약해 볼 수 있다.
Controller
는 사용자의 요청을 처리하고,
그에 대한 결과 데이터를Model
에 담아View
에 전달하면,
View Template
은 전달받은Model
과 선택한View
를 렌더링해서 사용자엑 반환(응답)해준다.
이러한 일련의 동작 흐름을 책임지는 것이 DispatcherServlet
의 역할이다.
구체적으로,DispatcherServlet
은 다음과 같은 동작을 한다.
Controller
에게 사용자 요청을 전달 Controller
가 DispatcherServlet
으로 부터 전달받은 Model
인자에 데이터 정보를 담으면, DispatcherServlet
가 View
에 전달해준다.Controller
가 View
에 대한 정보(경로/이름)을 반환하면, DispatcherServlet
이 해당 정보를 ViewResolver
에 전달한다.Controller
는 ModelAndView
타입으로 View
와 Model
정보를 한 번에 반환할 수도 있다.Controller
에서 반환한 View
정보를 ViewResolver
에게 전달해서 View
를 찾아낸다.View Template
에게 View
와 Model
정보를 전달해서 렌더링한 뒤 사용자에게 결과를 응답한다.그럼, 이제 DispatcherServlet
이 어떻게 Controller
, Model
, View
를 이용해서 일련의 요청을 처리하는지 자세히 살펴보자.
우선 사용자로부터 요청이 오면, DispatcherServlet
은 HandlerMapping
을 이용해서 경로에 맞는 Controller
를 찾는다.
탐색해서 찾은 Controller
를 Handler(HandlerExecutionChain)
라고 부르는데, HandlerAdapter
를 이용해서 Handler
를 실행한다.
그리고 Controller
가 반환한 View 정보(이름)를 View Resolver
전달해 반환할 View 오브젝트
를 탐색한다.
마지막으로, View Resolver
를 통해서 찾은 View 구현체
와 Contoller
가 담은 Model
정보를 View Tempate
에 전달해서 View 렌더링
을 수행한다.
렌더링이 완료된 결과물은 사용자 응답값에 담겨 반환된다.
위 그림과 같이 스프링은 Handler(HandlerExecutionChain)
탐색하기 이전에 아래와 같은 요청에 대한 선처리 작업을 수행한다.
그럼 각 단계에서 어떤 작업을 수행하는 지 알아보자.
우선 요청이 어떤 지역에서 왔는지, 즉 HTTP 헤더 정보를 기반으로 지역 정보를 결정해 국제화 처리를 한다.
즉, DispatcherServlet은 자동으로 클라이언트의 Locale
을 사용해서 메시지를 처리하는데, 이는 LocaleResolver
객체로 이뤄진다.
다음으로,RequestContextHolder
는 일반 Bean
에서 HttpServletRequest
, HttpServletResponse
, HttpSession
등을 쉽게 사용할 수 있도록 각 정보들을 ThreadLocal
타입으로 저장하는데, 해당 정보는 동일한 Thread내에서는 어느 곳에서든 같은 HttpServletRequest
, HttpServletResponse
, HttpSession
을 사용할 수 있도록 지원한다. (각 계층에서 해당 객체들을 주입받을 수 있다.)
요청마다 생성되는 Thread 내에서 안전(ThreadSafe)하게 HttpServletRequest
, HttpServletResponse
, HttpSession
를 저장한다.
HttpServletRequest req = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
FlashMapManager
은 리다이렉트된 정보(FlashMap
)를 조회한다.
즉 리다이렉션하는 경우, 현재 요청 정보를 FlashMap
에 저장하고, 리다리렉션 이후에는 이전 요청에 대한 저장된 속성 정보를 FlashMapManager
로 조회/복원한다.
FlashMap는 플래시 속성을 보관하는데 사용하고 FlashMapManager는 FlashMap 인스턴스를 저장하고 획득하고 관리하는데 사용한다.
마지막으로, MultipartResolver
를 이용해서 파일 업로드같은 멀티파트 요청을 처리한다.
HandlerAdapter
가 결정된 HandlerExecutionChain
을 실행하는데, 여기서 Interceptor
가 동작하게 된다.
즉, HandlerExecutionChain
실행 이전에 인터셉터의 preHanle()
메서드를 호출하고, 실행 이후에는 인터셉터의 postHanle()
메서드를 호출한다.
HandlerAdapter
는 핸들러를 실행하기에 앞서 어떠한 Arguments(인자)를 바인딩할 지를 결정한다.
여기서 대부분 사용자 요청에 대해 Arguments로 변환하는 과정을 거치는데, RequestMappingHandlerAdapter
에 디폴트로 등록된 ArgumentResolver
목록은 아래 그림과 같다.
즉, Controller
구현시 많이 사용했던 RequestParam
, PathVariable
, RequestParam
등에 대한 ArgumentResolver가 구현되어 등록된 것을 확인할 수 있다.
뿐만 아니라, Errors
, BindingResult
, HttpSession
, ServletRequest
, ServletResponse
, Model
같은 타입에 대한 ArgumentResolver가 구현되어 있기 때문에 인자로 전달받을 수 있는 것이다.
따라서, 특정 조건에 맞는 파라미터가 있는 경우에 핸들러를 호출시 원하는 Argument를 바인딩해준다.
또한, 스프링은 사용자가 필요한 ArgumentResolver를 커스텀하게 구현해서 등록할 수 있도록 HandlerMethodArgumentResolver
인터페이스를 제공하고 있다.
만약 HTTP 요청에서 User-Agent Header를 Argument 인자로 전달하고 싶다면 아래와 같이 추가할 수 있다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserAgent {
}
public class UserAgentArgumentResolver implements HandlerMethodArgumentResolver {
// 호출될 method 가 UserAgentArgumentResolver 를 실행 할 수 있는지 판별
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
// UserAgent 어노테이션이 붙은 파라미터가 있을 때만 바인딩해준다.
return methodParameter.hasParameterAnnotation(UserAgent.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
return nativeWebRequest.getHeader("User-Agent");
}
}
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.something.sample.controller"})
public class WebApplicationConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new UserAgentArgumentResolver());
}
}
@Controller
public class TestController {
@GetMapping(path="/test")
public void test(@UserAgent String userAgent) {
System.out.println("userAgent: " + userAgent);
}
}
핸들러 실행 후에는 반환한 값을 처리할 수 있도록 ReturnValueHandler
를 지원한다.
즉, 핸들러가 반환한 값에 대해서 특정 조건에 맞는 ReturnValueHandler
에서 처리하는데, 디폴트 목록은 아래와 같다.
ReturnValueHandler
도 HandlerMethodReturnValueHandler
인터페이스를 구현해서 HandlerAdapter
에 등록할 수 있다.
구체적인 예로, Controller에서 반환한 String 타입의 값이 적절한 View를 찾는 것을 본적이 있을 것이다.
이는 실제로, 반환값을 ViewResolver
로 전달하기 이전에 ViewNameMethodReturnValueHandler
에서 ModelAndViewContainer
에 View 이름을 등록하는 것을 확인할 수 있다.
DispatcherServlet
은 크게 두 가지 방법으로 설정이 가능하다.
web.xml 사용하는 방법은 이전에서도 보았듯이 DispatcherSetvlet
을 정의하고, Context
정보를 기재한다.
Context
정보는 어떤 종류의 컨테이너를 사용할 것인지와 Application Context
에 대한 설정 정보를 담고 있어야 한다.
Context
종류는 크게 두 가지로 나뉘는데, contextClass
에 정의한다.
XmlWebApplicationContext
: XML을 사용해서 Application Context을 설정하는 경우 사용AnnotationConfigWebApplicationContext
: Java Annotation 사용해서 Application Context을 설정하는 경우 사용Application Context
에 대한 설정 정보를 담은 위치는 contextConfigLocation
에 정의한다.
Spring 5.0 이전에는
WebMvcConfigurerAdapter
추상 클래스을 구현해서 설정하였지만, 이후부터는WebMvcConfigurer
인터페이스를 구현해서 설정 정보를 정의하는 것을 권장한다.
따라서, 설정 정보를 담은 경로를 contextConfigLocation
에 정의해준다.
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.something.sample.controller"})
public class WebApplicationConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926);
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("main");
}
@Bean
public InternalResourceViewResolver getInternalResourceViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<display-name>Spring Sample</display-name>
<servlet>
<servlet-name>spring-sample</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
// java annotation 기반의 application context 사용
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
// 설정 파일 경로
<param-value>com.something.sample.config.WebMvcContextConfiguration</param-value>
</init-param>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring-sample</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Java 코드 방식을 이용하는 경우에는 WebApplicationInitializer
인터페이스를 구현해서 DispatcherServlet
을 정의한다.
WebApplicationInitializer
인터페이스를 구현시에는 애플리케이션 구동시 반드시 필요한 설정은 onStartup()
메서드 구현에서 이루어진다.
public class SampleApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// 사용할 Application Context 정의
AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
// 설정 파일 정보
webApplicationContext.register(WebApplicationConfig.class);
// DispatcherServlet 생성
DispatcherServlet dispatcherServlet = new DispatcherServlet(webApplicationContext);
// Servlet 설정
ServletRegistration.Dynamic servlet = servletContext.addServlet("spring-sample", dispatcherServlet);
servlet.setLoadOnStartup(0);
servlet.addMapping("/");
}
}
위 코드를 살펴보면, DispatcherServlet
에서 사용할 Application Context
객체를 생성하고 설정 정보를 담은 클래스를 정보를 등록한다.
마직막에는 Servlet
정보를 설정하고 있는 것을 볼 수 있다.
@EnableWebMvc
어노테이션은 DelegatingWebMvcConfiguration
설정 정보를 Import한다.
구체적으로,DelegatingWebMvcConfiguration
은 WebMvcConfigurationSupport
를 상속받아, DispatcherServlet
에 필요한RequestMappingHandlerMapping
, RequestMappingHandlerAdapter
, ExceptionHandlerExceptionResolver
, MessageConverter
과 같은Bean
들을 자동으로 설정해준다.
뿐만 아니라, WebMvcConfigurer
타입 구현체에 정의한 모든 Configuration Bean을 감지하여 Context를 설정한다.
WebMvcConfigurationSupport
은 Spring MVC를 구성하는 메인 클래스이다.
WebMvcConfigurationSupport
은 Spring MVC를 구성하는데 필요한 HandlerMapping
, HandlerAdapter
HandlerExceptionResolverComposite
, ViewResolver
, AntPathMatcher
, UrlPathHelper
등을 Application Context에 등록한다.
Spring 5.0 이후부터는 WebMvcConfigurer
인터페이스를 구현해서 커스텀 설정 정보를 추가할 수 있다.
@EnableWebMvc
어노테이션이 붙은 클래스는 WebMvcConfigurer
인터페이스를 다시 호출하여 기본 구성을 사용자가 재정의 할 수 있도록 기회를 제공한다.
아래 예제를 보면, configureDefaultServletHandling(DefaultServletHandlerConfigurer)
메서드를 통해서 Default Servlet Handler 사용 여부를 결정할 수 있는 것을 확인할 수 있다.
구체적으로, 일치하지 않는 매핑 정보가 들어왔을 경우 서블릿 컨테이너의 Default Servlet(DefaultServletHttpRequestHandler)
이 사용되도록 설정하는 것이다.
즉, DefaultServletHttpRequestHandler
는 개발자가 설정한 리소스 처리 방식(addResourceHandlers(ResourceHandlerRegistry)
)을 토대로 기본 정적 리소스 처리 방식을 재정의한다.
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.something.sample.controller"})
public class WebApplicationConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/css/**").addResourceLocations("/css/").setCachePeriod(31556926);
registry.addResourceHandler("/img/**").addResourceLocations("/img/").setCachePeriod(31556926);
registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926);
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Bean
public InternalResourceViewResolver getInternalResourceViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
}