개발자가 비지니스 로직을 Servlet으로 정의하고 Servlet Container인 Tomcat을 통해 서비스를 제공할 수 있다는 것을 알았습니다. 하지만 아직 스프링 프레임워크의 @Controller
속 메서드가 어떻게 호출되는지 모르니 스프링 프레임워크가 마법같이 남아있을 수 있습니다.
이번 정리는 Servlet 에서 스프링 프레임워크의 연결점인 DispatchServlet에 대해 자세히 알아보겠습니다.
Spring Framework 핵심을 공부해보면 ApplicationContext
라는 인터페이스가 핵심이며 IoC 컨테이너의 역할 및 개발에 도움되는 여러 유틸리티 기능들을 제공합니다. 그리고 Spring Web MVC는 이런 Core 기술들을 잘 활용해서 편리하고 높은 수준의 웹서비스를 제공할 수 있는 틀을 제공해줍니다.
Servlet 기반의 웹서비스를 제공할 때 진입점이며 가장 중요한 요소가 Spring Web MVC 속 DispatchServlet
class 입니다. 공식 API Reference를 살펴보면 HttpServlet를 상속한 것을 볼 수 있습니다.
우선 늘 그렇듯 좋은 접근은 해당 기술이 왜 생겼는지를 생각해보면 이해에 도움이 됩니다. 우선 Servlet 기반의 웹 서비스의 경우, Servlet Container에서 특정 URL에 알맞는 서비스를 제공하기 위해 web.xml에 url-mapping 설정과 그에 맞는 Servlet을 정의해서 추가해야합니다. 그리고 뷰(JSP)와 모델, 비지니스 로직이 하나의 서블릿에서 관리하면 관심사 분리가 제대로 되지않습니다. 즉, 역할 구분이 정확하지 않아 유지보수가 어려워집니다.
이러한 문제를 해결하는 것이 FrontController패턴으로 하나의 서블릿에서 모든 요청을 받아들이고 각 요청에 알맞은 처리를 하는 handler들에게 dispatch해서 역할을 위임하는 방식입니다. 그러한 역할을 해주는 것이 Spring의 DispatchServlet입니다.
Front Controller 패턴은 들어오는 요청들에 대한 중앙 집중적으로 처리해서 중복되는 코드를 제거할 수 있습니다.
여기까지만 살펴봐도 스프링을 공부하면서 DispatchServlet을 모르는다는 것은 말이 안될 수준입니다.
DispatchServlet에 대해 더 자세히 알아보겠습니다.
This servlet is very flexible: It can be used with just about any workflow, with the installation of the appropriate adapter classes
문서를 살펴보면 위와 같은 말이 있습니다. 적절한 어뎁터 클래스의 설정(?)으로 어떠한 workflow에 대해서도 사용될 수 있다. 매우 유연하다.
HTTP를 통해 할 수 있는 수 많은 처리들(파일업로드, 헤더파일, METHOD별 구분, 웹소켓처리 등등)을 DispatchServlet
이라는 하나의 클래스가 모든 로직을 가지고 있지는 않습니다. 어플리케이션에 따라 처리해야할 방식도 달라집니다. 예를들어 REST API인지 아니면 웹페이지로 응답을 돌려줘야하는지 등등..
각 기능들을 인터페이스로 두고 적절한 구현체로 설정(교체)해주면 손쉽고 유연하게 처리방식을 변경할 수도 있습니다.
DispatchServlet의 동작방식을 살펴보면서 어떤 기능들을 변할 수 있는지 알아보겠습니다.
우선 DispatcherServlet은 서버 어플리케이션의 초기화 단계에서 ApplicationContext의 Bean으로 부터 의존성을 주입받습니다.
특별한 빈들을 찾거나, 혹은 기본 전략에 해당하는 빈들을 등록합니다.
@ResponseBody
가 있다면 Converter를 사용해서 응답을 생성한다. (REST API)위 각 단계에서 처리되는 방식은 앞서말했듯이 유연하게 변경될 수 있습니다.
DispatcherServlet
Class의 doDispatch
메소드에서 넘겨 받은 HttpServlet을 이용해서 처리를 시작합니다.
public class DispatcherServlet extends FrameworkServlet {
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
}
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;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
...
}
this.checkMultipart(request)
내부를 들여다 보겠습니다.
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
...
return this.multipartResolver.resolveMultipart(request);
...
}
핵심은 ApplicationContext를 통해 받은 multipartResolver
를 통해 요청을 처리하게 된다는 것 입니다. 즉,MultipartResolver
를 구현한 어떤 구현체로도 교체가 가능합니다.
private void initMultipartResolver(ApplicationContext context) {
this.multipartResolver = (MultipartResolver)context.getBean("multipartResolver", MultipartResolver.class);
}
위는 ApplicationContext에서 Bean을 Dispatcher에서 얻어오는 초기화 코드입니다.
doDipatch
메서드를 계속해서 살펴보면
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
...
this.getHandler
메서드를 통해 요청을 처리할 Handler를 찾게됩니다.
@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;
}
초기화 단계에서 ApplicationContext 통해 빈으로 얻은 MappingHandler들을 순회하면서 알맞은 Handler를 찾게됩니다.
private void initHandlerMappings(ApplicationContext context) {
...
Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList(matchingBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
...
}
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
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;
}
}
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
실제 Handler를 처리할 Adapter를 얻고 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
를 통해 실제 요청 처리 후 response를 얻게 됩니다.
HandlerAdapter도 마찬가지로 ApplicationContext를 통해 빈 형태로 등록되어있습니다.
default로 등록되는 Adapter들은 아래와 같습니다.
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
org.springframework.web.servlet.function.support.HandlerFunctionAdapter
private List<HandlerExceptionResolver> handlerExceptionResolvers;
마찬가지로 예외를 처리하는 방식도 유연하게 처리가능합니다.