ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • (4) MVC 프레임워크 만들기 - 유연한 FrontController 만들기
    Spring/MVC1-Servlet vs JSP vs MVC패턴 2023. 6. 13. 21:02
    728x90

    이전단계링크

    https://hongs429-blog.tistory.com/48
     

    (3) MVC 프레임워크 만들기 - Controller 구현체들의 공통로직 처리-2

    이전단계링크 https://hongs429-blog.tistory.com/47 (2) MVC 프레임워크 만들기 - Controller 구현체들의 공통로직 처리 https://hongs429-blog.tistory.com/46 (1) MVC 프레임워크 만들기 - FrontController의 도입 Servlet -> JSP ->

    hongs429-blog.tistory.com

     

     

    지금부터는 내가 만든 프레임워크를 다양한 원하는 방식의 Controller를 사용할 수 있게 해주는 작업을 하려고 한다.

    이전 단계에서는 Controller 구현체들이 리턴값을 ModelView 객체를 반환하는 방식을 취했다.

    하지만, 구현하는 방법에 따라서 단순히 View의 논리경로를 반환하도록 하게끔 만들 수도 있다.

     

    public class MemberSaveControllerV4 implements ControllerV4 {
    
        private 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";
    
        }
    }

    서비스로직의 결과 데이터는 파라미터로 넘어온 Map에 저장하여 값을 기억하도록 하게하고, 단순히 논리 경로만을 보내는 경우이다.

     

    이렇게 된다면 FrontController를 다음과 같이 설계를 해야 한다.

    @WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
    public class FrontControllerServletV4 extends HttpServlet {
    
        private Map<String, ControllerV4> controllerV4Map = new HashMap<>();
    
        public FrontControllerServletV4() {
            controllerV4Map.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
            controllerV4Map.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
            controllerV4Map.put("/front-controller/v4/members", new MemberListControllerV4());
    
        }
    
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            System.out.println("FrontControllerServletV4.service");
    
            String requestURI = request.getRequestURI();
            System.out.println("requestURI = " + requestURI);
    
            ControllerV4 controller = controllerV4Map.get(requestURI);
    
            if (controller == null) {
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                return;
            }
    
       		// 요청에 담겨 있는 데이터를 만드는 과정 servlet > map 으로 전환
            Map<String, String> paramMap = createParamMap(request);
            
            // ModelView 객체 대신에 데이터를 전송할 Map 객체
            Map<String, Object> model = new HashMap<>(); 
    
            // model 인자에는 parameter로 처리한 결과 객체가 담겨 있다.
            String viewName = controller.process(paramMap, model);
    
            MyView view = viewResolver(viewName);
    
            view.render(model, request, response);
    
        }
    
        private static MyView viewResolver(String viewName) {
            return new MyView("/WEB-INF/views/" + viewName + ".jsp");
        }
    
        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;
        }
    }

    이전과 달라진 것은 FrontController 에서 직접 Map객체를 만들어 인자로 넘겨주어 서비스로직이 수행되고 난 결과 데이터를 직접 가지고 오는 방식이다.

     

     

     

    이제부터 우리의 프레임워크는

    Controller가 ModelView객체를 반환하던지,

    view경로의 논리 경로를 String 으로 반환하던지

    알아서 판단하여 서비스 로직을 처리하는 유연한 FrontController를 만들 것이다.

     

     

    위의 과정을 해결하기 위해선 선행되어야 할 것이 몇가지 존재한다.

    • Controller interface가 2가지 중 무엇인지 알아야 한다.
    • 이후에는 해당 컨트롤러 interface를 상속받아 구현한 Controller 구현체의 로직을 2가지 interface를 상속받은 Controller 구현체 모두 동일한 리턴값을 내도록 작업을 해주어야 한다.

     

    우리의 작업을 그림으로 도식화 해보자.

    김영한의 로드맵 강의 - MVC 1편 내용 中

     

     

    1번 과정

    @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<>();
    
        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());
    
            // 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());
        }
    
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
            // handler mapping
            Object handler = getHandler(request);
            ...
         }
         
        private Object getHandler(HttpServletRequest request) {
            String requestURI = request.getRequestURI();
            return handlerMappingMap.get(requestURI);
        }
    }

    요청이 들어온 URL 정보를 통해서 Controller 구현체를 찾는다.

    하지만 해당 Controller 구현체는 어떤 컨트롤러 인터페이스를 상속받았는지를 알 수 없다.

    어떤 컨트롤러 인터페이스를 상속 받았는지에 따라 서비스 로직의 구성이 달라진다.

     

     

    여기서 우리는

     

    adapter 라는 개념

     

    을 도입할 것이다!

    adpater는 말그대로 원하는 형태의 것으로 갈아 끼울 수 있도록 도와주는 역할을 하는 Interface로 구현을 할 것이다.

    즉, adapter를 통해 ModelView를 리턴하는 버전을 사용할 것인지, 논리경로를 String으로 리턴하는 버전의 컨트롤러를 사용할 것인지를 adapter를 통해서 결정하게 되는 것이다.

     

     

    adapter의 역할을 정의한 interface를 보면서 설명해보자.

    public interface MyHandlerAdapter {
    
        boolean support(Object handler);
    
        ModelView handle(HttpServletRequest request,
        HttpServletResponse response,
        Object handler) throws ServletException, IOException;
    }

    support 메소드는 handler(Controller 구현체)가 어떤 인터페이스를 상속받았는지 boolean타입으로 체크를 한다.

     

    그리고 Controller 구현체를 통해서 ModelView 객체를 리턴받는 역할을 한다. 즉,

     

     

    <ModelView리턴 타입 버전의 Controller를 다룰 수 있게 해주는 adapter 구현체>

    public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
        @Override
        public boolean support(Object handler) {
            return (handler instanceof ControllerV3);
        }
    
        @Override
        public ModelView handle(
        		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 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;
        }
    }

     

     

    ✨✨✨✨<FrontController 보기>✨✨✨✨

    @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<>();
    
        public FrontControllerServletV5() {
            initHandlerMappingMap();
            initHandlerAdapters();
        }
    	// Controller 구현체 등록
        private void initHandlerMappingMap() {
           	// v3 : ModelView 를 리턴하는 Controller 구현체 버젼
            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 : 논리경로를 String으로 리턴하는 Controller 구현체 버젼
            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 {
    
            // 다형성을 통해 어떤 Controller 구현체(handler)가 선정되었는지 확인
            Object handler = getHandler(request);
    
    
            if (handler == null) {
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                return;
            }
    
            // adapter 조회과정
            MyHandlerAdapter adapter = getHandlerAdapter(handler);
    		
            // 각각의 버전에 맞는 서비스 로직 수행된 결과 데이터를 ModelView 객체로 받아옴
            ModelView mv = adapter.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.support(handler)) {
                    return adapter;
    
                }
            }
            return null;
        }
    
        private static MyView viewResolver(String viewName) {
            return new MyView("/WEB-INF/views/" + viewName + ".jsp");
        }
    }

     

     

     

     

     

     

    <서비스 로직 비교 코드>

    - 논리경로를 반환받는 버전의 Controller 구현체

    public class MemberSaveControllerV4 implements ControllerV4 {
    
        private 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";
    
        }
    }

    - ModelView 객체를 받환 받는 버전의 Controller 구현체

    public class MemberSaveControllerV3 implements ControllerV3 {
    
        private MemberRepository memberRepository = MemberRepository.getInstance();
    
        @Override
        public ModelView process(Map<String, String> paramMap) {
            String username = paramMap.get("username");
            int age = Integer.parseInt(paramMap.get("age"));
    
            Member member = new Member(username, age);
            memberRepository.save(member);
    
            ModelView mv = new ModelView("save-result");
            mv.getModel().put("member", member);
            return mv;
        }
    }

    핵심은,@@@@@

    이전의 두가지 버전의 서비스 로직은 하나도 손을 건드리지 않았다.

    그럼에도 현재의 FrontController는 두 로직 모두 사용할 수 있게끔 확장된 기능을 제공하도록 변경되었다.

     

     

     

    전체그림으로 다시한번 review

    김영한의 로드맵 강의 - MVC 1편 내용 中

     

    지금까지의 방식이 SpringBoot가 http 요청을 처리하는 로직의 핵심을 반영하여 만든 나만의 MVC 프레임워크이다.

     

Designed by Tistory.