Spring은 내부적으로 FrontController 패턴과 Adapter 패턴을 적용하고 있다.
Spring이 FrontController 패턴과 Adapter 패턴을 왜 적용하고 있고 어떻게 사용을 하고 있는 지 알아보자.
먼저, 패턴들을 알기 전에 Spring MVC가 어떻게 적용이 되고 있는지 전체적인 구조와 흐름을 알아보도록 하자.

클라이언트로 요창이 들어오면 Dispatcher Servlet이 받고 요청이 어떤 핸들러인지 정보를 조회를 한다.
그리고 핸들러 어댑터 목록에서 핸들러를 처리할 수 있는 어댑터를 찾아서 어댑터에서 핸들러를 실행을 한다.
그리고 그 결과를 Dispatcher Servlet이 viewResolver에 넘겨 주어서 View를 반환한 뒤 View를 랜더링을 해준다.
흐름을 알아 보았으니 Spring MVC에서 중요한 역할을 하는 Dispatcher Servlet에 대해서 코드를 보면서 알아보겠다.
public class DispatcherServlet extends FrameworkServlet {
// ...생략...
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...생략...
mappedHandler = this.getHandler(processedRequest);
// ...생략...
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
// ...생략...
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// ...생략...
this.applyDefaultViewName(processedRequest, mv);
// ...생략...
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
// ...생략...
}
// ...생략...
}
DispatcherServlet는 매우 복잡하다. 여기서는 설명을 위해서 중요한 doDispatcher 부분 만을 뽑아서 설명을 하겠다.
먼저, DispatcherServlet를 보면 FrameworkServlet를 상속을 받는 것을 알 수 있다.
이 FramewrokServlet를 따라 올라가면 HttpServletBean을 상속을 받고 HttpServlet를 상속을 받고 있는 것을 알 수 있다.
즉,
DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet
위와 같이 상속을 받고 있는 것이다. 이는 DispatcherServlet가 HttpServlet을 상속 받고 DispatcherServlet도 Servlet라는 것을 알 수 있다.
그러면 doDispatcher 메소드를 하나씩 살펴보자.
먼저, getHandler 메소드를 통해서 요청을 처리할 핸들러를 가지고 온다.
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;
}
getHandler 메소드의 내부를 살펴보면 handlerMappings는 HandlerMapping 묶은 리스트이다.
이 HandlerMapping이 하는 역할은 요청을 받아서 그 요청을 처리해줄 핸들러를 찾는 것이다.
그래서 이 handlerMappings를 순회를 하면서 요청을 처리할 핸들러를 찾아서 return을 해준다.
그리고 getHandlerAdapter 메소드에 우리가 찾아온 핸들러를 넣어서 이 핸들러를 처리해 줄 adapter를 찾아서 가지고 온다.
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
Iterator var2 = this.handlerAdapters.iterator();
while(var2.hasNext()) {
HandlerAdapter adapter = (HandlerAdapter)var2.next();
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
getHandlerAdapter 메소드의 내부도 살펴보면, 마찬가지로 handlerAdapters라는 handlerAdapter 구현체들의 리스트이고 이 리스트를 순회를 하면서 핸들러를 호출할 수 있는 adapter를 찾는다.
그리고 adapter안에 supports 메소드가 있는데 이 안에 핸들러를 넣어서 호출을 할 수 있는 adapter를 검증을 해서 adapter를 리턴을 해준다.
이 부분이 handler에 맞는 adapter를 찾는 adapter 패턴이 적용된 부분이다.
public interface HandlerAdapter {
boolean supports(Object handler);
@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
/** @deprecated */
@Deprecated
long getLastModified(HttpServletRequest request, Object handler);
}
그리고 handle 메소드를 보면 HandlerAdapter라는 인터페이스에 구현 메소드로 나와 있다.
아까 보았던 supports 메소드 있다.
adapter 구현체를 찾아서 그 구현체가 구현한 handler메소드를 통해서 handler를 호출을 한다.
그리고 handler 메소드는 ModelAndView를 반환을 한다.
private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
if (mv != null && !mv.hasView()) {
String defaultViewName = this.getDefaultViewName(request);
if (defaultViewName != null) {
mv.setViewName(defaultViewName);
}
}
}
다음은 applyDefaultViewName이라는 메소드는 ModelAndView객체 안에 viewName이 설정이 되어 있지 않은 경우 viewName을 설정을 해주는 메소드이다.
이름이 넣어주는 이유는 view를 찾을 때, viewName으로 하기 때문이다.
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
// ...생략...
this.render(mv, request, response);
// ...생략...
}
그리고 View를 랜더링해주는 processDispatchResult를 살펴보면 reder라는 메소드를 통해서 랜더링을 한다. 이 render 메소드도 살펴보자.
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...생략...
String viewName = mv.getViewName();
View view;
// ...생략...
view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);
// ...생략...
view.render(mv.getModelInternal(), request, response);
}
render메소드를 살펴보면 아까 applyDefaultViewName 메소드를 통해서 설정한 viewName을 통해서 viewName을 가지고 오고 viewResolver에 viewName을 통해서 View를 찾고 View의 render 메소드를 통해서 view을 랜더링 해주고 있다.
이러한 과정을 거쳐서 요청이 들어왔을 때, 로직 처리와 뷰를 랜더링하는 과정을 내부 코드를 통해서 알아보았다.
여기서 getHandlerAdapter 메소드를 통해서 handler에 맞는 handlerAdapeter 찾고 가지고 오는 부분에서 adapeter 패턴이 적용이 된 것을 알 수 있었고 또 handler와 model, view를 통해서 MVC 패턴이 적용이 된 것을 알 수 있었다.
내부 코드를 보면서 MVC패턴과 Adapter패턴이 어떤 식으로 동작을 하는지를 알아보았다.
이제 간단하게 Spring MVC를 구현을 해보면서 좀 더 확실하게 알아보자.
먼저, 거의 기능 처리가 없는 간단한 ControllerV1과 ControllerV2를 만들었다.
// handlerAdapter의 인터페이스
public interface MyHandlerAdapter {
public boolean supports(Object handler);
public ModelView handle(Object handler);
}
handler를 처리할 수 있는 Adapter인지를 확인하는 support 메소드, 그리고 handler를 실행하는 handler 메소드를 구현 메소드로 만들고 이를 구현한 ControllerV1Adapater와 ControllerV2Adapter를 만들었다.
handler 메소드의 경우, ModelView를 반환하도록 만들었다.
// dispacterServlet 역할을 하는 FrontController
@WebServlet(name = "frontController", urlPatterns = "/frontController/*")
public class FrontController extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontController() {
initFrontController();
initControllerAdapter();
}
public void initFrontController() {
handlerMappingMap.put("/frontController/v1", new ControllerV1());
handlerMappingMap.put("/frontController/v2", new ControllerV2());
}
public void initControllerAdapter() {
handlerAdapters.add(new ControllerV1Adapter());
handlerAdapters.add(new ControllerV2Adapter());
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Object controller = getHandler(req);
if (controller == null) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(controller);
ModelView modelView = adapter.handle(controller);
String viewName = modelView.getName();
MyView view = viewResolver(viewName);
view.render(req, resp);
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF" + viewName + ".jsp");
}
private MyHandlerAdapter getHandlerAdapter(Object controller) {
for (MyHandlerAdapter handlerAdapter : handlerAdapters) {
if (handlerAdapter.supports(controller)) {
return handlerAdapter;
}
}
return null;
}
private Object getHandler(HttpServletRequest req) {
Object controller = handlerMappingMap.get(req.getRequestURI());
return controller;
}
}
먼저, 요청에 따른 controller들을 맵핑을 해서 handlerMappingMap에 담아두고 Adapter들의 리스트를 만들어 주는데 그 리스트가 handlerAdapters이다.
요청이 들어오면 그 요청의 url을 뽑아서 handlerMappingMap에서 요청에 해당하는 controller를 찾는다. 이 부분이 getHandler 메소드이다.
그리고 handlerAdapters을 순회하면서 controller에 맞는 adapter를 찾아낸다. 이 때, support메소드를 통해서 해당되는 controller인지를 파악한다. 이 부분이 getHandlerAdapter 메소드이다.
찾아내면 adapter의 handler라는 메소드를 통해서 controller를 실행하고 modelView를 반환을 한다.
그리고 이 modelView의 viewName를 viewResolve에 넘겨주서 myView를 찾고 render 메소드를 통해서 view를 랜더링해준다.
간단하게 생각을 하면 FrontController라는 곳에서 위 로직을 다 짠 다음 역할에 따라 메소드로 빼주면 우리가 DispatcherServlet 내부에서 봤던 getHandler, getHandlerAdapter, viewResolve등이 된다.
이렇게 간단하게 구현을 해보면서 Spring MVC에서 MVC패턴과 adapter패턴을 사용하는 것을 알아보았다.