TIL

스프링 MVC 구조

MVC 패턴 - 개요

MVC 필요성

스프링 MVC 이전의 문제점

스프링 MVC 이전에는 공통 처리가 어렵다는 문제가 있었다.

RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

기능이 복잡해질 수 록 컨트롤러에서 공통으로 처리해야 하는 부분이 점점 더 많이 증가할 것이다. 공통 기능을 한 번에 처리할 수 없을까?

프론트 컨트롤러 패턴

img.png

스프링 MVC 구조

img_1.png

DispatcherServlet 구조 살펴 보기

org.springframework.web.servlet.DispatcherServlet

상속 구조 - DispatcherServletFrameworkServletHttpServletBeanHttpServlet

요청 흐름

  1. 서블릿이 호출되면 HttpServlet이 제공하는 serivce()가 호출된다.
  2. 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해두었다.
  3. FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서 DispacherServlet.doDispatch()가 호출된다.

DispacherServlet.doDispatch()

핵심만 요약한 소스 코드

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) 
																															throws Exception {
   HttpServletRequest processedRequest = request;
   HandlerExecutionChain mappedHandler = null;
   boolean multipartRequestParsed = false;

   WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

   try {
      ModelAndView mv = null;
      Exception dispatchException = null;

      try {
         processedRequest = checkMultipart(request);
         multipartRequestParsed = (processedRequest != request);

         **// 1. 핸들러 조회
         mappedHandler = getHandler(processedRequest);**
         if (mappedHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
         }

         **// 2. 핸들러 어댑터 조회
         HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());**

         if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;
         }

         **// 3. 핸들러 어댑터 실행 -> 4. 어댑터로 핸들러 실행 -> 5. ModelAndView 반환
         mv = ha.handle(processedRequest, response, mappedHandler.getHandler());**

         applyDefaultViewName(processedRequest, mv);
         mappedHandler.applyPostHandle(processedRequest, response, mv);
      }
      catch (Exception ex) {
         dispatchException = ex;
      }
      catch (Throwable err) {
         dispatchException = new NestedServletException("Handler dispatch failed", err);
      }
			// 결과 처리
      processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
 
// ...  
}

private void processDispatchResult(
			HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {

		boolean errorView = false;

		if (mv != null && !mv.wasCleared()) {
			**// 뷰 렌더링 호출
			render(mv, request, response);**
			if (errorView) {
				WebUtils.clearErrorRequestAttributes(request);
			}
		}
		else {
			if (logger.isTraceEnabled()) {
				logger.trace("No view rendering, null ModelAndView returned.");
			}
		}
}

protected void render(
						ModelAndView mv, HttpServletRequest request, HttpServletResponse response
) throws Exception {

		Locale locale =
				(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
		response.setLocale(locale);

		View view;
		String viewName = mv.getViewName();
		if (viewName != null) {
			**// 6. 뷰 리졸버를 통해 뷰 찾기, 7. View 반환
			view = resolveViewName(viewName, mv.getModelInternal(), locale, request);**
			if (view == null) {
				throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'");
			}
		}
		else {
			view = mv.getView();
			if (view == null) {
				throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name '" + getServletName() + "'");
			}
		}
		try {
			if (mv.getStatus() != null) {
				request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
				response.setStatus(mv.getStatus().value());
			}
			**// 8. 뷰 렌더링
			view.render(mv.getModelInternal(), request, response);**
		}
		catch (Exception ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Error rendering view [" + view + "]", ex);
			}
			throw ex;
		}
	}
  1. 핸들러 조회: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
  2. 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
  3. 핸들러 어댑터 실행: 핸들러 어댑터를 실행한다.
  4. 핸들러 실행: 핸들러 어댑터가 실제 핸들러를 실행한다.
  5. ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.
  6. viewResolver 호출: 뷰 리졸버를 찾고 실행한다.
  7. JSP의 경우: InternalResourceViewResolver가 자동 등록되고, 사용된다.
  8. View 반환: 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
  9. JSP의 경우 InternalResourceView(JstlView)를 반환하는데, 내부에 forward() 로직이 있다.
  10. 뷰 렌더링: 뷰를 통해서 뷰를 렌더링 한다.

핸들러 매핑과 핸들러 어댑터

컨트롤러가 호출되려면 다음 2가지가 필요하다.

정리