✅ 김영한 님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술을 공부하며 정리한 글입니다.
앞서 V1 ~ V4로 리팩토링 하면서 사용하는 함수의 파라메터나 반환 타입의 차이 등이 발생했다. 예를 들어:
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
//뷰 이름만 넘김
String process(Map<String, String> paramMap, Map<String, Object> model);
}
그런데 만약 V4에서 V3로 바꾸고 싶다면??
이런 경우 스펙에 맞춰 갈아 끼울 수 있게 도와주는 어댑터 패턴이 등장한다.
이해를 돕기 위해 코드를 바탕으로 클래스 다이어그램을 그려보았다.
위 아키텍처와 클래스 다이어그램을 참고하여 코드 구성을 뜯어보자.
package hello.servlet.web.frontcontroller.v5;
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
//모든 컨트롤러를 다 지원하기 위해 Object 형
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
//핸들러 등록
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
}
//핸들러 어댑터 등록
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object handler = getHandler(request);
if(handler == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter myAdapter = getHandlerAdapter(handler);
ModelView mv = myAdapter.handle(request, response, handler);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if(adapter.supports(handler)){
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다");
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
편의상 import 문은 다 생략했다.
우선 FrontControllerServletV5의 프로퍼트와 메소드를 정리하면 다음과 같다:
handlerMappingMap에는 다음과 같은 url과 이에 매핑된 컨트롤러가 들어있다.
"/front-controller/v5/v3/members/new-form": MemberFormControllerV3의 객체
"/front-controller/v5/v3/members/save": MemberSaveControllerV3의 객체
"/front-controller/v5/v3/members": MemberListControllerV3의 객체
...
그리고 handlerAdapters에는 ControllerV3와 ControllerV4에 해당하는 핸들러 어댑터를 담고 있다. 이 어댑터 목록으로부터 특정 핸들러를 처리할 수 있는 어댑터를 조회하게 된다.
service 코드를 따라가 보자.
Object handler = getHandler(request);
그러면 handler에는 MemberFormControllerV3의 객체와 같은 특정 컨트롤러 구현체를 담게 된다.
상위 타입인 Object에 담겨서 다형성을 구현할 수 있다.
MyHandlerAdapter myAdapter = getHandlerAdapter(handler);
결국 myAdapter 변수는 핸들러에 맞는 어댑터 정보를 가지게 된다. 예를 들어 handler가 MemberFormControllerV3의 객체였다면, ControllerV3HandlerAdapter를 가지게 될 것이다.
ModelView mv = myAdapter.handle(request, response, handler);
그러면 어떤 handle 메소드가 실행될까? 여기서 다형성의 활용을 엿볼 수 있다. 아마도 myAdapter의 구체 클래스인 ControllerV3HandlerAdapter의 handle이 호출될 것이다. 위 클래스 다이어그램을 참고하면 쉽다.
이 다음부터는 이전 V4까지의 흐름과 똑같다. ModelView를 반환하여 viewResolver를 호출하여 물리 이름으로 바꾼 뒤, 뷰 렌더링을 진행한다.
다만, 여기서 주목할 점은 ControllerV3와 ControllerV4의 파라메터와 반환 타입이 다르다는 것이다. (클래스 다이어그램 참고)
이러한 차이를 어댑터 패턴을 적용하여 어떻게 극복했는지 살펴보자.
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
//...
//어댑터 변환
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
}
분명 handler 메소드는 ModelView를 반환한다. 그러나 기존 V4는 String 타입인 뷰 이름만 반환한다. V4의 스펙을 어댑터 스펙에 맞추는 변환 과정이 필요하다. ModelView를 반환하기 위해 해당 뷰 이름과 모델을 담은 ModelView를 만들어 반환하면 된다.
이렇게 V3나 V4의 구현 방식을 선택하여 갈아끼울 수 있는 어댑터 패턴까지 적용해보았다. 이러한 설계가 어떤 면에서 좋은지 짚어보자면: