들어가며

  • 저번 포스트에서는 View를 컨트롤러에서 분리하는 작업을 수행하였다.
  • 이번에는 서블릿에 종속적인 코드들을 걷어내고 그 이후에 새로운 Model이라는 컴포넌트를 도입하여 어떻게 변화되는지를 알아본다.
  • 모델을 추가한 전체적인 코드는 여기에서 확인할 수 있다.
 

GitHub - ForteEscape/MVC-Study

Contribute to ForteEscape/MVC-Study development by creating an account on GitHub.

github.com

 

 

개선 방향

서블릿 종속성 제거

  • 다음의 코드를 보자
public class MemberFormControllerV2 implements ControllerV2 {
    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        return new MyView("/WEB-INF/views/new-form.jsp");
    }
}
public class MemberSaveControllerV2 implements ControllerV2 {

    private static final MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String userName = request.getParameter("username");
        int userAge = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(userName, userAge);
        memberRepository.save(member);

        request.setAttribute("member", member);
        
        return new MyView("/WEB-INF/views/save-result.jsp");
    }
}
  • 이 코드들은 저번 포스트에서 구현했던 버전 2 컨트롤러이다.
  • 그런데 잘 보면 내부의 어디에도  request, response를 찾아볼 수 없다. 저장에도 request를 뷰에 전달하기 위한 모델의 역할로 사용하고 있다.
  • 이 말은 적절한 Model만 구현하여 파라미터로 넣어준다면 서블릿 종속성을 제거할 수 있다는 것이다,

View 이름 중복 제거

  • 모든 컨트롤러의 MyView 생성을 보면 "/WEB-INF/views"와 ".jsp"가 중복되어 나타난다.
  • 중복되어 나타나는 것은 하나로 뽑아내어 제거하는것이 효율이 좋기 때문에 중복된 것들을 지우고, 컨트롤러는 오직 중간에 들어가는 뷰의 논리적 이름만 반환하도록 한다.
  • 해당 방식을 적용하면 중복되는 스트링이 제거되어 불필요한 타이핑을 줄일 수 있다는 것 이외에도, 뷰의 전체적인 경로가 재설정되어도 해당 부분만 다시 적어두면 된다는 것이다. 즉 변경하는 곳들이 적어진다.

 

 

 

개선된 구조

  • 이전과는 조금 달라진 점들이 보인다.
  • 바로 ModelView와 ViewResolver의 존재이다.
    • ModelView는 말 그대로 Model과 View를 가지고 있는 객체이다. 이때 View는 컨트롤러에서 반환한 뷰의 논리적 이름으로 String 타입이 될 것이다.
    • ViewResolver는 위에서 설명한 중복된 경로 스트링과 ModelView에서 넘어오는 View의 논리적 이름을 조합하여 새로운 MyView 객체를 반환하는 책임을 가진다.
  • 프론트 컨트롤러는 View Resolver에서 반환받은 MyView를 랜더링하여 JSP를 랜더링하고 이를 Http 응답으로 반환하게 된다.

 

 

 

구현

ModelView 구현

  • 먼저 위 구조의 핵심적인 역할을 맡을 ModelView 객체부터 구현하도록 한다.
@Getter
@Setter
public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();

    public ModelView(String viewName) {
        this.viewName = viewName;
    }
}
  • 위에서 말한 것 처럼 컨트롤러에서 넘어오는 뷰의 논리적 이름을 viewName으로 가지고, 뷰에 넘겨줄 데이터들을 가지고 있는 Model을 가진다.
  • 모델은 key, value로 이루어지도록 Map을 사용하여 구현하였고 value에는 여러가지 값이 들어갈 수 있기 때문에 Object로 구현하였다.
  • 이제 각 컨트롤러들은 이 ModelView를 반환하도록 하면 된다.

Controller 구현

public interface ControllerV3 {

    ModelView process(Map<String, String> paramMap);
}
public class MemberFormControllerV3 implements ControllerV3 {
    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form");
    }
}
public class MemberListControllerV3 implements ControllerV3 {

    private static final MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {

        List<Member> members = memberRepository.findAll();

        ModelView modelView = new ModelView("members");
        modelView.getModel().put("members", members);

        return modelView;
    }
}
public class MemberSaveControllerV3 implements ControllerV3 {

    private static final 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 = memberRepository.save(new Member(userName, age));

        ModelView modelView = new ModelView("save-result");
        modelView.getModel().put("member", member);

        return modelView;
    }
}
  • paramMap은 요청에서 넘어오는 데이터들로, 프론트 컨트롤러에서 한번 가공되어 넘어온다. 각 컨트롤러는 이 paramMap에서 필요한 데이터를 조회하여 가져다 사용하기만 하면 된다.
  • Model은 modelView에 있는 모델을 사용한다.
  • 수정된 코드가 매우 깔끔하다고 생각할 수 있다. 이유는 사용하지 않는 서블릿 관련 코드들을 들어내면서 함수 파라미터 부분이 깔끔하게 되었기 때문이다.

FrontController 구현

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
    private final 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 controllerV3 = controllerMap.get(requestURI);
        if (controllerV3 == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        Map<String, String> paramMap = createParamMap(request);
        ModelView modelView = controllerV3.process(paramMap);

        MyView view = viewResolver(modelView.getViewName());
        view.render(modelView.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;
    }
}
  • 프론트 컨트롤러에 새로 구현된 것들이 많은데 하나씩 살펴보도록 하자
  • 먼저 paramMap은 위에서 설명한 것 처럼 요청에서 넘어오는 데이터들을 모두 가지고 있는 일종의 모델이다.
    • 이 paramMap을 초기화 시키는 행동 자체가 꽤 큰 행동이기 때문에 이를 별개의 메서드로 빼내어 인식하기 쉽도록 하였다.
  • 그리고 새로 보이는게 viewResolver가 있는데 위의 구조 단에서 설명했던 그 ViewResolver이다. 보면 컨트롤러를 호출하고 얻은 ModelView에서 View의 논리적 이름을 가져와 MyView로 만들어 반환하는 것을 볼 수 있다.
  • view의 render가 이전과는 조금 다른 render() 메서드가 있는데 코드를 보면서 확인해보자.

View 변경점

public class MyView {
   ...

    public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        modelToRequestAttribute(model, request);
        request.getRequestDispatcher(viewPath).forward(request, response);
    }

    private static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
        model.forEach(request::setAttribute);
    }
}
  • 보면 request에 setAttribute를 하는 동작을 먼저 수행한다. 이는 우리가 ModelView에서 받은 Model을 그대로 뷰에 넘겨줄 수 없기 때문이다.
  • 따라서 request에 Model의 데이터들을 하나씩 매핑해주고 foward를 통해 응답을 보내준다.

 

 

결과

  • 개발자는 이제 각 컨트롤러를 구현할 때 마다 서블릿 관련 코드들을 삽입해야할 필요성이 사라졌다.
  • 이제 컨트롤러를 개발하는 개발자는 ModelView를 만들어 그 내부에 자신이 반환해야 할 View의 논리적 이름과 View에 넘겨줄 모델만 넣어주고 반환하면 된다. 기존에 request에 하나씩 데이터를 넣는 작업이나, 뷰의 중복된 이름을 적는 불편함이 사라진 것이다.
  • 다음 포스트에서는 약간의 접근 방식을 바꾸어 더 편리하게 컨트롤러를 만들 수 있도록 해볼 것이다.
복사했습니다!