[스프링 MVC] MVC 프레임워크 만들기 (2)


스프링 MVC의 동작 원리와 개발 히스토리를 이해하고자 MVC 프레임워크를 구현해보며 작성한 포스팅이며, 모든 포스팅의 내용은 김영한 님의 ‘스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술’ 강의를 참고하여 작성합니다.

base commit

해당 포스팅에서 작성하는 모든 내용은 위 base commit을 기반으로 해당 repository에 구현됨


이전 포스팅에서 MVC 이전 포스팅에서 서블릿과 프론트 컨트롤러 패턴을 이용해서 기본적인 MVC 프레임워크를 구현했었다. 그러나 해당 버전에서는 몇 가지 문제가 있다.

서블릿 종속성 문제

해당 버전에서는 모든 컨트롤러가 HttpServletRequest, HttpServletResponse를 파라미터로 전달받으며, 서블릿에 의존한다는 문제가 있다.

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

위와 같이 컨트롤러가 HttpServletRequest, HttpServletResponse를 파라미터로 전달받도록 구현했었는데, 사실 컨트롤러에서 필요한 건 해당 인스턴스들이 아닌 Request에 포함된 파라미터 정보이다. 따라서 파라미터 정보만 메소드의 인자로 넘겨주면, 컨트롤러 개발자는 서블릿에 대해서 알지 못해도 컨트롤러의 구현이 가능하다. (Model 객체를 이용해서 View에 데이터를 전달한다.)

컨트롤러가 서블릿에 의존하지 않을 경우 구현 코드도 단순해지고.. 무엇보다 테스트 코드 작성이 쉬워진다.

뷰 이름 중복 문제

컨트롤러에서 지정하는 View 이름에 중복이 있다. View의 경로 이름이 바뀔 경우 모든 컨트롤러가 수정되어야 하고, 모든 컨트롤러마다 중복해서 공통 경로를 작성해줘야 한다는 점이 번거롭다. 컨트롤러는 아래처럼 View의 이름만 반환하도록 하고, 공통 경로는 FrontController에서 처리하도록 수정한다.(ViewResolver) "/WEB-INF/views/new-form.jsp -> new-form.jsp

V3 구조

  • 1

3. Model/ModelView 사용

이전에는 컨트롤러에서 View로 데이터를 전달하기 위해 HttpServletRequest.setAttribute()를 사용했었다. 즉, 서블릿에 종속적인 객체를 이용한건데, 이 때문에 모든 컨트롤러가 서블릿에 의존한다는 점이 문제였다.

서블릿을 사용하지 않고 View에 데이터를 전달하고, 어떤 뷰를 사용할지 FrontController에게 알려줄 수 있는 클래스를 다음과 같이 정의한다.

// ModelView.java
public class ModelView {
    private String viewName;

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

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

    public String getViewName() {
        return viewName;
    }

    public void setViewName(String viewName) {
        this.viewName = viewName;
    }

    public Map<String, Object> getModel() {
        return model;
    }

    public void setModel(Map<String, Object> model) {
        this.model = model;
    }
}

그리고 모든 컨트롤러가 새로 정의한 위의 MyView 클래스의 인스턴스를 반환하도록 인터페이스를 수정해준다. 이를 통해 컨트롤러는 View의 이름을 MyView.viewName 클래스의 필드로 넣어주고, View에 전달할 데이터는 MyView.model을 통해 전달해주면 된다.

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

수정한 인터페이스를 바탕으로 컨트롤러를 새롭게 정의해준다. Controller에서 View에 전달할 데이터(model)을 ModelView에 담아서 FrontController에게 반환해준다.

// MemberListControllerV3.java
public class MemberListControllerV3 implements ControllerV3 {
    private final MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public ModelView process(Map<String, String> paramMap) {
        List<Member> members = memberRepository.findAll();
        ModelView mv = new ModelView("members");
        mv.getModel().put("members", members);
        return mv;
    }
}

HttpServletRequest를 컨트롤러에서 더 이상 사용하지 않기로 했으므로, 사용자의 요청 파라미터(paramMap)를 request로부터 꺼내와서 컨트롤러에 전달해준다.

FrontControllerServlet에서는 컨트롤러로부터 전달받은 ModelView 인스턴스로부터 View를 reslove하고(MyView 인스턴스 생성), 각 View의 render() 메소드를 호출하여 Forwarding을 위임한다.

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

    private Map<String, ControllerV3> controllerMap = new HashMap<>();

    public FrontControllerServletV3() {
        controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
        controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
        controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        ControllerV3 controller = controllerMap.get(requestURI);

        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        Map<String, String> paramMap = createParamMap(request);  // 컨트롤러의 서블릿 의존성 제거를 위해 파라미터 부분만 따로 추출 및 전달
        ModelView mv = controller.process(paramMap);
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);
        view.render(mv.getModel(), request, response);
    }
    ...

4. viewResolver 사용

컨트롤러가 반환한 논리 view의 이름을 실제 물리 View에 매핑되는 절대경로로 변환이 필요하다. 변환이라고는 했지만 절대경로를 만들어준다고 생각하면 된다. 이를 viewResolver가 처리하며, 메소드로 분리해서 위의 FrontControllerV3에 함께 정의해준다.

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





© 2020.02. by blupine