위 그림은 SpringMvc의 아키텍처이다. 전체적인 흐름을 한번 살펴보고 들어가자.
(ArgumentResolver , HttpMessageConverter, Interceptor 에 대한 내용은 차차 다뤄볼 예정이다)
클라이언트와 서버가 통신할 때 요청의 흐름을 생각해보면
클라이언트의 request -> WAS(톰캣) -> Filter -> DispatcherServlet -> Interceptor -> 컨트롤러 정도의 흐름으로 정리될 수 있을 것이다.
이 중 스프링 MVC의 핵심 로직인 DispatcherServlet 라는 녀석은 어떤일을 하고 어떻게 구현되어 있을까?
1. 다양한 종류의 컨트롤러(HandlerMapping)와 어댑터(HandlerMappingAdapter)
스프링은 다양한 종류의 컨트롤러를 지원한다.
1) HttpRequestHandler
2) Servlet
3) Controller
4) Requestmappinghandlermapping (어노테이션 방식의 컨트롤러)
5) ....등등
이러한 다양한 종류의 컨트롤러를 모두 지원하기 위해서는 앞에서 수문장 역할을 하는 녀석이 필요한데 DispatcherServlet이 수문장역할을 하는 녀석이다.
위 코드는 dispatcherServlet 코드의 일부분이다. 위 initStrategies 메서드에서 각종 요소들을 초기화 해주는 작업을 진행하는데, 이 중 initHandlerMappings 라는 메서드를 호출하는 것을 알 수 있다. 사실 우리가 스프링 빈으로 등록하여 사용하는 컨트롤러나 , 스프링 내부적으로 등록되어 있는 컨트롤러등 많은 컨트롤러들이 HandlerMaping 객체로써 initHandlerMappings 라는 메서드를 통해 handlerMappins라는 리스트에 담기게 된다.
이 후, handlerMappings 에 등록되어진 많은 컨트롤러 중 요청을 처리할 수 있는 핸들러 (컨트롤러)를 가져온 뒤 해당 컨트롤러를 실행시킬 수 있는 어댑터를 찾아서 컨트롤러의 로직을 실행한다. 스프링 내부에는 위에서 언급한 다양한 종류의 컨트롤러를 다룰 수 있는 어댑터들이 존재하는데 다음과 같은 어댑터들이 있다.
1) RequestMappingHandlerAdapter
- 우리가 흔히 사용하는 어노테이션 방식의 RequestMappingHandlerMapping 을 다루는 어댑터
2) HandlerFunctionAdapter
3) HttpRequestHandlerAdapter
4) SimpleControllerHandlerAdapter
스프링에서는 아래와 같이 HandlerAdapter들을 초기화 한다.
이후 dispatcherServlet의 doService -> doDispatch 를 통해 요청에 맞는 핸들러를 찾고 (아래 코드의 getHandler()에서) , 해당 핸들러를 다룰 수 있는 어댑터(아래 코드의 getHandlerAdapter() 에서)를 찾고 우리가 구현한 컨트롤러의 로직이 핸들러 어탭터를 통해 실행되는 흐름이다.
참고로 HandlerAdapter 인터페이스를 살펴보면 HandlerAdapter를 구현한 구현체들은 supports(), handle() 메서드를 구현해야한다.
supports() 메서드를 통해 핸들러를 처리할 수 있는 어댑터를 찾는다.
HandlerMapping 인터페이스도 마찬가지로 위 supports() 와 같은 메서드를 통해 다형성을 활용하여 요청을 처리할수 있는 핸들러를 찾는다.
2. View
아래 그림의 handle() 을 보면 ModelAndView 객체를 반환하는 것을 알 수 있다. 이렇게 로직을 처리하고 반환받은 ModelAndView 객체를 통해서 뷰를 렌더링 해야한다.
렌더링하기 전에 applyDefaultViewName() 메서트를 통해 view resource 의 경로를 얻고, ModelAndView 객체에 세팅해준다.
참고로 RequestToViewNameTranslator라는 객체를 통해 resource의 경로를 얻는다.
( RequestToViewNameTranslator도 인터페이스이며,
RequestToViewNameTranslator를 구현한 DefaultRequestToViewNameTranslator 구현체를 사용한다. )
이렇게 경로를 세팅해 준 뒤 비로소 processDispatchResult() 메서드를 통해 아래 코드의 순서로 뷰가 렌더링 된다.
이 때 사용되는 ViewResolver 인터페이스의 구현체들은 아래와 같고, 우리가 사용하는 템플릿엔진에 따라 다른 viewResolver 구현체가 유연하게 끼워져서 사용된다.
마지막으로 다시한번 mvc 아키텍처를 살펴보면서 흐름을 정리하면 아래와 같이 정리할 수 있다.
요청에 따른 핸들러매핑을 찾고
-> 핸들러 매핑을 처리할 수 있는 어댑터를 찾는다
-> 핸들러 어댑터의 handle 함수를 호출하여 우리가 구현한 컨트롤러의 로직을 실행한 뒤 ModelAndView 객체를 반환 받는다
-> resource 경로가 매핑된 ModelAndView 를 viewResolver 에 넘긴다
-> 우리가 사용하는 템플릿엔진에 맞는 viewResolver 구현체를 통해 view가 렌더링된다.
추가로 위 구조를 이해하고 살펴보면 스프링 mvc 프레임워크가 인터페이스와 어댑터 패턴을 활용하여 얼마나 유연하게 설계되었는지 알 수 있다.
사실 위 내용에 더해서 살펴봐야 할 것들이 아직 많이 남았는데 argumentResolver , HttpMessageConverter ... 등 추가적으로 다루어야 할 것들은 이후 포스팅에서 차근차근 정리해보려고 한다.