조컴퓨터

#MVC 프레임워크 만들기 - v1, v2, v3, v4 본문

공부/Spring

#MVC 프레임워크 만들기 - v1, v2, v3, v4

챠오위 2022. 1. 27. 15:22

###뇌 정리용 글###

 


FrontController 패턴 도입 (=DispatcherServlet) - v1

내부 로직은 기본 서블릿과 거의 같다. 아래 그림의 1, 2번 작업을 진행할 프론트 컨트롤러만 생겨났다. 

v1 구조

 


View 분리 - v2

모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고, 깔끔하지 않다.

아래의 중복 코드를 깔끔하게 분리하기 위해 별도로 뷰를 처리하는 객체를 만든다.

String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

v2 구조

 

*MyView

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);
    }
}

MyView.render() 를 실행하기 위한 코드를 만든다.

RequestDispatcher 의 forward() 메서드는 대상 자원으로 제어를 넘기는 역할을 한다. 브라우저에서 /a.jsp 로 요청했을 때 /a.jsp 에서 forward() 를 실행하여 /b.jsp 로 제어를 넘길 수 있다. 제어를 넘겨받은 /b.jsp 는 처리 결과를 최종적으로 브라우저에 출력한다. 브라우저 입장에서는 /a.jsp 를 요청했지만 받은 결과는 /b.jsp 가 된다. 이 모든 것은 HTTP 리다이렉트 방식과는 달리 하나의 HTTP 요청(Request) 범위 안에서 동작이 이루어 진다.

 

 


Model 추가 - v3

컨트롤러로 넘어가는 서블릿 기술들을 제거하는 단계이다.

이때 request 객체를 Model 로 사용하는 대신에 별도의 Model 객체를 만들어서 반환한다. 

이 방법은 구현 코드가 매우 단순해지고, 테스트 코드 작성 또한 쉬워진다.

v3 구조

 

ModelView

지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest 를 사용했다. 그리고 Model 도 request.setAttribute() 를 통해 데이터를 저장하고 뷰에 전달했다. 서블릿의 종속성을 제거하기 위해 Model 을 직접 만들고, 추가로 View 이름까지 전달하는 객체를 만들어야 한다.

 

v3 구조에서는 컨트롤러에서 HttpServletRequest 를 사용할 수 없다. 따라서 직접 request.setAttribute() 를 호출할 수 없기 때문에 Model 이 별도로 필요하다.

 

@Getter @Setter
public class ModelView {

    private String viewName;
    private Map<String, Object> model = new HashMap<>();

    public ModelView(String viewName) {
        this.viewName = viewName;
    }
}

 

ControllerV3

public interface ControllerV3 {
    ModelView process(Map<String, String> paramMap);
}

HttpServletRequest 가 제공하는 파라미터는 프론트 컨트롤러가 paramMap 에 담아서 호출해주면 된다. 응답 결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView 객체를 반환하면 된다.

 

 

FrontControllerServletV3

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {

    // key: 매핑 URL, value: 호출될 컨트롤러
    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");

        String requestURI = request.getRequestURI();

        ControllerV3 controller = controllerV3Map.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // paramMap
        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);

        String viewName = mv.getViewName(); // 논리이름 new-form

        // "/WEB-INF/views/new-form.jsp"
        MyView view = viewResolver(viewName);
        view.render(mv.getModel(), request, response);
    }

    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    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;
    }
}

 

*MyView 

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);
}

v3 부터는 뷰 객체의 render() 는 model 정보도 함께 받는다.

 

 


단순하고 실용적인 컨트롤러 - v4

v3 컨트롤러는 잘 설계된 컨트롤러이다. 그러나 ModelView 객체를 생성하고 반환해야 하는 부분에 있어서는 실용성이 떨어진다.

 

이를 효율적으로 개선한게 v4 버전이다.

v4 구조

기본적인 구조는 v3 와 같지만, 컨트롤러가 ModelView 를 반환하지 않고, ViewName 만 반환한다. 

 

 

ControllerV4

public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

인터페이스에 ModelView 가 없다. model 객체는 파라미터로 전달되기 때문에 그냥 사용하면 되고, 결과로 뷰 이름만 반환해주면 된다.

 

 

FrontControllerServletV4

@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();

        ControllerV4 controller = controllerV4Map.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

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

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

        MyView view = viewResolver(viewName);
        view.render(model, request, response);
    }

    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    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;
    }
}

v3 와 거의 비슷하다.

 

 

모델 객체 전달

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

모델 객체를 프론트 컨트롤러에서 생성해서 넘겨준다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨 있게 된다. 

 

 

 

 

참고)

1. 김영한 님의 '스프링 MVC 1편 - 백엔드 웹 개발 핵심'에서 MVC 프레임워크 만들기

2. 코끼리를 냉장고에 넣는 방법 :: [서블릿/JSP] RequestDispatcher란. RequestDispatcher로 forward() 하기 (tistory.com)

 

[서블릿/JSP] RequestDispatcher란. RequestDispatcher로 forward() 하기

참고글 [서블릿/JSP] JSP 리다이렉트로 페이지 이동시키기 [서블릿/JSP] JSP 기본객체 종류 [HTTP] 리다이렉트(Redirect)란? RequestDispatcher란 RequestDispatcher는 클라이언트로부터 최초에 들어온 요청을 JSP..

dololak.tistory.com

 

 

 

'공부 > Spring' 카테고리의 다른 글

#스프링 MVC - 구조 이해  (0) 2022.01.27
#MVC 프레임워크 만들기 - 유연한 컨트롤러 - v5  (0) 2022.01.27
MVC 패턴  (0) 2022.01.26
기본 생성자를 선언하는 이유  (0) 2022.01.25
HTTP 응답 데이터  (0) 2022.01.25