Spring MVC 프레임워크 동작 방식
스프링 MVC는 웹 요청을 처리하기 위해 다양한 구성 요소를 연동하는데 이 페이지에서는 핵심 구성 요소에 대해 살펴보도록 하자.
1. 스프링 MVC 핵심 구성 요소
스프링 MVC의 핵심 구성 요소와 각 요소 간의 관계는 아래 그림과 같이 정리할 수 있다.
-
«spring bean»이라고 표시한 것은 스프링 빈으로 등록해야 하는 것을 의미한다. 회색 배경을 가진 구성 요소는 개발자가 직접 구현해야 하는 요소이다.
-
DispatcherServlet은 모든 연결을 담당한다. 웹 브라우저로부터 요청이 들어오면 DispatcherServlet은 그 요청을 처리하기 위한 컨트롤러 객체를 검색한다. 이때 DispatcherServlet은 직접 컨트롤러를 검색하지 않고 HandlerMapping이라는 빈 객체에게 컨트롤러 검색을 요청한다.(2번 과정)
-
HandlerMapping은 클라이언트 요청 경로를 이용해서 이를 처리할 컨트롤러 빈 객체를 DispatcherServlet에 전달한다. 예를 들어 웹 요청 경로가 ‘/hello/’라면 등록된 컨트롤러 빈 중에서 ‘/hello’ 요청 경로를 처리할 컨트롤러를 리턴한다.
-
컨트롤러 객체를 DispatcherServlet이 전달받았다고 해서 바로 컨트롤러 객체의 메서드를 실행할 수 있는 것은 아니다. DispatcherServlet은 @Controller 애노테이션을 이용해서 구현한 컨트롤러뿐만 아니라 스프링 2.5까지 주로 사용됐던 Controller 인터페이스를 구현한 컨트롤러, 그리고 특수 목적으로 사용되는 HttpRequestHandler 인터페이스를 구현한 클래스를 동일한 방식으로 실행할 수 있도록 만들어졌다. @Controller, Controller 인터페이스, HttpRequestHandler 인터페이스를 동일한 방식으로 처리하기 위해 중간에 사용되는 것이 바로 HandlerAdapter 빈이다.
-
DispatcherServlet은 HandlerMapping이 찾아준 컨트롤러 객체를 처리할 수 있는 HandlerAdapter 빈에게 요청 처리를 위임한다.(3번 과정) HandlerAdapter는 컨트롤러의 알맞은 메서드를 호출해서 요청을 처리하고(4~5번 과정) 그 결과를 DispatcherServlet에 리턴한다.(6번 과정) 이때 HandlerAdapter는 컨트롤러의 처리 결과를 ModelAndView라는 객체로 변환해서 DispatcherServlet에 리턴한다.
-
HandlerAdapter로부터 컨트롤러의 요청 처리 결과를 ModelAndView로 받으면 DispatcherServlet은 결과를 보여줄 뷰를 찾기 위해 ViewResolver 빈 객체를 사용한다.(7번 과정) ModelAndView는 컨트롤러가 리턴한 뷰 이름을 담고 있는데 ViewResolver는 이 뷰 이름에 해당하는 View 객체를 찾거나 생성해서 리턴한다. 응답을 생성하기 위해 JSP를 사용하는 ViewResolver는 매번 새로운 View 객체를 생성해서 DispatcherServlet에 리턴한다.
-
DispatcherServlet은 ViewResolver가 리턴한 View 객체에게 응답 결과 생성을 요청한다.(8번 과정) JSP를 사용하는 경우 View 객체는 JSP를 실행함으로써 웹 브라우저에 전송할 응답 결과를 생성하고 이로써 모든 과정이 끝이 난다.
1.1 컨트롤러와 핸들러
클라이언트의 요청을 실제로 처리하는 것은 컨트롤러이고 DispatcherServlet은 클라이언트의 요청을 전달받은 창구 역할을 한다. 컨트롤러를 찾아주는 객체는 ControllerMapping 타입이어야 할 것 같은데 실제는 HandlerMapping이다. 왜 HandlerMapping일까?
스프링 MVC는 웹 요청을 처리할 수 있는 범용 프레임워크이다. 이 책에서는 @Controller 애노테이션을 붙인 클래스를 이용해서 클라이언트의 요청을 처리하지만 원한다면 자신이 직접 만든 클래스를 이용해서 클라이언트의 요청을 처리할 수도 있다. 즉, DispatcherServlet 입장에서는 클라이언트 요청을 처리하는 객체의 타입이 반드시 @Controller를 적용한 클래스일 필요는 없다. 실제로 클라이언트의 요청을 처리하기 위해 제공하는 타입 중에는 HttpRequestHandler도 존재한다.
이런 이유로 스프링 MVC는 웹 요청을 실제로 처리하는 객체를 핸들러(Handler)라고 표현하고 있으며 @Controller 적용 객체나 Controller 인터페이스를 구현한 객체는 모두 스프링 MVC 입장에서는 핸들러가 된다.
DispatcherServlet은 핸들러 객체의 실제 타입에 상관없이 실행 결과를 ModelAndView라는 타입으로만 받을 수 있으면 된다. 그런데 핸들러의 실제 구현 타입에 따라 ModelAndView를 리턴하는 객체도(Controller 인터페이스를 구현한 클래스의 객체) 있고 그렇지 않은 객체도 있다. 따라서 핸들러의 처리 결과를 ModelAndView로 변환해주는 객체가 필요하며 HandlerAdapter가 이 변환을 처리해준다.
핸들러 객체의 실제 타입마다 그에 알맞은 HandlerMapping과 HandlerAdapter가 존재하기 때문에, 사용할 핸들러의 종류에 따라 해당 HandlerMapping과 HandlerAdpater를 스프링 빈으로 등록해야 한다. 물론 스프링이 제공하는 설정 기능을 사용하면 이 두 종류의 빈을 직접 등록하지 않아도 된다.
2. DispatcherSerlet과 스프링 컨테이너
Spring MVC 시작의 web.xml 파일을 보면 다음과 같이 DispatcherServlet의 contextConfiguration 초기화 파라미터를 이용해서 스프링 설정 클래스 목록을 전달했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation<param-name>
<param-value>
config.MvcConfig
config.ControllerConfig
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
DispatcherServlet은 전달받은 설정 파일을 이용해서 스프링 컨테이너를 생성하는데 앞에서 언급한 HandlerMapping, HandlerAdapter, 컨트롤러, ViewResolver 등의 빈은 아래 그림처럼 DispatcherServlet이 생성한 스프링 컨테이너에서 구한다. 따라서 DispatcherServelt이 사용하는 설정 파일에 이들 빈에 대한 정의가 포함되어 있어햐 한다.
3. @Controller를 위한 HandlerMapping과 HandlerAdpater
@Controller 적용 객체는 DispatcherServlet 입장에서 보면 한 종류의 핸들러 객체이다. DispatcherServlet은 스프링 컨테이너에서 HandlerMapping과 HandlerAdapter타입의 빈을 사용하므로 핸들러에 알맞은 HandlerMapping 빈과 HandlerAdapter 빈이 스프링 설정에 등록되어 있어야 한다. 이때 사용하는 것이 @EnableWebMvc 애노테이션이다.
1
2
3
4
5
@Configuration
@EnableWebMvc
public class MvcConfig{
...
}
위 설정은 매우 다양한 스프링 빈 설정을 추가해준다. 이 태그가 빈으로 추가해주는 클래스 중에는 @Controller 타입의 핸들러 객체를 처리하기 위한 다음의 두 클래스도 포함되어 있다.
- org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
- org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
RequestMappingHandlerMapping은 @Controller 애노테이션이 적용된 객체의 요청 매핑 애노테이션(@GetMapping) 값을 이용해서 웹 브라우저의 요청을 처리할 컨트롤러 빈을 찾는다.
RequestMappingHandlerAdapter은 컨트롤러의 메서드를 알맞게 실행하고 그 결과를 ModelAndView 객체로 변환해서 DispatcherServlet에 리턴한다.다음 코드를 보자
1
2
3
4
5
6
7
8
9
10
@Controller
public class HelloController{
@RequestMapping("/hello")
public String hello(Model model,
@RequestParam(value = "name", required = false) String name){
model.addAttribute("greeting", "안녕하세요. " + name);
return "hello";
}
}
RequestMappingHandlerAdapter 클래스는 “/hello” 요청 경로에 대해 hello() 메서드를 호출한다. 이때 Model 객체를 생성해서 첫 번째 파라미터로 전달한다. 비슷하게 이름이 “name”인 HTTP 요청 파라미터의 값을 두 번째 파라미터로 전달한다.
RequestMappingHandlerAdapter는 컨트롤러 메서드 결과 값이 String 타입이면 해당 값을 뷰 이름으로 갖는 ModelAndView 객체를 생성해서 DispatcherServlet에 리턴한다. 이때 첫 번째 파라미터로 전달한 Model 객체에 보관된 값도 ModelAndView에 함께 전달된다.
4. WebMvcConfigurer 인터페이스와 설정
@EnableWebMvc 애노테이션을 사용하면 @Controller 애노테이션을 붙인 컨트롤러를 위한 설정을 생성한다. 또한 @EnableWebMvc 애노테이션을 사용하면 WebMvcConfigurer 타입의 빈을 이용해서 MVC 설정을 추가로 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer{
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/WEB-INF/view/", ".jsp");
}
}
여기서 설정 클래스는 WebMvcConfigurer 인터페이스를 상속하고 있다. @Configuration 애노테이션을 붙인 클래스 역시 컨테이너에 빈으로 등록되므로 MvcConfig 클래스는 WebMvcConfigurer 타입의 빈이 된다.
@EnableWebMvc 애노테이션을 사용하면 WebMvcConfigurer 타입의 빈 객체의 메서드를 호출해서 MVC 설정을 추가한다. 예를 들어 ViewResolver 설정을 추가하기 위해 WebMvcConfigurer 타입의 빈 객체의 configureViewResolvers() 메서드를 호출한다. 따라서 WebMvcConfigurer 인터페이스를 구현한 설정 클래스는 configureViewResolvers() 메서드를 재정의해서 알맞은 뷰 관련 설정을 추가하면 된다.
스프링 5 버전은 자바 8 버전붙 지원하는 디폴트 메서드를 사용해서 WebMvcConfigurer 인터페이스의 메서드에 기본 구현을 제공하고 있다. 다음은 스프링 5 버전이 제공하는 WebMvcConfigurer 인터페이스의 일부 구현 코드이다.
1
2
3
4
5
6
7
public interface WebMvcConfigurer
default void configurePathMatch(PathMachConfigurer configurer){}
default void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer){
}
...생략
기본 구현은 모두 빈 구현이다. 이 인터페이스를 상속한 설정 클래스는 재정의가 필요한 메서드만 구현하면 된다.
5. JSP를 위한 ViewResolver
컨트롤러 처리 결과를 JSP를 이용해서 생성하기 위해 다음 설정을 사용한다.
1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer{
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/WEB-INF/view/", ".jsp");
}
}
WebMvcConfigurer 인터페이스에 정의된 configureViewResolvers() 메서드는 ViewResolverRegistry 타입의 registry 파라미터를 갖는다. ViewResolverRegistry.registry() 메서드를 사용하면 JSP를 위한 ViewResolver를 설정할 수 있다.
위 설정은 org.springframwork.web.servlet.view.InternalResouceViewResolver 클래스를 이ㅛㅇ해서 다음과 같은 빈을 등록한다.
1
2
3
4
5
6
7
@Bean
public ViewResolver viewResolver(){
InternalResourceViewResolver vr = new InternalResourceViewResolver();
vr.setPrefix("/WEB-INF/view/");
vr.setSuffix(".jsp");
return vr;
}
컨트롤러의 실행 결과를 받은 DispatcherServlet은 ViewResolver에게 뷰 이름에 해당하는 View 객체를 요청한다. 이때 InternalResourceResolver는 “prefix+뷰이름+suffix”에 해당하는 경로를 뷰 코드로 사용하는 InternalResourceView 타입의 View 객체를 리턴한다. DispatcherServlet이 InternalResourceView 객체에 응답 생성 요청하면 InternalResouceView 객체는 경로를 저장한 JSP 코드를 실행해서 응답 결과를 생성한다.
DispatcherServlet은 컨트롤러의 실행 결과를 HandlerAdapter를 통해서 ModelAndView 형태롤 받는다고 했다. Model에 담긴 값은 View 객체에 Map 형식으로 전달된다. 예를 HelloController 클래스는 다음과 같인 Model에 “greeting”속성을 설정했다.
1
2
3
4
5
6
7
8
9
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello(Model model,
@RequestParam(value = "name", required = false) String name) {
model.addAttribute("greeting", "안녕하세요, " + name);
return "hello";
}
}
이 경우 DispatcherServlet은 View 객체에 응답 생성을 요청할 때 greeting 키를 갖는 Map 객체를 View 객체에 전달한다. View 객체는 전달받은 Map 객체에 담긴 값을 이용해서 알맞은 응답 결과를 출력한다. InternalResourceView는 Map 객체에 담겨 있는 키 값을 request.setAttribute()를 이용해서 request 속성에 저장한다. 그런 뒤 해당 경로의 JSP를 실행한다.
결과적으로 컨트롤러에서 지정한 Model 속성은 request 객체 속성으로 JSP에 전달되기 때문에 JSP는 다음과 같이 모델에 지정한 속성 이름을 사용해서 값을 사용할 수 있게 된다.
6. 디폴트 핸들러와 HandlerMapping의 우선순위
web.xml 설정을 보면 DispatcherServlet에 대한 매핑 경로를 다음과 같이 ‘/’로 주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
... 생략
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
매핑 경로가 ‘/’인 경우, .jsp로 끝나는 요청을 제외한 모든 요청을 DispatcherServlet이 처리한다. 즉 /index.html이나 /css/bootstrap.css 와 같이 확장자가 .jsp가 아닌 모든 요청을 DispatcherServlet이 처리하게 된다.
그런데 @EnableWebMvc 애노테이션이 등록하는 HandlerMapping은 @Controller 애노테이션을 적용한 빈 객체가 처리할 수 있는 요청 경로만 대응할 수 있다. 예를 들어 등록된 컨트롤러가 한 개이고 그 컨트롤러가 @GetMapping(“/hello”) 설정을 사용한다면, /hello 경로만 처리할 수 있게 된다. 따라서 “/index.html”이나 “/css/bootstrap.css”와 같은 요청을 처리할 수 있는 컨트롤러 객체를 찾지 못해 DispatcherServlet은 404 응답을 전송한다.
“/index.html”이나 “/css/bootstrap.css”와 같은 경로를 처리하기 위한 컨트롤러 객체를 직접 구현할 수도 있지만, 그보다는 WebMvcConfigurer의 configureDefaultServletHandling() 메서드를 사용하는 것이 편리하다.
1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer{
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
위 설정에서 DefaultServletHandlerConfigure.enable() 메서드는 다음의 두 빈 객체를 추가한다.
- DefaultServletHttpRequestHandler
- SimpleUrlHandlerMapping
DefaultServletHttpRequestHandler는 클라이언트의 모든 요청을 WAS(웹 어플리케이션 서버, 톰캣이나 웹로직 등)가 제공하는 디폴트 서블릿에 전달한다. 예를 들어 “index.html”에 대한 처리를 DefaultServletHttpRequestHandler에 요청하면 이 요청을 다시 디폴트 서블릿에 전달해서 처리하도록 한다.
그리고 SimpleUrlHandlerMapping을 이용해서 모든 경로(“/**“)를 DefaultServletHttp RequestHandler를 이용해서 처리하도록 설정한다.
@EnableWebMvc 애노테이션이 등록하는 RequestMappingHandleMapping 적용 순서가 DefaultServletHandlerConfigurer.enable() 메서드가 등록하는 SimpleUrlHandlerMapping의 우선순위 보다 높다. 때문에 웹 브라우저의 요청이 들어오면 DispatcherServlet은 다음과 같은 방식으로 요처을 처리한다.
- RequestMappingHandlerMapping을 사용해서 요청을 처리할 핸들러를 검색한다.
- 존재하면 해당 컨트롤러를 이용해서 요청을 처리한다.
- 존재하지 않으면 SimpleUrlHandlerMapping을 사용해서 요청을 처리할 핸들러를 검색하낟.
- DefaultServletHandlerConfigurer.enable() 메서드가 등록한 SimpleUrlHandlerMapping은 “/**” 경로(즉 모든 경로)에 대해 DefaultServletHttpRequestHandler를 리턴한다.
- DispatcherServlet은 DefaultServletHttpHandler에 처리를 요청한다.
- DefaultServletHttpHandler는 디폴트 서블릿에 처리를 위임한다.
DefualtServletHandlerConfigurer.enable() 외에 몇몇 설정도 SimpleUrlHandlerMapping을 등록하는데 DefualtServletHandlerConfigurer.enable()이 등록하는 우선순위가 가장 낮다. 이 설정으로 하면 별도 설정이 없는 모든 요청 경로를 디폴트 서블릿이 처리하게 된다.
7. 정리
DispatcherServlet은 웹 브라우저의 요청을 받기 위한 창구 역할을 하고, 다른 주요 구성 요소들을 이용해서 요청 흐름을 제어하는 역할을 한다.
HandlerMapping을 클라이언트의 요청을 처리할 핸들러 객체를 찾아준다.
Handler(커맨드) 객체는 클라이언트의 요청을 실제로 처리한 뒤 뷰 정보와 모델을 설정한다.
HandlerAdapter는 DispatcherServlet과 핸들러 객체 사이의 변환을 알맞게 처리해 준다.
ViewResolver는 요청 처리 결과를 생성할 View를 찾아준다.
View는 최종적으로 크라리언트에 응답을 생성해서 전달한다.
Ref.
- 최범균, 스프링프로그래밍입문5, 가메출판사.