스프링 MVC
위의 직접 만들어본 MVC와 아래의 스프링 MVC 프레임워크 구조를 비교해보자.
매우 유사한 것을 알 수 있다.
- DispatcherServlet -> FrontController
- HandlerMapping -> handlerMappingMap
- HandlerAdapter -> MyHandlerAdapter
- ModelAndView -> ModelView
- ViewResolver -> viewResolver
- View -> MyView
스프링 MVC 프레임워크의 핵심은 DispatcherServlet이다.
DispatcherServlet의 구조
org.springframework.web.servlet.DispatcherServlet
전체 코드는 매우매우 길다.
DispatcherServlet 서블릿 등록
- DispatcherServlet도 부모 클래스에서 HttpServlet을 상속받아 사용하고, 서블릿으로 동작한다.
- DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet
- 스프링부트는 DispatcherServlet을 서블릿으로 자동으로 등록하면서 모든 경로(urlPatterns= “/”)에 대해 매핑한다.
- 더 자세한 경로가 우선순위가 높다. 그래서 기존에 등록한 서블릿도 함께 동작한다.
요청 흐름
- 서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출된다.
- 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해두었다.
- FrameworkServlet.service()를 시작으로 여러 메서드가 호출되며 DispatcherServlet.doDispatch()가 호출된다.
doDispatch()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
ModelAndView mv = null;
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
// 뷰 렌더링 호출
render(mv, request, response);
}
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
View view;
String viewName = mv.getViewName();
// 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
// 8. 뷰 렌더링
view.render(mv.getModelInternal(), request, response);
}
작성했던 프론트 컨트롤러의 service() 와 비슷한 구조인 것을 확인할 수 있다.
동작 순서
- 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
- 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
- 핸들러 어댑터 실행: : 핸들러 어댑터를 실행한다.
- 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행한다.
- ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해 반환한다.
- viewResolver 호출 : 뷰 리졸버를 찾고 실행한다.
- View 반환 : 뷰 리졸버가 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
- 뷰 렌더링 : 뷰를 통해 뷰를 렌더링한다.
스프링 MVC의 강점
DispatcherServlet 코드의 변경 없이 원하는 기능을 변경하거나 확장할 수 있다. 대부분이 확장가능하도록 인터페이스로 제공된다.
이러한 인터페이스들만 구현해 DispatcherServlet에 등록하면 개인의 커스텀된 컨트롤러를 만들 수 있다.
물론 스프링 MVC의 기능을 확장하거나 나만의 컨트롤러를 만드는 일은 웬만하면 없다. 이미 필요한 대부분이 구현 되어있기 때문이다.
그러나 이렇게 동작 방식을 어느정도 알아둔다면 향후 문제가 발생했을 때 어떤 부분에서 문제가 발생했는지 쉽게 파악하고 문제를 해결할 수 있다.
그리고 확장 포인트가 필요할 때 어떤 부분을 확장해야 할지 감을 잡을 수 있다.
핸들러 매핑과 핸들러 어댑터
핸들러 매핑과 핸들러 어댑터가 어떤 것들이 어떻게 사용되는지 알아본다.
과거에 주로 사용했던 지금은 사용하지 않는 스프링의 간단한 컨트롤러로 이해해본다.
1
2
3
4
public interface Contorller{
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
참고로 Controller 인터페이스는 @Controller 애노테이션과 다르다.
그리고 이 컨트롤러를 상속받는 컨트롤러를 만들어본다.
@Component로 스프링 빈 등록이 되었는데, 이 빈의 이름으로 URL을 입력해보면 handleRequest가 실행된다.
이 컨트롤러는 어떻게 호출되는 것인가?
이 컨트롤러가 호출되려면 아래의 2가지가 필요하다.
- HandlerMapping
- 핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야한다.
- ex) 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.
- HandlerAdapter
- 핸들러 매핑을 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
- ex) Controller 인터페이스를 실행할 수 잇는 핸들러 어댑터를 찾고 실행해야 한다.
스프링부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터
실제로 더 많지만 중요한 부분만 본다.
- HandlerMapping
1
2
3
4
5
//애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
0 = RequestMappingHandlerMapping
//스프링 빈의 이름으로 핸들러를 찾는다.
1 = BeanNameUrlHandlerMapping
- HandlerAdapter
1
2
3
4
5
6
7
0 = RequestMappingHandlerAdapter
//HttpRequestHandler 처리
1 = HttpRequestHandlerAdapter
//Controller 인터페이스 처리
2 = SimpleControllerHandlerAdapter
실행 순서
- 핸들러 매핑으로 핸들러 조회
- HandlerMapping을 순서대로 실행해 핸들러를 찾는다.
- 위의 경우 BeanNameUrlHandlerMapping이 실행되고 핸들러인 OldController를 반환한다.
- 핸들러 어댑터 조회
- HandlerAdapter의 supports()를 순서대로 호출한다.
- SimpleControllerHandlerAdapter가 Controller 인터페이스를 지원하므로 대상이 된다.
- 핸들러 어댑터 실행
- 디스패처 서블릿이 조회한 SimpleControllerHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨준다.
- SimpleControllerHandlerAdapter는 핸들러인 OldController를 내부에서 실행하고 그 결과를 반환한다.
@RequestMapping
가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMapping~ 이다.
이 @RequestMapping의 앞글자를 따서 만든 이름인데, 이것이 지금 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터이다.
실무에서는 99.9% 이 방식의 컨트롤러를 사용한다.
뷰 리졸버
전의 OldController에서 ModelAndView를 반환해보고 실행해보자.
원하는대로 new-form을 불러오지 못한다. 논리적 주소로 보냈는데 물리적 주소로 변환되지 못했기 때문이다.
application.properties에 아래를 추가해 실행하면 정상적으로 작동된다.
1
2
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
왜?
스프링 부트는 InternalResourceViewResolver 라는 뷰 리졸버를 자동으로 등록하는데, 이 때 application.properties 에 등록한 위의 코드 설정정보를 사용해 등록하기 때문이다.
스프링 부트가 자동 등록하는 뷰 리졸버
실제로 더 많지만 중요한 부분만.
1
2
3
4
5
//빈 이름으로 뷰를 찾아 반환한다 (ex - 엑셀 파일 생성 기능에 사용)
1 = BeanNameViewResolver
//JSP를 처리할 수 있는 뷰를 반환한다.
2 = InternalResourceViewResolver
뷰 리졸버 작동 순서
- 핸들러 어댑터 호출
- 핸들러 어댑터를 통해 new-form 이라는 논리 뷰 이름을 획득한다.
- ViewResolver 호출
- new-form 이라는 뷰 이름으로 viewResolver를 순서대로 호출한다.
- BeanNameViewResolver는 new-form 이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없다.
- InternalResourceViewResolver가 호출된다.
- InternalResourceViewResolver
- 이 뷰 리졸버는 InternalResourceView 를 반환한다.
- 뷰 - InternalResourceView
- JSP처럼 forward()를 호출해 처리할 수 있는 경우에 사용한다.
- view.render()
- view.render()가 호출되고 InternalResourceView는 forward()를 사용해 JSP를 실행한다.
참고
다른 뷰는 실제 뷰를 렌더링 하지만 JSP의 경우 forward() 를 통해 해당 JSP로 이동해야 렌더링이 된다. JSP를 제외한 나머지 뷰 템플릿들은 forward() 과정 없이 바로 렌더링 된다.
Thymeleaf 뷰 템플릿을 사용하면 ThymeleafViewResolver를 등록해야 한다. 라이브러리만 추가하면 스프링 부트가 이런 작업도 모두 자동화해준다.