드디어 dispatcherServlet의 주요 기능인 dispatch 부분에 대해 공부해보려 한다. 아직 render나 viewResolver, multipart 같은 부분이 남아있는데, 먼저 dispatch 부분을 살펴보고 나머지 코드도 살펴보려 한다.
doService
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
/**
* 1. 요청 로그를 남기는 메서드 호출
*/
this.logRequest(request);
/**
* 요청 속성을 백업 - jsp 관련 코드
*/
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap();
Enumeration<?> attrNames = request.getAttributeNames();
label116:
while(true) {
String attrName;
do {
if (!attrNames.hasMoreElements()) {
break label116;
}
attrName = (String)attrNames.nextElement();
} while(!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet"));
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
/*
* 요청 처리에 필요한 wac, localeResolver, themeResolver 등을 초기화
*/
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response); // redirect 후 값을 유지하기 위해 flashmap update
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 {
this.doDispatch(request, response); // 실제 요청 처리 작업 수행
} finally {
/**
* 요청 속성을 복원하기 위해 사용
* include 요청이 발생하면 요청의 속성이 변경될 수 있기 때문에, 원래의 요청 상태를 유지해야 하는 상황에서는 이러한 속성 복원 작업이 필요
* 요청이 서블릿 간에 include 방식으로 호출될 경우, HttpServletRequest에 포함된 속성들이 다른 서블릿에 의해 추가, 수정, 삭제될 수 있음
* 비동기 처리가 시작된 경우, 요청 처리가 별도의 비동기 스레드에서 이루어지기 때문에 속성 복원이 필요하지 않음
* 비동기 요청에서는 스냅샷을 복원하는 대신 비동기 핸들러가 끝난 후 별도의 후처리를 수행
* attributesSnapshot은 include 요청이 발생하기 전의 속성 상태를 저장해 둔 것이므로, 이를 통해 속성을 복원
*/
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
/**
* include 요청이 종료된 후 요청 속성들을 백업된 상태로 복원하는 역할
* include 요청으로 인해 추가된 속성이 있는 경우, 이러한 속성들을 삭제하여 원래 요청의 상태를 복원
*/
this.restoreAttributesAfterInclude(request, attributesSnapshot);
}
if (this.parseRequestPath) { // 요청 경로 파싱이 설정되어있다면
ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); // 이전 경로 상태로 복원
}
}
}
doService는 모든 요청이 처음 들어올 때 걸리는 메서드이다. doService에서 요청에 대한 전처리를 수행하고 doDispatch를 호출하여 실질적인 요청 처리에 들어가게 된다.
doService의 주요 기능은 다음과 같다.
1. 요청 로깅을 남김
2. 요청 속성을 설정
3. doDispatch를 호출하여 실제 요청 처리
4. 요청 후처리
doService에는 jsp 관련 코드들이 많이 들어가 있는데 리다이렉트 등으로 include 요청이 들어왔을 때, 요청의 속성값들을 저장했다가 요청 처리가 끝나면서 다시 변경된 속성들을 원래 요청 상태로 만드는 코드를 포함하고 있다.
이 때, 비동기 처리가 된 코드에 대해서는 이러한 복원 작업을 수행하지 않는데 이 부분에 대해서는 추후에 비동기에 대해 더 공부하면서 살펴봐야할 것 같다.
이외에도 wac, resolver, flashMap 등을 request에 초기화시켜 이후에 값을 사용할 수 있도록 전처리하는 과정을 수행한다.
Include 요청
* include 요청은 포워딩 요청과 다르게 현재 응답을 종료하지 않고, 다른 서블릿의 출력을 현재 서블릿의 출력에 포함
* 주로 JSP에서 다른 JSP 페이지나 서블릿을 포함할 때 사용됨
* 요청 속성이 include될 때는 javax.servlet.include.* 속성들이 설정되며, 이는 일반적인 요청과 구분하기 위해 사용됨
* ex) jsp:include
요청 파싱
* 요청 URL 해석: https://example.com/api/users/123와 같은 URL이 있을 때, api/users/123 부분을 추출하여 논리적 경로로 해석
* 경로 변수 추출: @PathVariable로 정의된 users/{userId}와 같은 경로에서 userId 값을 추출하여, 핸들러 메서드에 주입하도록 준비
* 쿼리 파라미터 해석: 요청 URL의 ? 뒤에 붙는 쿼리 파라미터들을 추출하여, 이를 파라미터로 전달할 수 있도록 해석
parseAndCache(request)의 역할
- 요청 경로를 파싱하여, ServletRequestPathUtils.PATH_ATTRIBUTE에 캐싱, 이후에 요청 경로를 다시 해석할 필요 없이, 빠르게 요청 경로를 참조할 수 있게 한다
- parseAndCache() 메서드를 사용하여 경로를 파싱한 후, 요청 처리 과정에서 효율적으로 경로 정보를 사용할 수 있도록 준비
doDispatch
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
/**
* 초기화 과정
*/
HttpServletRequest processedRequest = request; // 요청을 처리할 Request 객체 초기화
HandlerExecutionChain mappedHandler = null; // handler와 interceptor를 담을 객체 선언
boolean multipartRequestParsed = false; // multipart 요청이 파싱되었는지 여부를 확인하기 위한 플래그
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); // 4. 요청의 비동기 처리 매니저를 가져옴
try {
try {
ModelAndView mv = null; // 핸들러 호출 후 반환될 ModelAndView 객체 초기화
Exception dispatchException = null; // 요청 처리 중 발생한 예외를 담을 변수 초기화
try {
/**
* multipart 요청 처리
*/
processedRequest = this.checkMultipart(request); // 멀티파트 요청인지 확인하고 처리
multipartRequestParsed = processedRequest != request; // 멀티파트 요청이 파싱되었는지 플래그 설정
/**
* 요청에 맞는 handler와 interceptor chain 찾기
*/
mappedHandler = this.getHandler(processedRequest); // 요청에 맞는 핸들러와 인터셉터 체인 찾기
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response); // 핸들러가 없을 경우 404 에러 처리
return;
}
/**
* 핸들러를 호출할 적절한 핸들러 어댑터 찾기
*/
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); // 13.
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method); // 요청이 GET 메서드인지 확인
if (isGet || HttpMethod.HEAD.matches(method)) { // 요청이 GET 또는 HEAD 메서드인 경우
long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); // 마지막 수정 시각 확인
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
// 요청이 변경되지 않은 경우(Not Modified) 304 상태 반환
return;
}
}
/**
* 인터셉터의 preHandle 메서드 실행
*/
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
/**
* handler 메서드를 호출하고, ModelAndView를 반환
* 이 때, 비동기 처리가 시작되었다면 요청 처리를 종료하고 반환
*/
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv); // 뷰 이름이 없는 경우 기본 뷰 이름 설정
/**
* interceptor의 postHanlde 호출
*/
mappedHandler.applyPostHandle(processedRequest, response, mv);
/**
* 예외를 저장하여 이후 처리할 수 있도록 설정
* 예외를 ServletException으로 래핑
*/
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new ServletException("Handler dispatch failed: " + var21, var21);
}
/**
* 처리 결과를 최종적으로 렌더링하고 예외 처리
*/
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
triggerAfterCompletion(processedRequest, response, mappedHandler, var22); // 예외 발생 시 afterCompletion 호출
} catch (Throwable var23) {
triggerAfterCompletion(processedRequest, response, mappedHandler, new ServletException("Handler processing failed: " + var23, var23));
// Throwable 발생 시 예외를 래핑하여 afterCompletion 호출
}
} finally {
/**
* 비동기 처리가 시작된 경우
* 인터셉터의 afterConcurrentHandlingStarted 호출
* 멀티파트 파싱 여부를 비동기 관리 매니저에 설정
*/
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
asyncManager.setMultipartRequestParsed(multipartRequestParsed);
/**
* 멀티파트 요청이 파싱되었거나, 비동기 처리 중 멀티파트 요청이 파싱된 경우
* 멀티파트 요청 처리가 끝났으므로 멀티파트 리소스를 정리
*/
} else if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
this.cleanupMultipart(processedRequest);
}
}
}
dispatcherServlet의 주요 로직이다. 인터넷에 검색하면 나오는 dispatcherServlet의 동작 코드는 위 코드에 해당하는 코드를 나타낸다.
이미지 검색을 하면 수 많은 그림 설명들이 나온다.
SpringMVC의 중앙 제어 역할을 하는 DispatcherServlet의 주요 메서드인만큼 하는 일이 생각보다 많다.
dispatcherServlet의 주 역할은 다음과 같다.
1. handler 찾기
2. handler adapter 찾기
3. handler interceptor 실행 및 요청 처리
4. ModelAndView 후처리 및 예외 처리
doDispatch는 요청에 해당하는 handler를 찾아 호출하고, interceptor를 실행하며, 요청을 처리하는 역할을 한다.
요청 처리 도중에 예외가 발생하면 이를 processDispatchResult()에서 처리할 수 있게 해준다
내가 생각했을 때, dispatcherServlet의 구조나 동작으로 볼 때 아마도 Mediator 패턴으로 볼 수 있지 않을까 싶다. 그러다보니 중간에서 HandlerMapping이나 HandlerAdapter, ViewResolver ... 다른 많은 요소들의 중간 중개자 역할을 하다보니 코드가 늘어난 것이란 생각이 든다.
processDispatchResult
processDispatchResult는 요청이 정상적으로 처리되었는지 또는 예외가 발생했는지에 따라 응답을 렌더링하고, 예외를 처리하며, 인터셉터의 afterCompletion 메서드를 호출하는 역할을 한다.
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
boolean errorView = false; // 에러가 발생하여 에러 뷰를 렌더링하는지 여부를 저장하는 플래그 변수 초기화
/**
* 예외가 발생한 경우 예외를 처리하기 위한 로직 수행
*/
if (exception != null) {
// 예외가 ModelAndViewDefiningException인 경우
if (exception instanceof ModelAndViewDefiningException) { //
ModelAndViewDefiningException mavDefiningException = (ModelAndViewDefiningException)exception; // 예외를 ModelAndViewDefiningException으로 캐스팅
this.logger.debug("ModelAndViewDefiningException encountered", exception);
mv = mavDefiningException.getModelAndView(); // 6. 예외에서 ModelAndView 객체를 가져와 mv에 할당
// 일반 예외인 경우
} else {
Object handler = mappedHandler != null ? mappedHandler.getHandler() : null; // 현재 요청을 처리하는 핸들러가 있는지 확인하여 핸들러 객체 가져옴
mv = this.processHandlerException(request, response, handler, exception); // 예외를 처리하고, 예외 처리 결과로 ModelAndView를 생성
errorView = mv != null; // 예외 처리 후 ModelAndView가 null이 아닌 경우 errorView를 true로 설정
}
}
if (mv != null && !mv.wasCleared()) { // ModelAndView가 존재하고, ModelAndView가 클리어되지 않은 경우에만 뷰를 렌더링
this.render(mv, request, response); // ModelAndView를 기반으로 응답을 렌더링
if (errorView) { // 에러 뷰를 렌더링한 경우
WebUtils.clearErrorRequestAttributes(request); // 요청의 에러 관련 속성들을 정리하여 요청을 클리어
}
} else if (this.logger.isTraceEnabled()) {
this.logger.trace("No view rendering, null ModelAndView returned.");
}
/**
* 비동기 처리가 시작되지 않았고, handler가 존재하는 경우
* 인터셉터의 afterCompletion 메서드를 호출하여 요청 후처리 작업 수행
*/
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
}
}
}
processDispatchResult의 주요 역할은 다음과 같다.
1. 예외 처리
2. 응답 렌더링
3. interceptor의 afterCompletion 호출
요청 처리가 완료된 후, handler의 응답을 기반으로 응답을 생성하고, 예외가 발생한 경우 예외를 처리하는 역할을 한다.
'Web > Spring' 카테고리의 다른 글
[Spring Web 6.1] DispatcherServlet 뜯어보기 - Multipart & ExceptionHandling (1) | 2024.10.07 |
---|---|
[Spring Web 6.1] DispatcherServlet 뜯어보기 - view 관련 (0) | 2024.10.07 |
[Spring Web 6.1] DispatcherServlet 뜯어보기 - getter & setter (0) | 2024.10.04 |
[Spring Web 6.1] DispatcherServlet 뜯어보기 - Constructor & init (0) | 2024.10.04 |
[Spring Web 6.1] DispatcherServlet 뜯어보기 - Deprecated & Static (0) | 2024.10.04 |