MVC 구조
MVC 구조
동작 순서
- DispatcherServlet으로 클라이언트의 웹 요청이 들어온다.
- 웹 요청을 핸들러 매핑에 위임해 요청 URL에 매핑된 핸들러를 조회한다.
- 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
- 핸들러 어댑터가 실행되고 핸들러 어댑터가 핸들러를 실행시킨다. 이때 핸들러가 반환한 정보를 핸들러 어댑터가 ModelAndView로 변환해서 반환한다.
- 뷰 리졸버가 뷰 이름을 전달받아 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
- DispatcherServlet은 View에게 Model을 전달하고 화면 표시를 요청한다. 이때, Model이 null이면 View를 그대로 사용하고 Model 값이 있으면 View에 Model 데이터를 렌더링한다.
- DispatcherServlet은 View 결과를 클라이언트에게 반환한다.
프론트 컨트롤러 패턴과 DispatcherServlet
DispatcherServlet을 설명하기 전에 프론트 컨트롤러 패턴에 대해 알아보자. 프론트 컨트롤러 패턴이란 모든 요청을 받는 서블릿을 하나로 두고, 서블릿이 요청에 맞는 컨트롤러를 호출해서 처리해주는 것이다. 프론트 컨트롤러 패턴을 도입하면, 컨트롤러를 구현할 때 직접 서블릿을 다루지 않아도 되고, 공통된 로직을 줄임으로써 개발자는 핵심 로직에만 집중할 수 있다.
- 프론트 컨트롤러 우리가 스프링을 이용해 개발을 해오면서 컨트롤러 로직을 작성할 때, 서블릿을 직접 다룬 적이 없었던 이유도 Spring web mvc가 프론트 컨트롤러 패턴을 사용하고 있기 때문이다.
클라이언트로부터 요청이 들어오면 서블릿 컨테이너가 해당하는 서블릿을 실행시키는데 이 때 실행되는 서블릿이 Dispatcher Servlet이다. 공통 작업은 DispatcherServlet에서 처리하고, 이외의 작업은 적절한 세부 컨트롤러를 호출하여 처리한다.
스프링 MVC 동작방식
DispatcherServlet은 FrameworkServlet.java > HttpServlet.java > Servlet.java 를 상속받아 구현한 서블릿으로 Servlet Container에서 들어오는 모든 요청을 먼저 받아 중앙 집중식으로 처리해주는 프론트 컨트롤러다.
DispatcherServlet은 웹 요청에 따른 처리를 해주기 위해 처음으로 doService()
메서드를 호출한다.
그리고 DispatcherServlet의 핵심인 doDispatch()
메서드를 호출한다.
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);
}
먼저 웹 요청을 처리할 수 있는 핸들러를 찾기 위해 getHandler()
메서드를 호출한다.
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
핸들러 매핑을 순서대로 실행해서, 핸들러를 찾는다.
0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping : URL과 일치하는 이름을 갖는 스프링 빈의 이름으로 핸들러를 찾는다.
2 = ControllerBeanNameHandlerMapping : URL과 일치하는 이름을 갖는 스프링 빈의 아이디로 핸들러를 찾는다.
핸들러 매핑으로 HandlerExecutionChain을 결정하는데 HandlerExecutionChain 구현체는 실제로 호출된 핸들러에 대한 참조를 가지고 있다. 즉 무엇이 실행되어야 될지 알고 있는 객체라고 말할 수 있다.
HandlerExecutionChian이 발견되지 않아 null을 반환하면 404 Not Found를 전달하고, 발견되면 HandlerAdapter 결정하러간다.
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
찾은 핸들러를 실행하기 위해 해당 핸들러를 맞는 핸들러 어댑터를 탐색해야 한다. 이를 위해 getHandlerAdapter()
메서드를 호출한다. 핸들러 어댑터도 순서대로 supports()
메서드를 실행해서 찾는다.
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
핸들러 어댑터를 이용하여 handle()
메서드로 핸들러의 메서드를 실행하고 그 결과로 ModelAndView를 반환한다.
핸들러가 작업을 마치고 정보를 ModelAndView 타입 오브젝트에 담아서 DispatcherServlet에 돌려주는 방법은 크게 두 가지다. 하나는 View 타입의 오브젝트를 돌려주는 방법이고, 다른 하나는 뷰 이름을 돌려주는 방법이다. 뷰 이름을 돌려주는 경우에는 실제 사용할 뷰를 결정해주는 뷰 리졸버가 필요하다.
다음으로 processDispatchResult()
메서드가 호출된다.
private void processDispatchResult(HttpServletRequest request,
HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView
mv, Exception exception) throws Exception {
render(mv, request, response);
}
processDispatchResult()
메서드는 render()
메서드를 호출한다.
protected void render(ModelAndView mv, HttpServletRequest request,
HttpServletResponse response) throws Exception {
View view;
String viewName = mv.getViewName();
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
view.render(mv.getModelInternal(), request, response);
}
render()
는 View의 논리 이름을 물리 이름으로 변환시키기 위해 resolveViewName()
메서드를 호출한다.
@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
뷰 리졸버의 여러 구현체의 resolveViewName()
메서드를 호출하여 View의 논리 이름을 물리 이름으로 변환시킨다.
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용)
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
스프링 부트는 InternalResourceViewResolver 뷰 리졸버를 자동으로 등록하는데, 이때 application.properties 에 등록한 spring.mvc.view.prefix , spring.mvc.view.suffix 설정 정보를 사용해서 등록한다.
마지막으로, View 객체의 render()
메소드를 호출하여 View에 Model 데이터를 렌더링한다.
참고
스프링 핵심 원리 - 기본편 (김영한 님) 토비의 스프링 - vol.1 (이일민 님)