스프링

[ Spring ] 고대의 서블릿을 찾아서(8) - 어댑터패턴과 트레이드 오프

do_it_zero 2024. 10. 14. 09:09

서블릿 -> 템플릿 엔진 -> MVC 패턴 -> FrontController 까지 왔다. 특히 FrontController를 직접 만들면서 HTTP 요청을 처리할 수 있는 다양한 버전의 Controller를 만들었다. 그렇다면 하나의 FrontController에서 다양한 버전의 Controller 들을 사용할 수 없을까?

어댑터 패턴 등장

다양한 버전의 Controller들은 반환 값이 다 다르다. 반환 값이 다 다르면 FrontController에서는 Controller 버전에 따라 로직을 따로 작성해야하는 번거로움이 생긴다. 따라서 Controller가 반환하는 값을 공통으로 만들어 줄 무언가가 필요한데 그것이 바로 어댑터이다. 아이폰을 충전하든, 맥북을 충전하든 결국 220v 소위 말하는 돼지코가 어댑터랑 연결되어야 충전기가 전기를 받아서 아이폰이든 맥북을 충전할 수 있는 것이다. 돼지코라는 규격이 FrontController에서 공통으로 처리할 수 있는 규격이라고 할 수 있고, 아이폰,맥북에 맞는 충전기 자체를 어댑터라고 할 수 있다.

어댑터 패턴을 추가한 FrontController

@WebServlet(name = "frontControllerServletV5",urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet{

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

    public FrontControllerServletV5(){
        initHandlerMappingMap();
        initHadlerAdpters();
    }

    private void initHandlerMappingMap(){
        // v2 컨트롤러를 handleMappingMap에 추가
        handelMappingMap.put("/front-controller/v5/v2/members/new-form",new MemberFormControllerV2());
        handelMappingMap.put("/front-controller/v5/v2/members/save",new MemberSaveControllerV2());
        handelMappingMap.put("/front-controller/v5/v2/members", new MemberListControllerV2());

        // v3 컨트롤러를 handleMappingMap에 추가
        handelMappingMap.put("/front-controller/v5/v3/members/new-form",new MemberFormControllerV3());
        handelMappingMap.put("/front-controller/v5/v3/members/save",new MemberSaveControllerV3());
        handelMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
    
        // v4 컨트롤러들을 handleMappingMap에 추가
        handelMappingMap.put("/front-controller/v5/v4/members/new-form",new MemberFormControllerV4());
        handelMappingMap.put("/front-controller/v5/v4/members/save",new MemberSaveControllerV4());
        handelMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
    }

    private void initHadlerAdpters(){
        handlerAdapters.add(new ControllerV2HandlerAdapter());
        handlerAdapters.add(new ControllerV3HandlerAdapter()); 
        handlerAdapters.add(new ControllerV4HandlerAdapter()); 

    }
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServletV2.service");
        
        // 1. 요청에 맞는 handler 찾기
        Object handler = getHandler(request);
        if(handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 2. 컨트롤러(handler)을 실행시킬 수 있는 어댑터 가져오기 
        MyHandlerAdapter adapter = getHandlerAdapter(handler);

        // 3. 어댑터 handle을 실행하면 컨트롤러 process가 실행되고, 
        //    컨트롤러 실행 후 데이터와 viewName이 담긴ModelView 객체를 반환  
        ModelView mv = adapter.hadnle(request, response, handler);
        
        // 4. viewResolver로 논리이름의 viewName을 물리이름의 viewPath로 바꾼 후 viewPath가 담긴 MyView객체 반환
        MyView view = viewResolver(mv.getViewName());

        // 5. jsp 서블릿에 데이터를 포워딩함.
        view.render(mv.getModel(), request, response);
    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handelMappingMap.get(requestURI); // key로 handleMappingMap에 컨트롤러 가져옴
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler){
        for(MyHandlerAdapter adapter:handlerAdapters){
            if(adapter.supports(handler)){
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = " + handler);
    }

    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/"+viewName+".jsp");
    }
}
  1. handlerMappingMAp
    FrontController의 handlerMappingMap에 키에는 요청 URL, 값에은 요청 URL에 해당하는 Controller가 저장된다. '요청 URL이 왔을때 바로 컨트롤러를 실행시키면 되지 않을까?' 하는 생각이 들 수 있다. 그러나 그렇게 되면 FrontController에는 Controller 버전에 해당하는 로직들을 추가해야한다. 그렇게 되면 코드가 매우 복잡해질 것이다. 따라서 어댑터라는 것을 만들어서 어댑터가 controller를 실행시키도록 만들면 되는 것이다.

2.handlerAdapters

private void initHadlerAdpters(){
    handlerAdapters.add(new ControllerV2HandlerAdapter());
    handlerAdapters.add(new ControllerV3HandlerAdapter()); 
    handlerAdapters.add(new ControllerV4HandlerAdapter()); 
}

각 버전의 맞는 controller를 실행시킬 수 있는 adpter들이 들어있다. getHandlerAdapter를 통해서 어댑터들 중에 controller를 실행시킬 수 있는 adapter를 찾을 수 있다.

어댑터들은 어떻게 만들면 될까? controller 실행 후 반환 값이 다르지만, 어댑터안에서 controller가 실행 후에는 동일한 반환 값이 되도록 만들어야한다. 따라서 adapter의 handle에서 controller를 실행 후 반환 값이 동일하도록 추상화 시켜야한다.

public interface MyHandlerAdapter {
    
    boolean supports(Object handler);

    ModelView hadnle(HttpServletRequest request,
        HttpServletResponse response,Object handler) throws ServletException,IOException;
}

public class ControllerV2HandlerAdapter implements MyHandlerAdapter{

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV2);
    }

    @Override
    public ModelView hadnle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws ServletException, IOException {
        ControllerV2 controller = (ControllerV2) handler;
        
        MyView view = controller.process(request,response);
        
        String viewPath = view.getViewPath();
        // 시작 인덱스
        int startIndex = viewPath.lastIndexOf("/") + 1;
        int endIndex = viewPath.lastIndexOf(".");

        // 논리 부분만 추출
        String viewName = viewPath.substring(startIndex, endIndex);
        
        ModelView mv = new ModelView(viewName);
        return mv;
    }
}

public class ControllerV3HandlerAdapter implements MyHandlerAdapter{

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV3);
    }

    @Override
    public ModelView hadnle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws ServletException, IOException {
        ControllerV3 controller = (ControllerV3) handler;
        
        Map<String,String> paramMap = createParamMap(request);
        
        ModelView mv = controller.process(paramMap);
        return mv;
    }
    
    private 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;        
    }
}

public class ControllerV4HandlerAdapter implements MyHandlerAdapter{

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV4);
    }

    @Override
    public ModelView hadnle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws ServletException, IOException {
        ControllerV4 controller = (ControllerV4) handler;
        
        Map<String,String> paramMap = createParamMap(request);
        Map<String,Object> model = new HashMap<>();
        
        String viewName = controller.process(paramMap,model);

        ModelView mv = new ModelView(viewName);
        mv.setModel(model);
        return mv;
    }
    
    private 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;        
    }
}

어댑터 패턴이 무조건 좋을까?? 여기서 중요한 점은 트레이드 오프가 발생한다는 것이다.
어디서 트레이드오프가 발생 할 수 있을까? 바로 ControllerV2 이다.

public interface ControllerV2 {
    MyView process(HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException; 
}

ControllerV2의 반환 타입은 MyView이며 viewPath 물리 경로를 MyView 객체안에 넣어서 반환한다. 하지만 adapter의 반환 타입은 ModelView로 통일하였다. 따라서 어댑터 안에서 controller를 실행 후 viewPath에서 논리 이름인 viewName을 추출하는 코드 부분을 추가해야 한다.

public class ControllerV2HandlerAdapter implements MyHandlerAdapter{

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV2);
    }

    @Override
    public ModelView hadnle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws ServletException, IOException {
        ControllerV2 controller = (ControllerV2) handler;
        
        MyView view = controller.process(request,response);
        
        String viewPath = view.getViewPath();
        // 시작 인덱스
        int startIndex = viewPath.lastIndexOf("/") + 1;
        int endIndex = viewPath.lastIndexOf(".");

        // 논리 부분만 추출
        String viewName = viewPath.substring(startIndex, endIndex);
        
        ModelView mv = new ModelView(viewName);
        return mv;
    }
}

어댑터 패턴을 써서 여러 버전의 contoller를 쓸 수 있다는 장점이 있지만 controllerV2와 같이 viewName을 추출해야하는 추가해야 한다면 오히려 비용이 증가가 될 수 있다. 따라서 어댑터패턴을 위해 contollerV2 추가 시 비용이 더 들것이 예상된다면 controllerV2를 굳이 추가하지 않을 것이다.

정리

고대의 서블릿부터 frontController 어댑터패턴까지 공부하면서 어떻게 불편함을 개선했는지 알게되었다. 특히 이전에는 불편함만 개선하는게 목적이였다면, 이번에는 추가하는게 나은 선택인지 아닌지 생각해볼 시간이 되었다. 모든 것에는 트레이드 오프가 있다. 트레이드 오프를 생각하며 변경이나 추가가 맞을지, 아니면 그대로 두는게 맞을지 등을 생각해볼 수 있는 시간이였다.

참고
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/