Spring MVC - FrontController(Dispatcher Servlet)

2022. 4. 9. 12:02
반응형

스프링 MVC의 내부 동작원리에 대해서 공부해보았다. 스프링 MVC 는 프론트 컨트롤러 패턴과 어댑터 패턴을 사용해서 필요한 부분만 사용할 수 있게 유연하게 HTTP 요청을 처리하고 응답한다. 오늘은 HTTP Message Body에 JSON 형식과 같은 결과값을 반환하는 방식이 아닌 뷰를 렌더링을 해서 사용자에게 화면을 보여주는 FrontController 패턴을 공부하였는데, 동작원리는 대략적으로 공부하면서 그린것을 바탕으로 보면 다음과 같다.

 

그림이 좀 허접해도 양해좀 부탁드리면서.. 자세한 동작원리는 다음과 같다. 이 때, 클라이언트가 웹 서버에 HTTP 요청을 한다고 가정한다.

  1. 프론트 컨트롤러(FrontController)는 사용자의 HTTP 요청에 따라서 HTTP Request Header의 URL을 통해서 요청 URL에 따른 핸들러가 있는지 조회한다.
  2. 핸들러 어댑터 목록에 우리가 조회한 핸들러를 처리할 수 있는 핸들러 어댑터가 존재하는지 조회한다.
  3. 핸들러 어댑터의 handle 메서드를 통해서 핸들러(컨트롤러)가 동작하고, 핸들러(컨트롤러)의 동작이 완료되면 클라이언트에게 보여줄 뷰의 논리적인 이름과 뷰를 렌더링하는데 필요한 정보를 Model에 담아서 반환한다.
  4. 프론트 컨트롤러(FrontController)는 전달받은 뷰의 논리적인 이름을 뷰 리졸버(View Resolver)에게 전달한다.
  5. 뷰 리졸버(View Resolver)는 전달받은 뷰의 논리적인 이름을 실제 뷰의 물리적인 경로로 변환하여 반환한다.
  6. 프론트 컨트롤러(FrontController)는 전달받은 실제 뷰의 물리적인 경로를 보고 클라이언트에게 뷰를 렌더링하여 화면을 뿌려준다.

여기까지 Spring MVC 동작원리의 핵심인 프론트 컨트롤러(FrontController)에 대해서 공부하였는데, 실제 Spring MVC의 FrontController는 디스패처 서블릿(Dispatcher Servlet)이라는 이름으로 되어있다. 

 

디스패처 서블릿이 실제로 어떻게 구현되어있는지 뜯어보자!

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());

				// Http Request Header에서 요청한 Http Method 조회
				String method = request.getMethod();
				boolean isGet = "GET".equals(method);
				if (isGet || "HEAD".equals(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

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

				// 3. 핸들러 어댑터를 통해서 핸들러(컨트롤러)를 실행하고 ModelAndView를 반환한다.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

논리적인 뷰 이름과 모델을 전달받으면 뷰 리졸버를 통해서 뷰를 찾는 코드이다.

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
		// Determine locale for request and apply it to the response.
		Locale locale =
				(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
		response.setLocale(locale);

		View view;
		String viewName = mv.getViewName();
		if (viewName != null) {
			// 뷰 리졸버를 통해서 물리적인 뷰 경로를 찾는다.
			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 {
			// No need to lookup: the ModelAndView object contains the actual View object.
			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() + "'");
			}
		}

		// Delegate to the View object for rendering.
		if (logger.isTraceEnabled()) {
			logger.trace("Rendering view [" + view + "] ");
		}
		try {
			if (mv.getStatus() != null) {
				response.setStatus(mv.getStatus().value());
			}
            //뷰 렌더링
			view.render(mv.getModelInternal(), request, response);
		}
		catch (Exception ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Error rendering view [" + view + "]", ex);
			}
			throw ex;
		}
	}

다음은 위의 모든 과정이 끝나면 뷰를 렌더링하는 코드이다.

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

		boolean errorView = false;

		if (exception != null) {
			if (exception instanceof ModelAndViewDefiningException) {
				logger.debug("ModelAndViewDefiningException encountered", exception);
				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
			}
			else {
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
				mv = processHandlerException(request, response, handler, exception);
				errorView = (mv != null);
			}
		}

		// Did the handler return a view to render?
		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.");
			}
		}

		if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
			// Concurrent handling started during a forward
			return;
		}

		if (mappedHandler != null) {
			// Exception (if any) is already handled..
			mappedHandler.triggerAfterCompletion(request, response, null);
		}
	}

프론트 컨트롤러(FrontController) 패턴을 사용하면 MVC 패턴을 효율적으로 수행할 수 있다!

반응형

BELATED ARTICLES

more