본문 바로가기
Backend/Spring | SpringBoot

[Servlet] 회원 관리 웹 애플리케이션 만들기3 (어댑터 패턴)

by 2245 2023. 8. 18.

https://wngml56.tistory.com/238

 

회원 관리 웹 애플리케이션 만들기 (프론트 컨트롤러 패턴)

서론 https://wngml56.tistory.com/237 회원관리 웹 애플리케이션 만들기 (서블릿, JSP, MVC 패턴) 서론 서블릿, JSP, MVC 패턴으로 회원 관리 웹 애플리케이션을 만들어 봅시다. 본론 회원 관리 웹 애플리케이

wngml56.tistory.com

와 이어지는 글입니다.

이번에는 한 개의 프론트 컨트롤러에서 여러 종류의 컨트롤러를 처리할 수 있는 어댑터 패턴에 대해 작성하겠습니다.

 

목차

     

    유연한 프론트 컨트롤러 - v5

    만약 ControllerV3와 ControllerV4 모두를 사용하고 싶다면 어떻게 해야 할까요?

    public interface ControllerV3 {
        //paraMap을 파라미터로 받고 ModelView를 반환한다. 
        ModelView process(Map<String, String> paramMap);
    }
    public interface ControllerV4 {
        //paramMap과 model을 파라미터로 받고, String을 반환한다. 
        String process(Map<String, String> paramMap, Map<String, Object> model);
    }

    FrontControllerServletV4

    private Map<String, ControllerV4> controllerMap = new HashMap<>();
    
    public FrontControllerServletV4() {
        controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
        controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
        controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
    }
    • 코드를 보면 FrontControllerServletV4는 ControllerV4만 사용할 수 있도록 작성되어 있습니다.
    • 만약, ControllerV4 대신 ControllerV3를 controllerMap에 put 한다면 컴파일 오류가 발생합니다.
    • 이를 어댑터 패턴을 사용해 해결할 수 있습니다. 

     

     

    어댑터 패턴

    • 지금까지 우리가 개발한 프론트 컨트롤러는 한 가지 방식의 컨트롤러 인터페이스만 사용할 수 있습니다.
    • ControllerV3와 ControllerV4는 완전히 다른 인터페이스입니다.
    • 마치 v3는 110v이고, v4는 220v 전기 콘센트와 같습니다. 이럴 때 사용하는 것이 바로 어댑터입니다
    • 어댑터 패턴을 사용해 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변환함으로써 유연한 컨트롤러 설계가 가능합니다. 

    v5 구조

    모든 HTTP 요청은 FrontController에서 받습니다.

    1. 핸들러 map에서 요청 URL에 맞는 컨트롤러를 찾습니다.
      (핸들러는 컨트롤러를 의미합니다. 컨트롤러의 이름을 더 넓은 범위의 핸들러로 변경했습니다.)
    2. 핸들러 어댑터 목록에서 핸들러에 맞는 어댑터를 조회합니다. 
    3. 프론트 컨트롤러가 핸들러 어댑터를 호출합니다.
    4. 핸들러 어댑터가 핸들러를 호출합니다.
    5. 핸들러 어댑터는 로직에 맞춰 ModelView를 반환합니다. (v3도 v4도 ModelView를 반환하도록 변환합니다.)
    6. 프론트 컨트롤러는 반환된 View를 viewResolver를 통해 논리 이름을 물리 이름으로 변환합니다. 
    7. 반환받은 MyView를 호출하고, MyView는 request에 model를 담아 View(JSP)를 렌더링합니다. 
    참고 핸들러
    컨트롤러의 이름을 더 넓은 범위의 핸들러로 변경했습니다.
    그 이유는 어댑터가 있으면 꼭 컨트롤러의 개념뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문입니다. 
    참고 핸들러 어댑터
    프론트 컨트롤러와 핸들러 사이에 어댑터 역할을 하는 핸들러 어댑터가 추가되었습니다. 
    중간에서 로직에 맞게 변환해주는 이 핸들러 어댑터 덕분에 다양한 종류의 컨트롤러를 호출할 수 있습니다.  

     

     

    이제 코드로 구현해봅시다.

    MyHandlerAdapter - 인터페이스 

    어댑터용 인터페이스입니다. 

    public interface MyHandlerAdapter {
        
        //어댑터의 컨트롤러 처리 가능 여부를 반환합니다.  
        boolean supports(Object handler);	
        
        //실제 컨트롤러 호출 메서드입니다. 그 결과로 ModelView를 반환해야 합니다. 
        //컨트롤러가 ModelView를 반환하지 않는다면, 어댑터가 직접 ModelView를 생성해서 반환해야 합니다. 
        ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
    }

     

    먼저, ControllerV3용 어댑터를 작성해봅시다.

    ControllerV3HandlerAdapter

    public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
    	
        //ControllerV3을 처리할 수 있는 어댑터를 뜻합니다. 
        @Override
        public boolean supports(Object handler) {
            return (handler instanceof ControllerV3);	
        }
    
        //ControllerV3 호출
        @Override
        public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
           	//Object로 들어온 handler를 V3로 변환합니다. supports 메서드를 통해 호출 전 검증하기 때문에 걱정없이 변환할 수 있습니다.
            ControllerV3 controller = (ControllerV3) handler;	
            Map<String, String> paramMap = createParamMap(request);
            
            //ControllerV3은 원래 ModelView를 반환하기 때문에, 어댑터에서 특별히 변환해주는 작업은 없습니다. 
            ModelView mv = controller.process(paramMap);
            return mv;
        }
    
        //request의 파라미터를 map에 담아 반환합니다. 
        private Map<String, String> createParamMap(HttpServletRequest request) {
            Map<String, String> paramMap = new HashMap<>();
    
            Enumeration<String> paramNames = request.getParameterNames();
            while(paramNames.hasMoreElements()) {
                String name = paramNames.nextElement();
                paramMap.put(name, request.getParameter(name));
            }
    
            return paramMap;
        }
    }

     

    FrontControllerServletV5

    @WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
    public class FrontControllerServletV5 extends HttpServlet {
    	
        //핸들러 맵 
        //value가 v3, v4 모두 받을 수 있도록 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());
        }
    	
        //어댑터 등록 
        private void initHandlerAdapters() {
            handlerAdapters.add(new ControllerV3HandlerAdapter());		
            //현재는 ControllerV3 핸들러 어댑터만 생성되었습니다. 
        }
    
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            //요청 URL에 맞는 handler를 찾습니다. 
            Object handler = getHandler(request);
            if(handler == null) {
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                return;
            }
    
            //handler를 처리할 수 있는 adpater를 찾습니다. 
            MyHandlerAdapter adapter = getHandlerAdapter(handler);
            
            //handler 어댑터를 호출하고, ModelView를 반환받습니다. 
            ModelView mv = adapter.handle(request, response, handler);
    
            //반환받은 view를 사용하여 MyView를 생성합니다. 
            MyView view = viewResolver(mv.getViewName());
            //model과 view를 사용하여 jsp를 렌더링합니다. 
            view.render(mv.getModel(), request, response);
        }
    	
        //요청 URI에 맞는 handler를 반환합니다. 
        private Object getHandler(HttpServletRequest request) {
            String requestURI = request.getRequestURI();
            return handlerMappingMap.get(requestURI);
        }
    
        //handler에 맞는 어댑터를 찾아 반환합니다. 
        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");
        }
    
    }

     

    FrontControllerServletV5 - ControllerV4 추가 

    이제 FrontcontrollerServletV5에 ControllerV4도 추가해봅시다.

    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());
    
      //V4 컨트롤러 추가
      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());	//V4 어댑터 추가 
    }

    ControllerV4HandlerAdpater

    public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
    
        //ControllerV4를 처리할 수 있는 어댑터를 뜻합니다.
        @Override
        public boolean supports(Object handler) {
            return (handler instanceof ControllerV4);
        }
    
        //ControllerV4 호출
        @Override
        public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
            ControllerV4 controller = (ControllerV4) handler;
            
            //ControllerV4에 맞춰 paramMap과 model을 만들어 컨트롤러를 호출합니다. 
            Map<String, String> paramMap = createParamMap(request);
            Map<String, Object> model = new HashMap<>();
    
            //viewName을 반환받습니다. 
            String viewName = controller.process(paramMap, model);
     
            //어댑터 변환 -> view이름과 model을 사용해 ModelView로 변환합니다. 
            ModelView mv = new ModelView(viewName);
            mv.setModel(model);
            
            return mv;
        }
    
        private Map<String, String> createParamMap(HttpServletRequest request) {
            Map<String, String> paramMap = new HashMap<>();
    
            Enumeration<String> paramNames = request.getParameterNames();
            while(paramNames.hasMoreElements()) {
                String name = paramNames.nextElement();
                paramMap.put(name, request.getParameter(name));
            }
    
            return paramMap;
        }
    }

    어댑터 변환

    ModelView mv = new ModelView(viewName);
    mv.setModel(model);
    
    return mv;
    • 어댑터에서 이 부분은 단순하지만 중요한 부분입니다.
    • ControllerV4는 ModelView가 아니라 단순 View의 이름을 반환합니다. 어댑터는 이것을 ModelView로 만들어 형식에 맞추어 반환합니다.
    • 이것이 어댑터가 꼭 필요한 이유입니다. 마치 110v 전기 콘센트를 220v 전기 콘센트로 변경하는 것과 같습니다.

    어댑터와 ControllerV4

    public interface ControllerV4 {
        String process(Map<String, String> paramMap, Map<String, Object> model);
    }
    public interface MyHandlerAdapter {
        ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
    }

     

    정리

    어댑터를 사용함으로써 FrontController의 코드는 거의 손대지 않고, ControllerV3, ControllerV4 모두 처리할 수 있도록 변경했습니다.

    여기에 애노테이션을 사용한 컨트롤러를 사용하고 싶다면, 애노테이션을 지원하는 어댑터만 추가하면 됩니다.

    다형성과 어댑터 덕분에 기존 구조를 유지하면서 손쉽게 프레임워크의 기능을 확장할 수 있습니다.

     

     

    스프링 MVC

    사실 지금까지 작성한 코드는 스프링 MVC 프레임워크의 축약 버전이고, 구조도 거의 같습니다.

    직접 만든 MVC 프레임워크 구조

    SpringMVC 구조

    직접 만든 프레임워크 → 스프링 MVC

    • FrontController → DispatcherServlet
    • handlerMapping → HandlerMapping
    • MyHandlerAdpater → HandlerAdapter
    • ModelView → ModelAndView
    • viewResolver → viewResolver
    • MyView → View

     


    출처

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

     

    스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

    웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 원

    www.inflearn.com