Spring-MVC 읽기 #6. DispatcherServlet - @Controller는 어떻게 실행될까?
Spring-MVC로 웹 애플리케이션을 개발하면 아래와 같은 Controller를 만든다.
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello Spring~";
}
}
어떻게 @Controller 선언 만으로 메소드가 실행되고, Controller 실행 전/후로 전처리, 후처리 가능한 Interceptor가 동작할까? 뿐만 아니라, 파일 업로드, View 반환 등 Spring-MVC에서 제공되는 여러 가지 기능들은 어떻게 실행될까?
Spring-MVC 읽기 #5. AbstractDispatcherServletInitializer와 AbstractAnnotationConfigDispatcherServletInitializer에서 봤던 DispatcherServlet에 의해 동작된다. 이 번 글은 Spring-MVC의 핵심이라 할 수 있는 DispatcherServlet 클래스 코드를 살펴볼 것이다.
DispatcherServlet
DispatcherServlet의 Hierarchy를 보자. 위 이미지에 빨간색 박스를 누르면 DispatcherServlet의 상위 클래스를 볼 수 있다.
Spring-MVC는 Servlet 스펙을 기반으로 만들어진 프레임워크다. 때문에 당연히(?) Servlet interface를 구현하고 있다. 앞선 글에서 WAS를 자세히 설명하지 않았지만, 우리가 만든 웹 애플리케이션을 구동하고 실행하는 건 알고 있다. 그리고 WAS의 종류 중 하나인 Tomcat을 기준으로 설명했고, Tomcat은 Servlet 웹 애플리케이션을 실행한다.
HTTP를 요청하면, Tomcat은 이 Servlet 인터페이스를 실행한다.
Servlet#service
public interface Servlet {
public void init(ServletConfig config) throws ServletException;
public ServletConfig getServletConfig();
/**
* Called by the servlet container to allow the servlet to respond to
* a request.
*
* <p>This method is only called after the servlet's <code>init()</code>
* method has completed successfully.
*
* <p> The status code of the response always should be set for a servlet
* that throws or sends an error.
*
*
* <p>Servlets typically run inside multithreaded servlet containers
* that can handle multiple requests concurrently. Developers must
* be aware to synchronize access to any shared resources such as files,
* network connections, and as well as the servlet's class and instance
* variables.
* More information on multithreaded programming in Java is available in
* <a href="http://java.sun.com/Series/Tutorial/java/threads/multithreaded.html">
* the Java tutorial on multi-threaded programming</a>.
*
*
* @param req the <code>ServletRequest</code> object that contains
* the client's request
*
* @param res the <code>ServletResponse</code> object that contains
* the servlet's response
*
* @exception ServletException if an exception occurs that interferes
* with the servlet's normal operation
*
* @exception IOException if an input or output exception occurs
*
*/
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
public String getServletInfo();
public void destroy();
}
Servlet 인터페이스는 몇 가지 선언 메소드가 있다. Servlet#service
메소드의 주석을 보면 Servlet 컨테이너에 의해 호출되어 서블릿이 요청에 응답할 수 있게 합니다.
라고 설명한다. 여기서 요청
과 응답
이 무엇을 의미할까? 바로 HTTP request, response를 의미한다. Servlet#service 메소드의 파라미터인 ServletRequest와 ServletResponse를 봐도 Servlet#service 메소드가 어떤 의미를 하는지 추측할 수 있다.
GenericServlet#service
public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable {
public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
}
Servlet 인터페이스의 구현체인 GenericServlet 클래스의 service
메소드는 abstract로 선언되어있다. 즉 GenericServlet의 하위 클래스가 구현하고 있다는 의미다.
HttpServlet#service
HttpServlet 클래스는 오버 로딩된 두 개의 service 메소드가 존재한다. ServletRequest와 ServletResponse
를 파라미터로 받는 service 메소드를 보자.
HttpServlet.service(ServletRequest, ServletResponse)
public abstract class HttpServlet extends GenericServlet {
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
if (!(req instanceof HttpServletRequest &&
res instanceof HttpServletResponse)) {
throw new ServletException("non-HTTP request or response");
}
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
service(request, response);
}
}
- ServletRequest를 HttpServletRequest로 ServletResponse를 HttpServletResponse로 형 변환한다.
- HttpServlet.service(HttpServletRequest, HttpServletResponse) 메소드를 호출한다.
HttpServlet.service(HttpServletRequest, HttpServletResponse)
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 = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
// 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);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
- HTTP method를 구한다. (req.getMethod() 메소드)
- HTTP method에 맞게
do{method}
메소드(doGet, doPost 등)를 호출한다.
HttpServlet#doGet 메소드를 기준으로 코드를 살펴보자.
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
}
- HttpServlet#doGet 메소드는 protocol을 구해 HttpServletResponse#sendError 메소드를 호출한다.
이상하다. 이게 끝일까? 단순히 sendError 메소드를 호출하고 doGet 메소드는 종료된다.
org.springframework.web.servlet.FrameworkServlet#doGet
HttpServlet 클래스의 하위 클래스인 FrameworkServlet 클래스도 doGet
메소드와 같은 HTTP method를 처리할 수 있는 메소드가 존재한다.
HttpServlet#service 메소드는 HttpServlet#doGet 메소드를 호출하는 게 아니라, FrameworkServlet#doGet 메소드를 호출한다.
FrameworkServlet 클래스에서 한 가지 주의 깊게 볼 것이 있다. FrameworkServlet부터는 Spring-MVC에서 구현한 클래스다. FrameworkServlet은 org.springframework.web.servlet
패키지에 존재한다. 이 클래스부터 하위 클래스는 Spring의 구현체다.
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
}
- FrameworkServlet#processRequest 메소드를 호출한다.
FrameworkServlet#processRequest
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
initContextHolders(request, localeContext, requestAttributes);
try {
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
- 현재 HTTP request의 Locale을 구한다. (LocaleContextHolder.getLocaleContext() 메소드)
- HTTP request의 속성(session, request, response)을 담고 있는 ServletRequestAttributes 클래스를 생성한다.
- 현재 요청된 정보(request, locale, requuest 속성 등)를 ThreadLocal에 담는다. (initContextHolders(request, localeContext, requestAttributes) 메소드)
- FrameworkServlet#doService 메소드를 호출한다.
FrameworkServlet#doService
protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception;
- FrameworkServlet#doService 메소드는 abstract 메소드다.
DispatcherServlet#doService
FrameworkServlet의 하위 클래스인 DispatcherServlet이 doService
메소드를 구현하고 있다.
public class DispatcherServlet extends FrameworkServlet {
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);
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));
}
}
}
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);
}
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);
}
}
}
}
}
- HTTP 요청 log를 찍는다. (logRequest(request) 메소드)
- attributesSnapshot에 HTTP reqeust 속성을 보관(snapshot)한다.
WebApplicationContext, LocaleResolver, ThemeResolver, ThemeSource를 HTTP reuqest 속성에 담는다.
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());
FlashMap에 속성을 저장한다. (this.flashMapManager.retrieveAndUpdate(request, response) 메소드)
- FlashMap은 HTTP 요청, 응답 생명주기에도 살아있고, View가 렌더링 된 후에 삭제된다.
DispatcherServlet#doDispatch(request, response) 메소드 호출
DispatcherServlet#doDispatcher
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 = "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;
}
// 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);
}
}
}
}
DispatcherServlet은 여러 가지 Resolver와 클래스를 이용해 요청을 처리한다. 때문에 이 글에서 모두 설명하기는 어렵고 대략적인 것만 설명할 예정이다. 자세한 설명은 다음 글에서 여러 가지 Resolver를 하나씩 살펴볼 예정이다.
Multipart
요청 인지 체크한다. (DispatcherServlet#checkMultipart(request) 메소드)HTTP 요청에 해당하는
HandlerExecutionChain을 구한다. (DispatcherServlet#getHandler(processedRequest) 메소드)- URL을 기준으로 HandlerMapping을 찾는다.
- HandlerExecutionChain은 HandlerMapping에 의해 생성된다.
- HandlerExecutionChain은 Interceptor List를 포함하고 있다.
- HandlerExecutionChain이 null이면
HTTP Status 404를 response
한다. (DispatcherServlet#noHandlerFound(processedRequest, response) 메소드)- 개발하며 자주 보게 되는 404 not found error를 이 메소드에서 실행한다.
- Controller를 실행하기 위해 HandlerAdapter를 구한다.(DispatcherServlet#getHandlerAdapter(mappedHandler.getHandler()) 메소드)
- HandlerAdapter는 우리가 작성한 Controller를 실행하는 역할을 한다.
- HandlerMapping은 URL을 기준으로 어떤 Handler로 매핑할지 결정한다면, HandlerAdapter는 결정된 Handler를 실행한다.
- HandlerInterceptor로 전처리(Interceptor#preHandle)를 실행한다. (mappedHandler.applyPreHandle(processedRequest, response) 메소드)
- HandlerAdapter#handle 메소드를 호출해 실제 로직(Controller)을 실행한 후 ModelAndView를 생성한다. (mv = ha.handle(processedRequest, response, mappedHandler.getHandler()) 메소드)
- Interceptor로 후처리(Interceptor#postHandle)를 실행한다. (mappedHandler.applyPostHandle(processedRequest, response, mv) 메소드)
- View를 렌더링 하거나, Exception을 핸들링한다. (processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException) 메소드)
- View 렌더링은 ViewResolver가 담당한다.
- Exception 핸들링은 HandlerExceptionResolver가 담당한다.
DispatcherServlet#doDispatcher에서 알아야 할 것.
- Multipart (파일 업로드)를 처리하는 MultipartResolver
- Controller 실행에 필요한 HandlerMapping, HandlerAdapter 그리고 ModelAndView
- View 렌더링 또는 Exception 핸들링에 필요한 ViewResolver와 HandlerExceptionResolver
책 추천
이 글에서 설명한 DispatcherServlet에 대한 자세한 설명을 다룬 책이 있다. 스프링 MVC 프로그래밍
출처 : https://blog.woniper.net/369?category=699184
'Spring Framework > Spring boot' 카테고리의 다른 글
스프링부트로 이메일보내기(비밀번호 찾기 / 회원가입 이메일 인증) (0) | 2022.05.24 |
---|---|
Spring-MVC 읽기 #7. HandlerMapping (0) | 2020.09.04 |
Spring-MVC 읽기 #5. AbstractDispatcherServletInitializer와 AbstractAnnotationConfigDispatcherServletInitializer (0) | 2020.09.04 |
Spring-MVC 읽기 #4. AbstractContextLoaderInitializer (0) | 2020.09.04 |
Spring-MVC 읽기 #3. Spring-MVC의 시작 (0) | 2020.09.04 |
Spring-MVC 읽기 #2. 빌드 (0) | 2020.09.04 |
Spring-MVC 읽기 #1. 나는 왜 오픈소스를 읽을까? (0) | 2020.09.04 |
실행 중인 Spring Boot pid 파일 생성 (0) | 2020.09.04 |