ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • (3) MVC 프레임워크 만들기 - Controller 구현체들의 공통로직 처리-2
    Spring/MVC1-Servlet vs JSP vs MVC패턴 2023. 6. 13. 20:01
    728x90

    이전단계링크

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

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

    https://hongs429-blog.tistory.com/46 (1) MVC 프레임워크 만들기 - FrontController의 도입 Servlet -> JSP -> 고전 MVC(Servlet + JSP) 의 과정을 거치면서 나름대로 기능별로 나누어 파일을 관리하였다. 하지만, 여전히

    hongs429-blog.tistory.com

     

     

    지금까지 한 작업은 FrontController에서 최대한 중복적인 코드를 처리하고, FrontController에서 mapping 된 각각의 Controller 구현체에서는 최대한 서비스 로직에만 집중할 수 있도록 구조화하고 있었다.

     

    이번 시간에도 동일하게 Controller 구현체의 중복을 제거하는 작업과 클라이언트의 요청에 대해 처리를 돕는 Servlet을 FrontController에서만 사용하여, 개발자가 Servlet을 모르더라도 서비스로직을 작성할 수 있도록

    Controller 구현체의 Servlet 종속성을 제거하는 작업을 진행할려고 한다.

     

     

     

    1. View 이름에서 반복적으로 사용되는 경로를 제거(일종의 ViewResolver 기능)

    // MemberFormController에서의 리턴값
    return new MyView("/WEB-INF/views/new-form.jsp");
    
    // MemberSaveController에서의 리턴값
    return new MyView("/WEB-INF/views/save-result.jsp");
    
    // MemberListController에서의 리턴값
    return new MyView("/WEB-INF/views/members.jsp");

    위의 코드는 Controller 구현체에서 return 해주는 값을 가지고 와봤다.

    보는 것처럼 현재는 모든 경로(물리적 경로)를 전부 지정해주어야 한다.

    지금부터 해줄 작업은 물리적 경로에서 중복이 일어나는 부분을 처리하여,

    개발자는 논리적 경로("members", "new-form", "save-result")만으로 view를 처리하도록 만들어줄 것이다.

     

     

    2. Controller 구현체의 Servlet 종속성 제거

    <이전버전의 Controller 구현체 내부 메소드의 파라미터>

    public class MemberSaveControllerV2 implements ControllerV2 {
    
        private MemberRepository memberRepository = MemberRepository.getInstance();
    
        @Override
        public MyView process(
        	HttpServletRequest request,	// HttpServletRequest 의 사용..
        	HttpServletResponse response // HttpServletResponse 의 사용..
        ) throws ServletException, IOException {...}

    현재까지 우리는 FrontController에서 @WebServlet 으로 Servlet 객체를 등록하고,

    HttpServlet을 상속받아 사용하고 있다. 이후, FrontController에서 특정 Controller와 mapping이 일어나고나면,

    사실상 Servlet 객체의 사용은 무의미해진다.즉, Servlet 객체의 사용이 필요없는 Controller 구현체를 실제로 servlet 객체의 종속성을 없애준다면, 해당 프레임워크를 사용하는 사용자는 servlet을 몰라도 프레임워크를 자유롭게 사용할 수 있게 된다. 우리는 Controller 구현체의 Servlet 종속성을 없애고자 ModelView라는 객체를 도입할 것이다.

    (실제 SpringBoot에서 ModelAndView와 동등한 기능을 하는 객체를 구현하려는 것이다.)

     

     

     

     

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

     

     

     

    회원저장, 회원가입이동, 회원전체 조회 기능 모두에게 적용되므로, 회원 저장 기능가지고 살펴보자.

     

    <이전 버전의 Controller 구현체>

    public class MemberSaveControllerV2 implements ControllerV2 {
    
        private MemberRepository memberRepository = MemberRepository.getInstance();
    
        @Override
        public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            String username = request.getParameter("username");
            int age = Integer.parseInt(request.getParameter("age"));
    
            Member member = new Member(username, age);
            memberRepository.save(member);
    
            request.setAttribute("member", member); // model 역할 : 데이터를 싣어 나르는 역할
    
            return new MyView("/WEB-INF/views/save-result.jsp");
        }
    }

    <새로운 버전의 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;
        }
    }

    ModelView 객체의 기능은 다음과 같다.

    • View의 물리경로("/WEB-INF/views/save-result.jsp") 가 아닌, 논리경로("save-result")를 저장
    • Controller 구현체에서 서비스 로직을 수행하고 난 뒤, 만들어진 데이터를 FrontController로 전달하는 역할

     

     

    위의 기능을 토대로 ModelView Class를 만들어보자.

    public class ModelView {
        private String viewName;
        private Map<String, Object> model = new HashMap<>();
    
        public ModelView(String viewName) {
            this.viewName = viewName;
        }
    
    	// ViewResolver로 보내기 위한 용도
        public String getViewName() {
            return viewName;
        }
    
    
        public void setViewName(String viewName) {
            this.viewName = viewName;
        }
    	
        
        public Map<String, Object> getModel() {
            return model;
        }
    	
        // 로직 실행 후, 데이터 전송을 위한 setter
        public void setModel(Map<String, Object> model) {
            this.model = model;
        }
    }

     

     

    <FrontController 보기>

    @WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
    public class FrontControllerServletV3 extends HttpServlet {
    
        private Map<String, ControllerV3> controllerV3Map = new HashMap<>();
    
        public FrontControllerServletV3() {
            controllerV3Map.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
            controllerV3Map.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
            controllerV3Map.put("/front-controller/v3/members", new MemberListControllerV3());
    
        }
    
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            System.out.println("FrontControllerServletV3.service");
    
    
    		// URL을 가지고 다형성을 통해 Controller 구현체를 mapping 하는 과정
            String requestURI = request.getRequestURI();
            System.out.println("requestURI = " + requestURI);
    
            ControllerV3 controller = controllerV3Map.get(requestURI);
    
            if (controller == null) {
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                return;
            }
            
            
            
    
            //paramMap
            // client 요청에 담긴 파라미터 맵을 생성하여, ModelView에 넘겨주기위함
            Map<String, String> paramMap = createParamMap(request);
            // String, String인 이유? > 웹에서의 요청 정보는 String으로 넘어온다
    
            // 드디어, 모델(데이터 전달 역할)을 만들어 mv 객체에 담아 놓는다.
            // 현재 논리 경로는 박혀 있고, Map<"key", "로직이 처리된 후의 리턴값(Object) ">을 mv객체에 세팅.getter 사용
            ModelView mv = controller.process(paramMap);
            // ↑↑↑
            // 여기까지 진행되었다면, 1. 각각의 컨트롤러 구현체마다 "논리 경로"를 박아놨고,
            //                      2. createParamMap(요청파라미터 내부 값 꺼내기)
            //                                    > mv의 Map<String, 로직실행결과 Object>으로 값을 가지고 있다
    
    
            String viewName = mv.getViewName();// 논리이름 new-form 꺼내기
            MyView view = viewResolver(viewName); // render할 수 있는 MyView 객체 생성
    
            // mv의 모델을 하나씩 돌며, request.setxxx을 실행 후,dispatcher로 forward메소드 실행하는작업수행
            view.render(mv.getModel(), 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;
        }
    }

    * MyView의 render 메소드는 이전에 비해서 ModelView객체의 model()을 넘겨받는 작업이 추가 되었다.

    이는 메소드의 overloading으로 처리하여주면 된다.

    public class MyView {
        private String viewPath;
    
        public MyView(String viewPath) {
            this.viewPath = viewPath;
        }
    
        public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
            dispatcher.forward(request, response);
        }
    
        // overloading
        public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            modelToRequestAttribute(model, request);
            RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
            dispatcher.forward(request, response);
    
        }
    
        private static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
            model.forEach((key, value) -> request.setAttribute(key, value));
        }
    }

     

     

    정리

    FrontController는 코드가 더 복잡해졌지만, 실제 개발자가 코드를 입력하는 Controller 구현체에서는 이전보다 훨씬 간편해진 것을 확인할 수 있다. 또한 View의 이름을 지정할 때, 물리적인 경로를 전부 작성하는 것이 아니라, 논리적인 경로(변경이 일어나는 부분)만 적어도 FrontController에서 ViewResolver 메소드를 통해 preffix + path + suffix 처리를 해주었다.

    또한, Controller 구현체 내부에서 Servlet을 사용하지 않고, ModelView 객체의 Map<>을 이용하여 데이터를 view까지 나를 수 있게 구성하였다.

    지금까지 한 내용을 그림으로 한번 다시 살펴보자

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

    여기까지가 현재의 Spring의 MVC패턴의 가장 근간이 되는 부분을 한번 만들어본 것이다.

    다음에는 최종적으로 매핑을 다양한 방법으로 Mapping할 수 있게 해주는 adapter의 개념을 적용해 볼 것이다.

     

     

    다음단계링크

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

    (4) MVC 프레임워크 만들기 - 유연한 FrontController 만들기

    이전단계링크 https://hongs429-blog.tistory.com/48 (3) MVC 프레임워크 만들기 - Controller 구현체들의 공통로직 처리-2 이전단계링크 https://hongs429-blog.tistory.com/47 (2) MVC 프레임워크 만들기 - Controller 구현체들

    hongs429-blog.tistory.com

     

Designed by Tistory.