들어가며

  • 저번 포스트에서는 프론트 컨트롤러를 도입하여 구현한 서블릿 구조를 보았다.
  • 이번 포스트에서는 각 컨트롤러에 묶여있는 뷰들을 어떻게 효과적으로 빼낼지에 대해 알아본다.
  • 모든 코드는 깃허브 링크에 올려저 있다.
 

GitHub - ForteEscape/MVC-Study

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

github.com

 

 

 

View의 분리

  • 지금까지 구현한 구조는 컨트롤러가 JSP로 포워딩하기 위해 JSP파일의 물리적 경로를 가지고 있었어야 했다.
  • 단적으로 다음의 코드를 보자.
public class MemberFormControllerV1 implements ControllerV1 {
    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String viewPath = "/WEB-INF/views/new-form.jsp";
        request.getRequestDispatcher(viewPath).forward(request, response);
    }
}
  • 보면 viewPath라는 변수가 우리가 표현할 View의 물리적 경로를 명시해두고 있고, 이를 이용하여 foward를 수행하고 있다.
  • 해당 부분이 모든 컨트롤러에 분산되어 있다. 이렇게 분산되어 있는 코드를 하나의 View 객체를 통하여 관리하도록 해보자. 이를 위해서는 새로운 View 클래스가 필요하다.
public class MyView{
    String viewPath;
    
    MyView(String viewPath){
        this.viewPath = viewPath;
    }
    
    public void render(
        HttpServletRequest request, HttpServletResponse response
    ) throws ServletException, IOException{
    
        request.getRequestDispatcher(viewPath).foward(request, response)
    }
}
  • 코드를 보면 위의 코드를 클래스로 구현한 것에 불과하다는 것을 알 것이다.
    • viewPath를 통해 객체를 생성하여 해당 객체가 뷰를 렌더링하여 반환하도록 하였다. 즉 이 MyView 객체는 jsp 뷰를 랜더링하여 반환하는 책임을 가진다.
  • 이제 이 MyView를 이용하여 컨트롤러들을 구현해보자.
public interface ControllerV2{
    void process(
        HttpServletRequest request, 
        HttpServletResponse response
    ) throws ServletException, IOException;
}
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 MemberListControllerV2 implements ControllerV2 {

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

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        request.setAttribute("members", members);
        
        return new MyView("/WEB-INF/views/members.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");
    }
}
  • request객체를 활용하여 뷰로 포워딩하는 부분을 삭제하고 대신 MyView 객체를 반환하도록 하였다.
  • 각 객체들은 자신들이 랜더링해야할 뷰의 물리적 경로를 가지고 있다. 이를 랜더링하는 책임은 바로 자신들을 호출하는 프론트 컨트롤러에서 맡아야 한다.
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV1 extends HttpServlet {
    private final Map<String, ControllerV2> controllerMap = new HashMap<>();

    public FrontControllerServletV1(){
        controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
        controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
        controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerV1.service");
        String requestURI = request.getRequestURI();

        System.out.println(requestURI);

        ControllerV2 controllerV2 = controllerMap.get(requestURI);
        if (controllerV2 == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyView myView = controllerV2.process(request, response);
        myView.render(request, response);
    }
}
  • 프론트 컨트롤러에서 이제 view를 랜더링한다. 이제 각 컨트롤러들이 뷰를 랜더링 할 필요가 없다. 그져 뷰 객체에 랜더링해야할 jsp의 물리적 경로만 넣어 반환하면 된다.

 

 

 

MyView로 변경된 점

  • 원래 뷰로 포워딩 하는 것은 각 컨트롤러들이 책임지던 것이었다. 그로 인해 각 컨트롤러마다 자신이 포워딩해야할 뷰의 물리적 경로를 가지고 있어야 했고 포워딩하는 함수도 호출하는 중복이 발생했다.
  • 이번 과정을 통해서 MyView라는 객체에 포워딩하는 책임을 넘기고, 컨트롤러는 MyView 객체만을 생성하여 반환하였다.
  • 이렇게 반환받은 MyView 객체는 프론트 컨트롤러가 해당 객체의 render() 메서드를 통해 보여줄 뷰로 포워딩하도록 요청하고, 각 MyView 객체들이 해당 요청을 받고 포워딩하는 책임(기능)을 수행한다.

 

 

 

개선해야 할 점

  • 각 뷰로 포워딩 하는 기능들을 제거하고 MyView에서 처리하도록 책임을 옮겼지만 아직 부족하다. 위의 코드를 보면 뷰의 위치는 항상 "/WEB-INF/views/something.jsp"로 앞의 "/WEB-INF/views"와 .jsp는 항상 동일하단 것을 알 수 있다.
  • 그렇다면 굳이 매번 번거롭게 타이핑할 필요 없이 해당 스트링을 미리 만들어 두고 컨트롤러는 뷰의 논리적 이름만 반환하도록 하면 조금 더 편리해질 것 같다.
public class MemberListControllerV2 implements ControllerV2 {

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

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        request.setAttribute("members", members);
        
        return new MyView("/WEB-INF/views/members.jsp");
    }
}
  • 그리고 이 코드를 보면 컨트롤러는 request, response를 받고 있는 모습인데 막상 해당 스펙들을 활용하는 코드는 단 한줄에 불과하다.
  • 심지어 다른 페이지로 넘어가는 컨트롤러의 경우 MyView로 인하여 request.getRequestDispatcher()가 호출되지도 않기 때문에 해당 스펙이 아예 사용되지도 않는다.
  • 위의 코드도 뷰에 전달할 모델(Model)의 역할로 request를 사용하고 있기 때문에 사용한다. 따라서 적절한 모델을 제공하도록 한다면 request, response가 아예 필요하지 않게 된다.
  • 다음 포스트에서 해당 개선 사항들을 고쳐보도록 한다.
복사했습니다!