[SpringMVC] 스프링 MVC의 구조를 이해해보자 (3/3)
V4: 사용하기 쉬운 컨트롤러
지난 포스팅에서 만든 V3는 분명 좋은 설계이지만 컨트롤러 인터페이스를 개발하는 개발자 입장에서 매번 ModelAndView를 생성하고 반환하는 것이 번거로울 수 있다. 그래서 이번에는 개발자가 사용하기 편한 컨트롤러를 만들어본다.
기본적인 구조는 V3와 같다. 하지만 프론트 컨트롤러가 컨트롤러를 호출할 때, paramMap 뿐만 아니라 객체를 담을 model을 추가적으로 전달한다는 것, 그리고 컨트롤러가 ModelAndView를 반환하는 것이 아니라 viewName만 반환한다는 차이가 있다.
프로세스 또한 V3와 크게 다르지 않다. 다만 Controller가 더이상 ModelAndView를 반환하지 않고 String을 반환한다. model 객체는 프론트 컨트롤러에서 파라미터로 전달받는다.
public interface Controller {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
FrontController.java
...
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
...
Controller의 구현체들은 전달받은 model에 객체를 넣고 논리적 뷰 이름을 반환하면 된다. model의 key-value가 변화되는 것은 부수효과이다.
MemberFormController.java
public class MemberFormController implements Controller {
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "form";
}
}
MemberListController.java
public class MemberListController implements Controller {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
List<Member> members = memberRepository.findAll();
model.put("members", members);
return "members";
}
}
MemberSaveController.java
public class MemberSaveController implements Controller {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
model.put("member", member);
return "save-result";
}
}
그리고 FrontController에서 더이상 ModelAndView를 사용하지 않도록 고쳐주면 끝이다.
FrontController.java
public class FrontController extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>(); //추가
String viewName = controller.process(paramMap, model);
View view = viewResolver(viewName);
view.render(model, request, response);
}
}
V5: 유연한 컨트롤러
어댑터 패턴
이제 대망의 V5를 만들 시간이다. V5의 구조는 스프링 MVC의 구조와 동일하다. 어떤 클라이언트는 V3를 사용하고 싶고, 어떤 사용자는 V4를 사용하고 싶을 수 있다. 하지만 지금까지 만든 프론트컨트롤러는 V3나 V4 둘 중에 하나만 사용할 수 있다. 즉 호환이 안된다. V5에서는 어댑터 패턴을 적용해서 프론트컨트롤러가 다양한 형태의 컨트롤러를 처리할 수 있도록한다.
❗️컨트롤러의 이름이 핸들러로 변경되었다. 지금부터는 ‘컨트롤러’와 ‘핸들러’라는 용어를 혼용한다.
- 핸들러: 컨트롤러의 이름을 핸들러로 변경했다. 어댑터로 인해서 해당하는 어댑터만 있으면 컨트롤러 역할 이외에도 다양한 처리가 가능하기 때문이다.
- 핸들러 어댑터: 프론트 컨트롤러와 핸들러 사이의 인터페이스 역할을 한다. 여러 종류의 핸들러가 있어도 이에 대응하는 핸들러어댑터만 있으면 프론트컨트롤러의 구현을 변경하지 않고도 여러 핸들러를 바꿔가면서 사용할 수 있다.
우선 어댑터를 구현하기 전에 큰 흐름을 먼저 살펴보자.
FrontController.java
@WebServlet(name = "frontController", urlPatterns = "/v5/*")
public class FrontController extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<HandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontController() {
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerAdapters() {
handlerAdapters.add(new V4HandlerAdapter());
handlerAdapters.add(new V3HandlerAdapter());
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/v5/v4/members/form", new MemberFormControllerV4());
handlerMappingMap.put("/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/v5/v4/members", new MemberListControllerV4());
handlerMappingMap.put("/v5/v3/members/form", new MemberFormControllerV3());
handlerMappingMap.put("/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/v5/v3/members", new MemberListControllerV3());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 요청 URI에 매핑되는 핸들러를 handlerMappingMap에서 가져옴
Object handler = getHandler(request);
if (handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 해당 핸들러를 처리할 수 있는 핸들러어댑터를 handlerAdapters에서 찾아옴
HandlerAdapter handlerAdapter = getHandlerAdapter(handler);
// 찾아온 핸들러어댑터에서 핸들러를 호출해 ModelAndView를 반환받는다
ModelAndView mv = handlerAdapter.handle(request, response, handler);
View view = viewResolver(mv.getViewName());
view.render(mv.getModel(), request, response);
}
...
}
- handlerMappingMap: 이전에 V3, V4에서 사용했던 ControllerMap과 같다. 다만 V5에서는 V3, V4 컨트롤러를 모두 사용할 수 있는 구조를 만들 것이기 때문에 URI를 구분해서 V3 컨트롤러와 V4 컨트롤러를 모두 담아두었다.
- handlerAdapters: 요청된 URI에 맞는 핸들러(컨트롤러)가 선택되면 이에 맞는 핸들러어댑터를 여기서 꺼내온다. 실제로 컨트롤러를 호출하는 것은 여기서 꺼내온 handler가 될 것이다.
우선 앞으로 만들 HandlerAdapterV3와 HandlerAdapterV4를 어떻게 구현할지 정의하는 인터페이스를 만든다.
HandlerAdapter.java
public interface HandlerAdapter {
boolean support(Object handler);
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler);
default Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> result = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(
paramName -> result.put(paramName, request.getParameter(paramName)));
return result;
}
}
- support(): 해당 어댑터가 인자로 넘어온 핸들러를 처리할 수 있는지 여부를 반환한다.
- handle(): 핸들러를 호출하고 ModelAndView를 반환받는다.
V3에서는 컨트롤러가 ModelAndView를 반환하고, V4에서는 컨트롤러가 String만 반환한다. 이전에는 컨트롤러가 ModelAndView를 반환하느냐 String을 반환하느냐에 따라 프론트컨트롤러의 구현이 달라졌다. 그러나 V5의 핵심은 프론트컨트롤러의 구현을 변경하지 않고도 여러 핸들러를 처리할 수 있도록 하는 것이다.
이를 위해서 V5 프론트컨트롤러에서는 일관적으로 ModelAndView를 처리하도록 만들고 대신 V4의 핸들러어댑터가 V4의 컨트롤러로부터 String을 반환받아서 ModelAndView를 만들어 반환하도록 만든다.
V3HandlerAdapter.java
public class V3HandlerAdapter implements HandlerAdapter {
@Override
public boolean support(Object handler) {
return handler instanceof ControllerV3;
}
@Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
return controller.process(paramMap);
}
}
V3의 핸들러(컨트롤러)는 ModelAndView를 반환하기 때문에 단순히 컨트롤러를 호출하고 반환 값을 그대로 반환해주기만 하면 된다.
V4HandlerAdapter.java
public class V4HandlerAdapter implements HandlerAdapter {
@Override
public boolean support(Object handler) {
return handler instanceof ControllerV4;
}
@Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
ModelAndView mv = new ModelAndView(viewName);
mv.setModel(model);
return mv;
}
}
이에 반해 V4의 컨트롤러는 논리적인 view name(String)을 반환하기 때문에 핸들러어댑터가 직접 ModelAndView를 만들어서 반환한다.
이렇게 해서 스프링 MVC의 구조를 직접 구현해보았다.
Spring MVC의 구조
스프링 MVC의 구조는 다음과 같다.
출처: https://velog.io/@gillog/Spring-MVC-%EA%B5%AC%EC%A1%B0
구조를 보면 앞서 만든 V5 버전과 거의 똑같은 것을 알 수 있다. V5에서는 HandlerMapping과 HandlerAdapter를 자료구조로, ViewResolver를 메서드로 만들었지만 스프링에는 빈으로 등록되어있다.
Spring MVC는 프론트 컨트롤러 패턴을 적용하고 있으며 DispatcherServlet
은 Spring MVC의 프론트 컨트롤러이다. DispatcherServlet의 상속 관계를 보면 HttpServlet을 상속받고 있다.
스프링부트는 DispatcherServlet
을 서블릿으로 자동으로 등록하면서 모든 경로(“/“)에 대해 매핑한다. 서블릿이 호출되면 HttpServlet
의 service() 메서드가 호출되는데, 그 자식인 FrameworkServlet
에서 service() 메서드를 오버라이드하고 있다. 이어서 DispatcherServlet
의 doDispatch() 메서드가 호출되는데, 코드를 살펴보면 핸들러를 조회하고, 핸들러 어댑터를 조회한 뒤, 핸들러 어댑터가 핸들러를 실행하며 ModelAndView
를 반환받고 View를 render() 하는 것을 볼 수 있다.
org.springframework.web.servlet.DispatcherServlet.java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
...
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
...
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
...
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
...
render(mv, request, response);
...
}
protected void render(ModelAndView mv, HttpServletRequest request,HttpServletResponse response) throws Exception {
View view;
String viewName = mv.getViewName();
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
...
// render view
view.render(mv.getModelInternal(), request, response);
}
가장 많이 사용하는 @RequestMapping
애너테이션을 사용하는 컨트롤러들은 RequstMappingHandlerMapping
과 RequestMappingHandlerAdpater
를 통해 검색되고 호출된다. 이 핸들러 매핑과 핸들러 어댑터가 스프링에서 우선순위가 제일 높다.