벌써 새해가 밝았다. 1년이 후딱 지나갔다. 벌써 2017년이라니..
오늘은 새해들어서 첫 포스팅은 한다. 어느덧 포스팅 개수도 200개가 넘었고 블로그를 시작한지도 1년이 다 되어간다. 작년 2월말부터 시작했으니 담달이면 1년이 된다. 생각보다 포스팅을 많이 한 것같다.

오늘은 Spring mvc에 대해서 살펴보도록 하자. mvc를 한다고해서 아주 많은 부분을 하는 건 아니고 핵심적인 부분이나 중요한 부분정도면 간단하게 살펴보자.

Spring mvc의 핵심적인 클래스는 뭘까? 뭐 필자와 다르게 생각할 수도 있겠지만 필자는 DispatcherServlet이 Spring mvc의 핵심적인 클래스라 생각한다. Spring mvc의 DispatcherServlet만 잘 알아도 쉽게 개발 할 수 있을 듯 하다.
그래서 오늘은 DispatcherServlet에 대해서 살펴 보도록 하자. 모든 것을 살펴보기엔 양이 너무 많고 중요한부분 또는 자주 사용하는 부분위주로 살펴보도록 하자. 문서의 그림을 잘 보도록하자. 이와 같은 구조로 Spring mvc는 동작하고 있다. 그림에서 Front ControllerDispatcherServlet에 해당한다.

보통은 Servlet을 등록할 때는 web.xml을 사용해서 등록한다. 물론 DispatcherServlet도 마찬가지로 web.xml에 등록해야 한다. 그래서 엄밀히 따지면 DispatcherServlet은 그자체로 스프링 컨텍스트에 빈으로 등록되지는 않는다. 그럼 어떻게 빈으로 등록한 여러 클래스를 사용하는 것일까? 그건 바로 등록 된 ApplicationContext를 직접사용해서 해당 빈들을 가져온다.

...
HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);
...

위는 DispatcherServlet의 일부분이다.

참고로 Spring boot의 경우에는 DispatcherServlet 빈으로 등록된다.

DispatcherServlet 에서 컨트롤러로 위임

자바의 서블릿 컨테이너는 HTTP 프로토콜을 통해 들어오는 모든 요청 (URL 패턴에 맞게) DispatcherServlet에 전달한다. 전달 받은 DispatcherServlet은 어떻게 해당 Controller에 작업을 전달할까?
DispatcherServlet은 URL, HTTP 메서드 및 정보, 파라미터 등을 참고해서 해당 컨트롤러에게 작업을 위임할지 결정한다. 해당 컨트롤러를 선정하는 것은 DispatcherServlet의 핸들러 매핑 전략을 사용한다. 실질적은 인터페이스는 HandlerMapping라는 인터페이스가 담당하고 있다. 이 인터페이스는 해당 컨트롤러에 대한 정보들을 가지고 있다. 해당하는 컨트롤러의 클래스, 메서드, 리턴타입, 파라미터 타입등 결정된 컨트롤러의 모든 정보들을 가지고 있다. 컨트롤러가 결정 되었다면 실제로 해당되는 컨트롤러의 메서드를 호출해야 되는데 컨트롤러 호출 방법은 타입에 따라 다르기 때문에 컨트롤러를 결정했다고 해도 DispatcherServlet은 알 길이 없다. 그래서 컨트롤러 타입을 지원하는 HandlerAdapter라는 인터페이스가 존재 한다. 이 인터페이스는 적합한 파라미터를 추출해 해당 컨트롤러의 메서드를 호출해 주는 역할을 한다.

HandlerMapping

위에서도 말했지만 요청 정보를 기준으로 어떤 컨트롤러를 사용할 것인가를 결정하는 인터페이스이다. DispatcherServlet에서는 한개 이상의 HandlerMapping을 가질수가 있다.

private List<HandlerMapping> handlerMappings;

DispatcherServlet 클래스의 일부분이다.

꽤 많은 HandlerMapping 인터페이스 구현체가 있으니 참고하고 기본적으로 흔히 사용하는 @Controller@RequestMapping 어노테이션을 통해 결정되는 컨트롤러의 경우에는 RequestMappingHandlerMapping 구현체의 의해 핸들러가 결정된다. 거기에 대응되는 Adapter로는 RequestMappingHandlerAdapter 클래스에 의해 컨트롤러의 메서드가 호출된다.

참고로 3.1 이전에는 DefaultAnnotationHandlerMapping 클래스로 결정되었으며 그에 대응되는 AnnotationMethodHandlerAdapter 에 의해 호출이 일어났다. 현재는 Deprecated 처리 되어 있다.

HandlerAdapter

자주 사용하는 구현체로는 위에서 잠깐 언급한 RequestMappingHandlerAdapter 클래스이다. 어댑터 또한 한개 이상을 가질수 있다.
RequestMappingHandlerMapping과 대응되는 클래스이므로 두 클래스가 쌍으로 존재해야 한다. 하지만 다른 핸들러 매핑과 어댑터는 서로 관련 있어도 되고 없어도 된다.

여기서는 컨트롤러의 타입을 결정해야 하므로 파라미터를 알아야 한다. 그래서 해당 어댑터(RequestMappingHandlerAdapter)에서는 HandlerMethodArgumentResolver 인터페이스와 HandlerMethodReturnValueHandler 인터페이스를 가지고 있다.

HandlerExceptionResolver

해당 인터페이스는 예외가 발생하였을 때 이를 처리하는 로직을 담당한다. 예외가 발생했을 때 종류에 따라 에러 페이지를 출력 한다거나 로그를 출력 한다거나 기타 등등 처리할 예외 로직을 처리하면 된다.
기본전략 이였던 구현체 중 AnnotationMethodHandlerExceptionResolver 클래스는 Deprecated 되었고 ExceptionHandlerExceptionResolver 클래스로 변경 되었다.

ViewResolver

이 인터페이스는 컨트롤러가 리턴한 뷰 이름을 참고해서 적절하게 뷰 오브젝트를 찾아주는 로직을 담당한다. Spring에서 지원하는 뷰 리졸버는 다양하게 있으니 참고하면 되겠다.

LocaleResolver

이 인터페이스는 지역 정보를 결정해주는 로직을 담당한다. Header, URL 파라미터, 쿠키등 다양한 방식으로 지역정보는 결정할 수 있다.

ThemeResolver

이 인터페이스는 테마를 가지고 이를 변경해서 사이트를 구성할 경우 사용한다. 이건 필자도 잘모르겠다. 사용할 일이 없어서 한번도 사용해보지 않았다.

RequestToViewNameTranslator

이 인터페이스는 컨트롤러에서 뷰 이름이나 오브젝트를 제공해주지 않았을 경우 URL과 같은 요청정보를 참고해서 자동으로 뷰 이름을 생성해주는 전략이다.
구현체로는 DefaultRequestToViewNameTranslator 클래스가 존재한다.

MultipartResolver

이 인터페이스는 파일 처리를 위한 멀티파트 요청을 파싱하는 인터페이스이다.

지금까지 설명한 8개는 모두 인터페이스이다.(FlashMapManager 라고 한개 더 있는데.. 잘 모르기에..) 이렇게 DispatcherServlet은 여러 방면으로 확장해서 사용자가 원하는 개발을 할 수 있도록 많은 도움을 준다. 가장 좋은점은 아주 유연하게 컨트롤러를 호출 할 수 있다. 컨트롤러의 종류의 제약받지 않고 얼마든지 새로운 타입을 추가할 수 있다.

DispatcherServlet 처리 순서

DispatcherServlet의 아주 간단한 동작 방식을 차례대로 살펴보자.

  • 최초에 WebApplicationContext, ViewResolver, ThemeResolver를 다른 곳에서 사용할 수 있는 속성처럼 요청에 바인딩한다.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
  • 멀티파트 인지를 검사를 한다. 만약 멀티파트라면 추가적으로 요청 정보를 MultipartHttpServletRequest로 감싼다.
processedRequest = checkMultipart(request);
  • 적절한 핸들러를 검색한다.
mappedHandler = getHandler(processedRequest);
  • 적절한 어댑터를 검색한다.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
  • 해당 컨트롤러를 호출한다.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
  • 컨트롤러 호출전 에는 전처리가 실행되며 호출 후에는 후처리가 실행 된다.
//전처리
mappedHandler.applyPreHandle(processedRequest, response);

//후처리
mappedHandler.applyPostHandle(processedRequest, response, mv);

이렇게 DispatcherServlet의 확장 포인트와 호출 순서에 대해서 알아봤다. 더 많은 일을 하고 있으니 소스를 보면서 참고하길 바란다.
오늘은 spring mvc의 핵심적인 클래스인 DispatcherServlet에 대해서 알아봤다!