들어가며

  • 이전 포스트에서 확장성을 구현하기 위해 HandlerAdapter라는 개념을 가져왔고, 이 HandlerAdapter가 가져야 하는 책임까지 알아보았다.
  • 이번 포스트에서는 이 HandlerAdapter 인터페이스를 구현한 구현체들을 알아보고, 프론트 컨트롤러가 어떻게 변경되는지에 대해 알아보도록 한다. 마지막으로는 확장성을 적용한 전체적인 구조에 대해서도 살펴본다.

 

 

 

HandlerAdapter의 구현체

  • HandlerAdapter의 구현은 다음과 같았다.
public interface MyHandlerAdapter {

    boolean supports(Object handler);

    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
  • 이제 이 구현체들을 구현해보자. 먼저 ControllerV3을 호출할 수 있는 HandlerAdapter 구현체부터 구현해 보도록 하자.

ControllerV3HandlerAdapter

  • 먼저 이 구현체가 자신이 받은 handler 객체를 다룰 수 있는지를 확인하는 supports 부터 구현해보자.
    • 해당 메서드는 들어온 handler가 버전 3의 컨트롤러인 ControllerV3 인스턴스로 변환될 수 있는가를 반환하도록 하면 끝난다.
public class ControllerV3HandlerAdapter implements HandlerAdapter{
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV3);
    }
    
    ...
}
  • 이제 호출하는 부분인 handle을 살펴보자. 버전 3의 컨트롤러는 서블릿 종속성을 제거하고 그 대신에 요청에서 넘어온 파라미터들을 넘겨주는 별도의 모델을 넣어주었다. 이 모델의 형식이 key, value의 Map 형식이었기 때문에 해당 Map을 만들어서 호출하도록 해야한다.
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV3);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        ControllerV3 controllerV3 = (ControllerV3) handler;

        Map<String, String> paramMap = createParamMap(request);
        return controllerV3.process(paramMap);
    }

    private static Map<String, String> createParamMap(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));

        return paramMap;
    }
}
  • 코드가 어디선가 본 적이 있을수 있다. 바로 프론트 컨트롤러 V3을 만들 때의 그 코드들이다.
  • 이제 다음 구현체인 버전 4 컨트롤러를 다루는 어댑터 구현체를 구현해보자.

ControllerV4HandlerAdapter

  • 코드부터 보도록 하자.
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV4);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        ControllerV4 controllerV4 = (ControllerV4) handler;

        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();

        String viewName = controllerV4.process(paramMap, model);

        ModelView modelView = new ModelView(viewName);
        modelView.setModel(model);

        return modelView;
    }

    private static Map<String, String> createParamMap(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));

        return paramMap;
    }
}
  • 이전 프론트 컨트롤러 버전 4에서 컨트롤러를 호출하는 부분을 때온것과 거의 일치한다. 달라진게 있다면 프론트 컨트롤러에서 직접 호출할 때는 View의 논리적 이름을 ViewResolver를 통해 MyView 객체로 만들게 했다면 여기서는 ModelView에 View의 논리적 이름과 모델을 넣도록 하고 있다.
  • 이는 버전 3에서는 ModelView를 반환했지만, 버전 4에서는 String을 반환함으로 인해 생기는 여러 타입을 처리하는 문제를 회피하기 위해 View의 논리적 이름과 모델을 둘 다 가진 ModelView를 반환하도록 한 것이다.
  • ModelView로 통일함으로서 프론트 컨트롤러는 View의 논리적 이름을 가져와 ViewResolver에 처리하게 하도록 하는 방법으로 방식을 통일할 수 있다.

 

 

 

프론트 컨트롤러

  • 이번 부분에서는 확장성만을 다루고 있기 때문에 컨트롤러의 구현은 없다. 왜냐하면 이미 존재하는 컨트롤러들을 하나의 프레임워크에 모두 넣어 돌아갈 수 있게 만드는 것이기 때문이다.
  • 프론트 컨트롤러는 조금 달라지는데 어떤 부분이 달라지는지를 확인해보자.
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {

    private final Map<String, Object> handlerMappingMap = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();

    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 adapter = getHandlerAdapter(handler);
        ModelView modelView = adapter.handle(request, response, handler);

        MyView view = viewResolver(modelView.getViewName());
        view.render(modelView.getModel(), request, response);
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)){
                return adapter;
            }
        }

        throw new IllegalArgumentException("handler adapter 를 찾을 수 없습니다. handler : " + handler);
    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

    private static MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}
  • 코드가 꽤 방대해졌다. 하나씩 천천히 알아보도록 하자. 우선은 초기화 부분부터 살펴본다.
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {

    private final Map<String, Object> handlerMappingMap = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();

    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());
    }
}
  • 이전에 각 버전에 맞게 설정했던 handlerMapping 정보가 Object로 모든 타입의 핸들러들을 받을 수 있게 바뀌었다.
  • 그에 더해 HandlerAdapter의 구현체들을 모아놓은 handlerAdapterList가 새롭게 생겼다.
  • 프론트 컨트롤러 서블릿이 수행되는 순간 등록한 모든 핸들러(컨트롤러) 정보들을 handlerMapping에 저장한다. 그리고 현재 HandlerAdapter의 구현체들도 handlerAdapterList에 저장된다.
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {

   ...

    @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 adapter = getHandlerAdapter(handler);
        ModelView modelView = adapter.handle(request, response, handler);

        MyView view = viewResolver(modelView.getViewName());
        view.render(modelView.getModel(), request, response);
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)){
                return adapter;
            }
        }

        throw new IllegalArgumentException("handler adapter 를 찾을 수 없습니다. handler : " + handler);
    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

    private static MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}
  • 먼저 getHandler부터 보도록 하자. getHandler는 들어온 요청의 URI를 확인하여 handlerMapping에 해당 URI를 처리 가능한 핸들러가 존재하는지를 찾는다. 이후 해당 핸들러를 반환한다.
  • 핸들러(컨트롤러)가 반환되었다면 해당 핸들러를 호출할 수 있는 HandlerAdapter를 찾기 시작한다. 이는 getHandlerAdapter() 메서드에서 확인이 가능한데 handlerAdapterList를 순회하면서 supports()를 통해 호출 가능한 HandlerAdapter인지를 확인한다.
    • 만약 다룰 수 있는 HandlerAdapter 구현체를 찾았다면 해당 구현체를 반환한다.
    • 만약 찾을 수 없다면 해당 핸들러를 처리할 수 있는 HandlerAdapter를 찾을 수 없었다는 예외를 던진다.
  • 이후 해당 HandlerAdapter 구현체에 핸들러를 호출하도록 요청을 보낸다. HandlerAdapter는 해당 요청을 받으면 입력으로 받은 핸들러를 호출하여 요청을 처리하고 그 결과를 ModelView로 반환한다.
  • ModelView를 반환받은 프론트 컨트롤러는 ViewResolver를 호출하여 View의 물리적 경로를 가진 MyView 객체를 반환받고, ModelView에 존재하는 Model과 함께 render를 수행한다.

 

 

 

결과

  • HandlerAdapter 라는 개념을 도입하고, 이 HandlerAdapter에 컨트롤러를 호출하는 책임을 위임한 덕분에 이 HandlerAdapter를 추상화 시킴으로서 여러 타입의 컨트롤러들을 호출할 수 있도록 확장성이 추가되었다.
  • 이제 개발자는 이전 버전의 컨트롤러를 구현하여 꽂아주어도 문제 없이 해당 컨트롤러를 프레임워크에서 사용할 수 있게 되었다.
  • 심지어 아예 처음 보는 타입의 컨트롤러를 구현하게 되더라도 해당 타입에 맞는 HandlerAdapter구현체를 만든다면 문제 없이 프레임워크에 포함시킬 수 있게 되었다.
  • 다음 포스트에서는 확장성까지 포함된 전체적인 프레임워크 구조를 알아본다.
복사했습니다!