DispatcherServlet?
Spring 공식문서에 따르면 DispatcherServlet 정의는 다음과 같다.
Spring MVC, as many other web frameworks, is designed around the front controller pattern where a central Servlet, the DispatcherServlet, provides a shared algorithm for request processing, while actual work is performed by configurable delegate components. This model is flexible and supports diverse workflows.
The DispatcherServlet, as any Servlet, needs to be declared and mapped according to the Servlet specification by using Java configuration or in web.xml. In turn, the DispatcherServlet uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, and more.
요약하자면,
- Spring MVC는 중앙 Servlet인 DispatcherServlet이 front controller로 설계되었다.
- 실제 작업은 구성 가능한 위임 구성 요소에 의해 수행된다.
- DispatcherServlet은 Java configuration 또는 web.xml을 사용하여 선언하고 매핑해야 한다.
- 이 경우 DispatcherServlet은 Spring configuration을 사용하여 요청 매핑, view 확인, 예외 처리 등에 필요한 위임 구성 요소를 검색한다.
즉, Client의 요청에 대해 실제 처리하는 method를 호출해주거나 하는 front Controller의 역할을 DispatcherServlet이 한다고 볼 수 있다.
DispatcherServlet 동작 과정
출처:https://hirlawldo.tistory.com/48
DispatcherServlet의 상속 관계는 위와 같다.
DispatcherServlet은 결국 HttpServlet을 상속받고 있는 것을 알 수 있다. 따라서 기존 Servlet의 life-cycle과 비슷하다는 것을 유추할 수 있다. Servlet의 life-cycle에서의 service(), doGet(), doPost()을 DispatcherServlet이 구현하여 아래와 같이 동작한다는 것이다.
출처:https://yeoooo.github.io/spring/day22\_mvc/
전체적인 DispatcherServlet 동작 과정을 살펴보자.
출처:https://yeoooo.github.io/spring/day22\_mvc/
- Client의 Request가 들어오면 DispatcherServlet은 Request를 확인한다.
- DispatcherServlet은 HandlerMapping에게 Request를 처리할 수 있는 Handler(Controller)가 있는지 확인한다.
- HandlerMapping에서 확인한 Handler를 수행하여 Business Logic을 처리하고, view name 정보를 가지고 있는 ModelAndView 객체를 반환한다.
- view 이름을 ViewResolver에 전달하여 view 객체를 조회한다.
- DispatcherServlet이 view와 model(데이터)를 Response로 만들어 Client에게 응답한다.
코드 분석
FrameworkServlet 코드 중 Servlet mothod인 service이다.
private static final Set<String> HTTP_SERVLET_METHODS =
Set.of("DELETE", "HEAD", "GET", "OPTIONS", "POST", "PUT", "TRACE");
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if (HTTP_SERVLET_METHODS.contains(request.getMethod())) {
super.service(request, response);
}
else {
processRequest(request, response);
}
}
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
...
Client로부터 Request가 오게되면, service 메서드가 실행되고 super.service가 실행된다. 여기서 super는 HttpServlet으로, 다음 HttpServlet의 service가 실행된다.
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
}
...
}
“GET”방식으로 요청 시 doGet 메소드가 실행된다. (lastModified와 비교하여 304 redirect를 하는 것 캐시 처리 또한 볼 수 있다.)
이 때 실행되는 doGet 메소드는 FrameworkServlet에 Override한 doGet 메소드이고, processRequest가 실행된다.
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
...
try {
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
...
}
protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
throws Exception;
processRequest는 doService 메소드를 호출하지만 추상메소드로 상속받는 DispatcherServlet에게 위임하고 있다.
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);
// Keep a snapshot of the request attributes in case of an include,
// to be able to restore the original attributes after the include.
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
// Make framework objects available to handlers and view objects.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
RequestPath previousRequestPath = null;
if (this.parseRequestPath) {
previousRequestPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
ServletRequestPathUtils.parseAndCache(request);
}
try {
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
if (this.parseRequestPath) {
ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
}
}
}
Dispatcher의 doService 소스 코드이다. WebApplicationContext, localeResolver, themeResolver, themeSource를 바인딩하고, 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 {
// 들어온 요청에 대해서 사용할 수 있는 multipart resolver를 찾고 binding한다.
// 요청이 실제로 multipart 요청이라면 multipart 요청을 처리한다.
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
// 현재 요청에 알맞은 핸들러를 가져온다.
// 만약 찾았다면 rendering을 위한 handler와 interceptor가 실행된다.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// Handler를 찾은 것을 실제로 실행시켜줄 Adapter에 매핑시킨다.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
// resource cache를 체크한다.
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;
}
// view를 찾고 model을 매핑한다.
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 ServletException("Handler dispatch failed: " + err, err);
}
// rendering을 진행하고 결과값을 response에 넣는다.
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new ServletException("Handler processing failed: " + err, 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);
}
}
}
}
doDispatch에서 multipart 요청을 처리한 뒤 첫 번째로 실행되는 getHandler 메소드는 요청된 URL에 매핑되는 알맞은 Handler(Controller)를 가져오기 위한 HandlerMapping 인터페이스의 getHandler 메소드이다.
public interface HandlerMapping {
/**
* Return a handler and any interceptors for this request. The choice may be made
* on request URL, session state, or any factor the implementing class chooses.
* ...
*/
@Nullable
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
다음으로 Handler를 HandlerAdapter에 매핑시킨다.
public interface HandlerAdapter {
/**
* Given a handler instance, return whether this {@code HandlerAdapter}
* can support it. Typical HandlerAdapters will base the decision on the handler
* type. HandlerAdapters will usually only support one handler type each.
* ...
* @param handler the handler object to check
* @return whether this object can use the given handler
*/
boolean supports(Object handler);
/**
* Use the given handler to handle this request.
* ...
*/
@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}
supports는 해당 핸들러 인스턴스를 이 핸들러 어댑터가 지원할 수 있는지 여부를 반환한다.
핸들러 어댑터로 handle 메소드를 실행하면 실제 Handler(Controller)가 실행된다.
마지막으로 핸들러 실행 결과를 적절한 response형태로 처리하는 processDispatchResult를 살펴보자.
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
...
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
...
}
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
...
View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need to resolve the view name.
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
}
...
try {
if (mv.getStatus() != null) {
request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
}
...
}
processDispatchResult 내부에서는 render 메소드를 호출한다.
render는 ViewResolver의 resolveViewName로 view 이름에 해당하는 view 객체를 조회한다.
후에 view.render로 렌더링을 하여 응답하게된다.
ViewResolver 소스코드를 살펴보자.
public interface ViewResolver {
/**
* Resolve the given view by name.
* ...
*/
@Nullable
View resolveViewName(String viewName, Locale locale) throws Exception;
}
ViewResolver는 반환된 view 이름을 실제 view 객체로 변환하는 역할을 한다. 이 때 view의 이름은 앞서 봤던 핸들러 어댑터의 handle 메소드 반환타입인 ModelAndView 객체의 정보를 통해 찾을 수 있다. (ModelAndView는 컨트롤러에서 처리한 데이터와 해당 데이터를 표시할 view의 정보를 가지고 있음)
public class ModelAndView {
/** View instance or view name String. */
@Nullable
private Object view;
/** Model Map. */
@Nullable
private ModelMap model;
DispatcherServlet은 HandlerMapping, HandlerAdapter, ViewResolver 이 세가지 요소를 리스트로 가지고 있다. 요청이 들어오면 해당 요청을 처리할 수 있는 구현체들을 선택하여 수행을 하게 된다.
@Nullable
private List<HandlerMapping> handlerMappings;
/** List of HandlerAdapters used by this servlet. */
@Nullable
private List<HandlerAdapter> handlerAdapters;
/** List of ViewResolvers used by this servlet. */
@Nullable
private List<ViewResolver> viewResolvers;
참고
https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet/themeresolver.html
https://sabarada.tistory.com/16#google_vignette
https://hirlawldo.tistory.com/48
https://yeoooo.github.io/spring/day22_mvc/
https://jess-m.tistory.com/15
'Spring' 카테고리의 다른 글
Spring Cloud Config URL 조회 시 보안 (0) | 2023.11.04 |
---|---|
Embedded Redis로 테스트 환경 구축하기 (0) | 2023.10.30 |
Spring Cloud Config Watch (1) | 2023.10.11 |
Spring Gateway 에러 핸들링 (0) | 2023.09.19 |
MSA에서 로그인 되어 있는 회원 정보 조회 (0) | 2023.09.18 |