[SPRING] Dispatcher Servlet

[ Spring MVC ]

 

Spring에서 제공하는 웹 모듈로 Model, VIew, Controller 구성요소를 사용해 클라이언트의 요청을 처리하고 다양한 리턴 응답을 할 수 있는 프레임워크이다.

 

 

[ MVC 흐름도 ]

  1. 클라이언트의 요청은 DispatcherServlet에게 전달된다.
  2. DispatcherServlet은 해당 요청을 분석하여 HandlerMapping 목록에서 이 요청을 처리할 수 있는 핸들러를 가져온다.
  3. DispatcherServlet은 HandlerAdapter 목록 중 2번 과정에서 가져온 핸들러를 지원(support)하는 HandlerAdapter를 가져온다.
  4. DispatcherServlet은 3번 과정에서 가져온 HandlerAdapter.handle()을 호출한다.
  5. HandlerAdapter를 통해서 핸들러(Controller)를 호출한다.
  6. HandlerAdapter는 결과적으로 ModelAndView를 반환한다.
  7. DispatcherServlet은 ViewResolver.resolveViewName()를 호출한다.
  8. ViewResolver는 전달받은 뷰 이름에 맞는 View 객체를 반환한다.
  9. DispatcherServlet은 View.render(model)를 호출하여 렌더링하여 응답을 만든다.
  10. 응답은 클라이언트에게 전달된다.

[ 웹 모듈 ]

 

웹 모듈은 웹 어플리케이션을 의미하며 JSP ( Java Server Pages ), HTML 와 같은 정적 컨텐츠를 단일 배치 가능 단위로 조합하여 작성하며 웹 모듈은 WAR 파일에 저장이 된다.

하나 이상의 서블릿, JSP, HTML 파일을 포함하며, web.xml 파일에 모듈의 내용을 선언한다.

 

[ DispatcherSevlet ]

디스패처 서블릿을 Front Controller 라고도 부른다. 스프링 MVC 구조의 중요한 역할을 맡는다.

DispatcherServlet Diagram

Dispatcher Servlet은 Http Servlet 을 상속 받으며, 다양한 함수(ex. doGet(), doPost() 등)를 오버라이드 하여 사용할 수 있다.

 

[ doDispatch Method ]

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

			// Determine handler for the current request.
			mappedHandler = getHandler(processedRequest);
			if (mappedHandler == null) {
				noHandlerFound(processedRequest, response);
				return;
			}

			// Determine handler adapter for the current request.
			HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

			// Process last-modified header, if supported by the handler.
			String method = request.getMethod();
			boolean isGet = HttpMethod.GET.matches(method);
			if (isGet || HttpMethod.HEAD.matches(method)) {
				long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
				if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
					return;
				}
			}

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

			// Actually invoke the handler.
			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);
			}
		}
	}
}

 

[ checkMultipart ]

checkMultipart는 파라미터로 request 를 가진다.

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



protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
	
    // 코드 생략    
	return request;
}

multipart 요청이라면 요청을 감싸 MultipartHttpServletRequest로 만들어 파일에 접근할 수 있는 메소드를 제공한다.

 

- Multipart란 HTTP의 한 종류로 HTTP 서버로 파일 혹은 데이터를 보내기 위한 요청 방식이다.

 

[ getHandler ]

getHandler 함수는 파라미터로 request를 가진다.

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

mappedHandler = getHandler(processedRequest);

@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;
}

 

HandlerMapping 리스트를 반복문으로 순환하며 어떤 핸들러를 매핑할건지 찾고 @RequestMapping을 통해 RequestMappingHandlerMapping 객체를 이용하여 핸들러를 가져온다.

 

[ getHandlerAdapter ]

getHandlerAdapter 함수는 파라미터로 handler를 가진다.

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

mappedHandler = getHandler(processedRequest);

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

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");
}

handlerAdapters 리스트에서 for문으로 순환하여 핸들러를 지원하는 어댑터를 찾는다. 일반적으로 @RequestMapping을 사용하면 RequestMappingHandlerAdapter 객체가 반환된다.

 

[ preHandle ]

 

preHandle 메소드는 스프링 인터셉터에서 제공하는 함수로 파라미터로 request, response 를 가진다

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

mappedHandler = getHandler(processedRequest);

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

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

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
	for (int i = 0; i < this.interceptorList.size(); i++) {
		HandlerInterceptor interceptor = this.interceptorList.get(i);
		if (!interceptor.preHandle(request, response, this.handler)) {
			triggerAfterCompletion(request, response, null);
			return false;
		}
		this.interceptorIndex = i;
	}
	return true;
}

스프링 인터셉터에서 제공하는 함수인 preHandle()을 실행시켜 false 값이 리턴되면 doDispatch 메소드는 끝난다.

 

[ ha.handle ]

ha.handle 메소드는 파라미터로 request, response, handler 를 가진다.

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

mappedHandler = getHandler(processedRequest);

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

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

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

handle() 메소드는 HandlerAdapter 인터페이스에 정의되어 있으며 리턴타입이 ModelAndView이다.

 

Handler에는 크게 @Controller 와 @RestController가 있다.

 

@Controller : 응답이 view(HTML, JSP) 이다.

@RestController : 응답이 JSON, String 이다.

 

RestController가 Handler로 매핑된 경우 반환하는 타입이 String이나 JSON이기 때문에 ModelAndView의 반환타입을 가질 필요가 없다. 그렇기 때문에 ha.handle()에서 ha는 HandlerAdapter인데 @RestController 는 @ResponseBody를 포함하고 있고 @ResponseBody가 적용된 컨트롤러는 handle()의 호출결과가 null이고 applyDefultViewName 메소드에서 mv가 null이라면 렌더링 하지 않고 doDispatch() 메소드가 끝난다.

 

이러한 이유로 어댑터가 필요하다.

 

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
  
  ...
  
  @Override
  public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
      throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
  
    mavContainer.setRequestHandled(true);
    ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
  
    // Try even with null return value. ResponseBodyAdvice could get involved.
    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
  } 
}

   

RequestBodyMethod에서 outputMessage에 JSON 형식의 데이터를 담기에 mv는 예외가 발생하여 try문을 빠져나간다.

 

[ postHandle ]

postHandle 메소드는 스프링 인터셉터에서 제공하는 함수로 request, response, mv를 파라미터로 받는다.

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

mappedHandler = getHandler(processedRequest);

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

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

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

mappedHandler.applyPostHandle(processedRequest, response, mv);

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {

	for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
		HandlerInterceptor interceptor = this.interceptorList.get(i);
		interceptor.postHandle(request, response, this.handler, mv);
	}
}

 

스프링 인터셉터에서 제공하는 postHandle() 을 실행시켜 mv(ModelAndView 타입)을 함께 인자로 넣어 전달한다.

 

[ processDispatchResult ]

processDispatchResult 메소드는 request, response, handler, mv, exception을 파라미터로 받는다.

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

mappedHandler = getHandler(processedRequest);

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

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

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

mappedHandler.applyPostHandle(processedRequest, response, mv);

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

processDispatchResult 메소드를 통해 뷰를 렌더링하여 response를 만든다.

 

processDispatchResult 의 render() 메소드를 보면

render(mv, request, response)

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 를 찾아야 하기 때문에 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;
}

ViewResolver 리스트에서 (우리가 만든 HTML 이나 JSP 파일)을 돌며 뷰를 찾는다.

 

render(mv, request, response)

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);
  
   ...
   
  if (mappedHandler != null) {
	// Exception (if any) is already handled..
	mappedHandler.triggerAfterCompletion(request, response, null);
  }
}

view 에게 model을 전달하여 응답을 만든다. 렌더링 이후 스프링 인터셉터 기능인 afterCompletion() 을 실행시켜 null 자리에 예외문이 들어가게 되면 인터셉터에게 예외가 전달된다.

 

 

 

[ 참고 자료 ]

https://www.ibm.com/docs/ko/was/9.0.5?topic=applications-web-modules 

 

웹 모듈

웹 모듈은 웹 애플리케이션을 나타냅니다. 웹 모듈은 서블릿, JSP(JavaServer Pages) 파일 및 HTML(Hypertext Markup Language) 페이지와 같은 정적 컨텐츠를 단일 배치 가능 단위로 조합하여 작성됩니다. 웹 모

www.ibm.com

https://minho-jang.github.io/development/20/#prehandlerequest-response-of-handlerinterceptor

 

[Spring] 스프링 MVC 흐름도

Spring Web MVC에서 클라이언트의 요청부터 응답까지

minho-jang.github.io

 

'🍃 스프링' 카테고리의 다른 글

[SPRING] 남의 코드 이해하기  (1) 2023.05.07
[SPRING] Filter, Interceptor  (0) 2022.10.06
[SPRING] JPA, ORM  (0) 2022.09.27
[SPRING] JAVA8  (0) 2022.09.25
[SPRING] Maven, Gradle  (0) 2022.09.24