DispatcherServlet 요청 흐름
- Coding/Spring
- 2022. 12. 13.
DispatcherServlet
서블릿 컨테이너의 가장 앞단에서 들어오는 모든 요청을 먼저 받아, 적합한 컨트롤러에 위임해주는 프론트 컨트롤러다.
dispatch
"보내다" 라는 뜻
클라이언트로부터 어떤 요청이 오면, 서블릿 컨테이너가 이 요청을 받게되고, 이 요청을 프론트 컨트롤러인 DispatcherServlet이 받아서 해당 요청을 처리할 컨트롤러(Bean)을 getBean() 메서드로 받아와서 요청에 적합한 컨트롤러의 메서드를 실행시킨다.
Spring : web.xml 을 통한 매핑
web.xml
<servlet>
<servlet-name>Hello_servlet</servlet-name>
<servlet-class>Hello.Hello_servlet</servlet-class> // servlet class
</servlet>
<servlet-mapping>
<servlet-name>Hello_servlet</servlet-name>
<url-pattern>/Hello</url-pattern>
</servlet-mapping>
SpringBoot : 자동 구성
SpringBoot는 아래 spring-boot-starter-web 라이브러리를 제공한다. DispatcherServlet을 자동으로 등록하고 구성하여, 별도로 설정할 필요가 없다.
- 기본적으로 spring-boot-starter-web 은 DispatcherServlet의 URL 패턴을 "/"로 구성한다. 따라서 모든 경로(urlPatterns = "/")에 대해서 매핑한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
DispatcherServlet 요청 처리 흐름
DispatcherServlet은 적합한 컨트롤러와 메서드를 찾아 요청을 위임해야한다.
1) 클라이언트에서 요청이 오면 DispatcherServlet이 해당 요청을 받는다.
2) HandlerMapping을 통해서 요청에 맞는 Controller을 매핑한다.
HandlerMapping의 구현체 중 하나인 RequestMappingHandlerMapping은 @Controller로 작성된 모든 컨트롤러 Bean을 파싱하여, HashMap<Key, Value>(Key : 요청정보, Value : 처리할 대상)을 관리한다.
요청에 매핑되는 컨트롤러와 해당 메서드를 갖는 HandlerMethod 객체를 찾아, 컨트롤러로 요청을 넘겨주기 전에 처리해야하는 인터셉터 등을 포함하기 위해 HandlerMethodExecutionChain으로 감싸서 반환한다.
3) 찾아낸 Controller에게 처리 요청한다.
실제로는 HandlerAdapter를 통해 컨트롤러로 요청을 위임한다.
스프링은 HandlerAdapter라는 어댑터 인터페이스를 통해 컨트롤러의 구현 방식에 상관없이 요청을 위임한다.
4) 컨트롤러는 클라이언트의 요청을 처리한 후, 결과와 결과를 출력할 View의 이름을 리턴한다. (modelAndView)
5) DispatcherServlet이 viewResolver을 통해서 컨트롤러가 보내온 view 이름으로 view를 찾는다.
6) 처리 결과를 view에 송신한다.
7) 처리 결과가 포함된 view를 다시 DispatcherServlet에 송신한다.
8) 서버의 응답을 클라이언트에게 반환한다.
추가로, 다른 그림으로 이해해보자.
내용은 책 '스프링 철저 입문' 중 프런트 컨트롤러 아키텍처 부분을 참고했다.
1) DispatcherServlet 클래스는 클라이언트의 요청을 받는다.
2) DispatcherServlet 클래스는 HandlerMapping 인터페이스의 getHandler() 메서드를 호출해서 요청 처리를 하는 Handler 객체(컨트롤러)를 가져온다.
3) DispatcherServlet 클래스는 HandlerAdapter 인터페이스의 handle() 메서드를 호출해서 handler 객체의 메서드 호출을 의뢰한다.
4) HandlerAdapter 인터페이스 구현 클래스는 Handler 객체에 구현된 메서드를 호출해서 요청 처리를 수행한다.
5) 반환되는 ModelAndView 객체를 받는다.
6)~7) DispatcherServlet 클래스는 ViewResolver 인터페이스의 resolveViewName 메서드를 호출해서 Handler 객체에서 반환된 뷰 이름에 대응하는 View 인터페이스 객체를 가져온다.
8) DispatcherSErvlet 클래스는 VIew 인터페이스의 render 메서드를 호출해서 응답 데이터에 대한 렌더링을 요청한다. View 인터페이스의 구현 클래스는 JSP와 같은 템플릿 엔진을 사용해 렌더링할 데이터를 생성한다.
요청 처리순서 도식화
1) Client에서 요청은 서블릿 컨텍스트(Web Context)에서 Filter을 지난다.
2) Filter을 거친 요청은 Spring Context의 Dispatcher Servlet이 가장 먼저 요청을 받게된다.
3) Dispatcher Servlet의 요청은 Interceptor을 거친다.
4) Interceptor을 거친후 다시 Dispatcher Servlet이 Controller에게 요청을 위임한다.
DispatcherServlet 소스코드
스프링 어플리케이션이 구동될때, DispatcherServlet을 자동으로 등록하고 @RequestMapping 어노테이션이 있는 Handler(컨트롤러)들을 모두 매핑한다.
DispatcherServlet.java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
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;
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
...
...
...
}
1) getHandler() 메서드를 통해 매핑한다.
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
Iterator var2 = this.handlerMappings.iterator();
while(var2.hasNext()) {
HandlerMapping mapping = (HandlerMapping)var2.next();
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
2) getHandlerAdapter() 메서드에서 매핑되는 HandlerAdapter 객체를 찾는다.
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
Iterator var2 = this.handlerAdapters.iterator();
while(var2.hasNext()) {
HandlerAdapter adapter = (HandlerAdapter)var2.next();
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");
}
3) handle() 메서드를 호출하여 실제 Controller의 메서드를 수행한다. 이때 리턴값이 ModelAndView 객체다.
ModelAndView mv = null;
...
ha.handle(processedRequest, response, mappedHandler.getHandler());
4) processDispatchResult() 메서드를 호출하여 ModelAndView 객체를 View 객체로 변환한다.
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
processDispatchResult()
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);
}
}
render()
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) {
// We need to resolve the view name.
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;
}
}
5) 클라이언트로 결과를 반환한다.
view.render(mv.getModelInternal(), request, response);
Reference
https://mozzi-devlog.tistory.com/8
https://kkang-joo.tistory.com/19
https://mangkyu.tistory.com/18
https://recordsoflife.tistory.com/603
https://yejun-the-developer.tistory.com/m/4