스프링 MVC - Model 분리 - V3

최지환·2022년 4월 10일
0

스프링

목록 보기
3/12
post-thumbnail

스프링 MVC - Model 분리

기존의 코드인 V2를 확인해보자.

MemberFormControllerV2

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

MemberListControllerV2

public class MemberListControllerV2 implements ControllerV2 {
    private 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");
    }
}

MemberSaveControllerV2

public class MemberSaveControllerV2 implements ControllerV2 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, **HttpServletResponse response**) throws ServletException, IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));
        Member member = new Member(username, age);
        memberRepository.save(member);
        request.setAttribute("member", member);

        return new MyView("/WEB-INF/views/save-result.jsp");
    }
}

자세히 보면 process 함수에서 파라미터로 받은 HttpServletResponse 타입의 response를 전혀 사용하지 않는다.

process함수 실행 시 , response 를 받긴 하지만, 함수 로직에서 이를 전혀 사용하지 않기 때문에, 보기 좋은 코드는 아니다. 또한 HttpServletRequest request는 단순 정보를 뽑기 위해 사용한다.

또한 각 컨트롤러의 return 부분을 확인해 보면, 뷰 경로에 “/WEB-INF/views/” 부분이 중복 됨을 확인 할 수 있다. → 중복은 지양해야한다.

문제 1. 서블릿 종속성

컨트롤러 입장에서 보았을 때, HttpServletRequest와 HttpServletResponse가 필요할까?

굳이 필요하지는 않다. 컨트롤러는 단지 요청 파라미터의 ‘정보'가 필요하다. 따라서 이런 정보를 자바의 Map을 이용해 넘겨 받는다면, 파라미터로 굳이 사용하지 않는 범주의 값을 받을 필요가 없다.

만약 단순 자바 코드(Map)로 파라미터의 ‘정보'를 넘겨줄 수 있다면 컨트롤러 입장에서는 굳이 서블릿 기술(HttpServletRequest 나 HttpServletResponse)에 대해 몰라도 된다.

따라서 우리는 Request 객체를 Model로서 사용하는 대신에 Model에 대한 처리를 하는 Model객체를 만들어주면 된다.

문제2. 뷰 이름 중복 - 문제

각 컨트롤러의 return 부분을 확인해 보면, 뷰 경로에 “/WEB-INF/views/” 부분이 중복 됨을 확인 할 수 있다. → 해결법으로는 뷰의 논리 이름을 컨트롤러에서 반환하고, 실제 물리 위치에 대한 경로는 프론트 컨트롤러에서 처리하도록 단순화 하면된다.

ex) 각 컨트롤러가 new-form, save-result,members와 같이 논리이름을 반환하도록 수정

  • /WEB-INF/views/new-form.jsp ⇒ new-form
  • /WEB-INF/views/save-result.jsp ⇒ save-result
  • /WEB-INF/views/members.jsp ⇒ members

컨트롤러가 논리 경로를 반환하여, 프런트 컨트롤러에서 이에 대한 처리를 해주는 로직으로 바뀐다면, 뷰의 폴더위치가 바뀌더라도 프런트 컨트롤러만 수정해주면 된다.

예상 구조

순서

  1. 클라이언트가 Http 요청을 Front Controller에게 보냄
  2. Front Controller는 매핑 정보를 기반으로 요청을 처리할 수 있는 컨트롤러가 있는지 조회
  3. 조회한 컨트롤러를 호출, 이때 호출 된 컨트롤러는 ModelView 객체(논리 경로, Map형태의 처리한 정보)를 반환해줌
  4. Front Controller 는 viewResolver 메소드(논리 경로를 절대 경로로 변환 로직)를 호출하여 MyView 객체 생성
  5. Front Controller에서 render를 호출하여 뷰에게 모델(처리한 정보)와 request와 Response를 넘겨줌 → 이때 render는 입력 받은 정보(Map)를 다 꺼내어 JSP에게 전달
  6. HTML 응답

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

ModelView ⇒ (논리 경로, Map형태의 처리한 정보)를 반환해줌.
Model은 모델 역할과 view경로 데이터를 저장하는 역할을 함. → View를 생성해주는 것이 아님을 주의!!!!

MyView 소스코드

public class MyView {
    private final 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 {
        modelToRequestAttribute(model, request);
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }

    private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
        model.forEach((key, value) -> request.setAttribute(key, value));
    }
}

각 컨트롤러 소스 코드

MemberFormControllerV3

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

MemberListControllerV3

public class MemberListControllerV3 implements ControllerV3 {
    private 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;
    }
}

MemberSaveControllerV3

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

Front Controller에서 Controller에게 request 정보를 Map형태로 전달해주면, Controller는 이를 처리하고, Map형태의 처리 결과 정보와, String 형태의 viewName을 담은 ModelView 객체를 반환

FrontController 소스 코드

@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;
        }
        **//paraMap 넘겨줘야함 -> why? 컨트롤러들에게 HttpServeltRequest와 HttpServletResponse를 넘겨주지 않기위해
        Map<String, String> paraMap = createParamMap(request);
        ModelView mv = controller.process(paraMap);
        String viewName = mv.getViewName();//논리 이름만 알고 있는 상태
        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> paraMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paraName -> paraMap.put(paraName, request.getParameter(paraName)));
        return paraMap;
    }
}

Bold 체로 타이핑 된 곳이 이번 버전의 핵심 로직이다.

변경 시 알아야할 점

  • Map<String, String> paraMap = createParamMap(request);

    createParamMap을 이용하여, 컨트롤러에게 Map 형태의 Request 정보만을 넘겨주는 것이다.
    → HttpServeltRequest와 HttpServletResponse와 같은 서블릿 기술을 list, form ,save 컨트롤러는 몰라도 된다.

  • ModelView mv = controller.process(paraMap);
    String viewName = mv.getViewName();//논리 이름만 알고 있는 상태

    컨트롤러를 통해 ModelView객체 를 반환 받음.
    → 이를 통해 front Controller는 호출해야하는 view의 논리 이름을 알고 있고 model(처리 완료된 데이터)를 갖고 있는 상태임.

  • MyView view = viewResolver (viewName);
    view.render(mv.getModel(),request, response);

    viewResolver함수 호출을 통해, 생성해야하는 view의 논리경로를 절대경로로 변경하고, MyView객체 생성함. 그리고 render 실행 -> 이후 render에서 모델의 데이터를 꺼내 jsp에게 전달하고 이를 포워드 해서 렌더링 한다.


요약

ModelView 객체 생성을 통해, 서블릿 종속성 문제와 뷰 이름 반환 중복 코드 문제를 해결 할 수 있음.

Model View Controller의 분리를 발전 시킬 수 있음.

하지만 fornt Controller가 각 controller에게 메시지를 요청하고 이에 대한 값을 받을 때마다 ModelView 객체를 생성해 받는 과정은 번거롭고 불편하다. 다음에는 이를 발전 시켜보자.

0개의 댓글