3️⃣   V3

🤔💭 가독성이 좀 나아진 것 같아. 그런데 viewPath 의 “/WEB-INF/views” 경로는 항상 겹치는데 이걸 공통 로직으로 만들 수 없을까? 그리고 request 대신 다른 Model 을 사용하면 서블릿에 대한 종속성을 없앨 수 있을 것 같은데…

출처: 김영한의 스프링MVC (인프런)

💡 이번에는 각 컨트롤러에서 Model 과 viewPath 를 둘 다 반환하게 할 것이다. 그리고 viewPath 에서 중복되는 부분을 최소화하기 위해 uri 의 논리적 부분(각 uri 를 구분하기 위한 최소의 데이터)만 넘기게 할 것이다 !

⚡️   3-1. ModelView 와 MyView

  • Model 과 View 역할을 수행할 클래스를 만들어준다.

  • Model 역할을 수행할 새로운 HashMap 객체가 생겼으니 V2에서 사용했던 MyView 클래스에 render 함수를 오버로딩 (매개변수에 Map 객체를 추가함) 하여 새로 V3용 render 함수를 만들어준다.


public class ModelView {

    //uri 의 논리적 부분을 viewName 으로 저장한다.
    private String viewName;
    //Model 로 활용하기 위한 HashMap 객체를 만들어준다.
    private Map<String, Object> model = new HashMap<>();

    //생성자
    public ModelView(String viewName) {
        this.viewName = viewName;
    }

    //getter setter
    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;
    }
}

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

    public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        //jsp는 getAttribute로 값을 가져오기 때문에 request 객체로 값을 세팅해줘야한다.
        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));
    }
}

⚡️   3-2. ControllerV3

  • 모델로 request 객체를 사용할 수 없게 되어서 HashMap 으로 paramMap 객체를 생성해서 각 컨트롤러에서 생성되는 파라미터값을 받아오게 만들었다.

public interface ControllerV3 {

    //그간 Model 용도로 썼던 request 객체를 대신하기 위해 paramMap 객체를 만들었다.
    //process 메서드를 실행하면 ModelView 객체를 반환하도록 짜여짐.
    ModelView process(Map<String, String> paramMap);
}

⚡️   3-3. FrontControllerServletV3

  • uri 와 컨트롤러를 매핑하는 기능을 제공하는 것에는 변함이 없으며, 각 컨트롤러에서 paramMap, viewPath 정보를 담은 ModelView 객체를 반환하면 render 함수를 실행시켜서 브라우저로 전송해준다.

  • 또한, 논리적 데이터만 담긴 viewPath 앞 뒤로 경로 및 확장자명을 달아주는 viewResolver 메소드를 추가로 달았다. 이렇게 하면 경로의 수정이 생겨도 프론트 컨트롤러의 해당 메서드 값만 변경해주면 되어 유지보수의 편의성이 높아진다.


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

        //paramMap 을 넘겨줘야 함
        //request getparameterNames 로 username, age 를 가져옴.
        //iterator로 돌리면서 key에 name, value에 파라미터값을 paramMap에 넣어줌.
        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);

        String viewName = mv.getViewName();//여기에는 논리 이름만 들어있다 (new-form)
        //그래서 앞에 경로를 추가해준다. 컴퓨터가 찾아갈 수 있도록 !
        MyView view = viewResolver(viewName);

        //이번에는 request 객체로 파라미터값을 넘겨줄 수 없어서 Model 객체를 넘겨준다.
        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;
    }
}

⚡️   3-4. 구현체 컨트롤러들…


public class MemberFormControllerV3 implements ControllerV3 {

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

}


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

public class MemberListControllerV3 implements ControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {
        //findAll() 메서드는 Member 객체를 반환하도록 코드를 짰다. (멤버객체의 필드값은 id, username, age 가 있음)
        List<Member> members = memberRepository.findAll();

        //modelView 객체 mv 를 생성 (생성자로 viewName 필드값에 members 가 들어간다. 이게 jsp경로가 된다.)
        ModelView mv = new ModelView("members");

        //mv.getModel() : modelView 객체의 필드인 model (Map<String, Object>) 을 받아온다.
        //Map 형태의 자료구조이므로 put 메서드로 key값 : members, value값 : members 라는 이름의 List 타입 데이터를 넣는다.
        mv.getModel().put("members", members);

        //이렇게 되면 mv객체에는 모든 Member 객체가 들어가게 된다. 이것을 리턴한다.
        return mv;
    }
}

🤔💭 구현체 컨트롤러들의 코드가 단순해져서 좋긴 한데, 항상 ModelView 객체를 생성해야 한다는 점이 조금 불편하네..?