본문 바로가기
웹 개발/Spring

[SpringMVC] 스프링 MVC의 구조를 이해해보자 (2/3)

by 어썸오184 2021. 7. 9.
728x90
반응형

V2: JSP 포워드 중복 제거

이전 포스팅에서 단순히 프론트 컨트롤러만 도입한 V1 을 만들어봤다. 이때는 단순히 프론트 컨트롤러를 도입하는 것에 의의를 두고 딱히 설계상 진보된 점은 없었다. 이번에는 V1에서 조금 더 진보된 형태인 V2를 만들어본다.

이번에는 V1에 화면을 그리는 책임을 가지는 오브젝트인 View를 도입해서 JSP를 포워드하는 코드의 중복을 없애보도록 하겠다. 아래의 코드 말이다.

RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);
requestDispatcher.forward(request, response);

V2의 구조는 아래와 같다

이번에는 ControllerView를 반환하는데, Controller는 View에 viewPath를 넘겨주고 View에서 jsp를 포워드한다. 그리 어렵지 않다.

public class View {

    String viewPath;

    // Controller가 View를 생성하면서 viewPath를 넘겨줄 것이다.
    public View(String viewPath) {
        this.viewPath = viewPath;
    }

    public void render(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);
        requestDispatcher.forward(request, response);
    }
}

Controller는 기존의 로직을 그대로 처리하되, View를 반환하고 FrontController는 반환받은 View에 대해 render()를 호출한다.

public interface Controller {
    View process(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException;
}
public class MemberSaveController implements Controller {

    private final MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public View 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);

        // viewPath를 넘겨주면서 View 인스턴스를 반환
        return new View("/WEB-INF/views/save-result.jsp");
    }
}

FrontController는 이제 Controller에서 view를 반환받고 render를 호출한다.

FrontController.java

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

    if (controller == null) {
        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        return;
    }

    View view = controller.process(request, response);
    view.render(request, response);
}

이게 끝이다. 기존 Controller들의 코드와 비교해보면 requestDispatcher를 생성하고 forward를 호출하는 코드가 제거되었다. 중복이 사라진 것이다.

V1의 MemberFormController.java

public class MemberFormController implements Controller {

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

        String viewPath = "/WEB-INF/views/form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

V2의 MemberFrontController.java

public class MemberFormController implements Controller {

    @Override
    public View process(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        return new View("/WEB-INF/views/form.jsp");
    }
}

V3: 서블릿 종속성, viewPath 중복 제거

서블릿 종속성 제거

컨트롤러의 입장에서 HttpServletRequest와 HttpServletResponse가 꼭 필요할지 고민해볼 필요가 있다.

컨트롤러가 request를 필요로 하는 이유를 생각해보자. MemberSaveController를 기준으로 생각해보면, request의 요청 파라미터에서 “username”과 “age”를 꺼내고 Member 객체를 생성한다. 생성한 멤버 정보를 다시 request.setAttribute(member)를 호출해서 View가 화면을 그리는데 필요한 멤버 객체를 전달해준다.

그럼 request 객체를 Model로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 서블릿 종속성을 제거할 수 있지 않을까?

viewPath 중복 제거

viewPath를 보면 앞에 “/WEB-INF/views”와 뒤에 “.jsp”가 중복되어있다. 컨트롤러는 핵심이 되는 논리 이름만 반환하고 실제 물리 위치의 이름은 프론트 컨트롤러가 처리하도록 하면 중복을 없앨 수 있다. 중복이 사라지면 나중에 뷰의 위치가 변경되거나 뷰 템플릿이 바뀌더라도 컨트롤러를 수정할 필요없이 프론트 컨트롤러 하나만 수정하면 되기 때문에 유지 보수성이 좋아진다.

  • /WEB-INF/views/form.jsp —> form
  • /WEB-INF/views/save-result.jsp —> save-result
  • /WEB-INF/views/members.jsp —> members

V3의 구조는 다음과 같다.

코딩하기 전에 프로세스를 대략적으로 서술하면 다음과 같다.

  1. Controller에서 서블릿의 종속성을 제거하기 위해 FrontController는 미리 요청 파라미터 정보를 꺼내 Map 객체에 세팅한 후 Controller에게 paramMap이라는 이름으로 전달할 것이다.
  2. Controller는 paramMap에서 필요한 파라미터를 꺼내 비즈니스 로직을 수행한 후, View에 전달할 정보를 담은 ModelAndView라는 객체를 반환한다.
  3. FrontController는 다시 ModelAndView에서 뷰의 논리적 이름인 viewName을 얻어 viewResolver라는 메서드를 호출해 View를 생성한다.
  4. 생성된 View에 Controller에서 세팅된 model을 전달하고 View는 model에서 정보를 꺼내 request.setAttribute()로 request 객체에 값을 넣는다. 그 후 JSP forward를 호출한다.

우선 Controller에게 전달할 파라미터 정보를 Map 객체에 담아 전달한다.

FrontContoller.java

...

@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {

    ...
    // paramMap 세팅
    Map<String, String> paramMap = new HashMap<>();
    request.getParameterNames().asIterator()
        .forEachRemaining(
            paramName -> paramMap.put(paramName, request.getParameter(paramName)));

    ...
}

예를 들어 요청 파라미터로 username=awesomeo184, age=25 가 오면

paramMap.put("username", "awesomeo184");
paramMap.put("age", "25");

이렇게 담기는 것이다.

Controller는 전달받은 paramMap에서 필요한 파라미터를 꺼내 비즈니스 로직을 수행한 후, View에 전달할 정보를 담은 ModelAndView 객체를 반환해야 하는데, 이 ModelAndView라는 오브젝트는 뷰가 화면을 그리기 위한 model과 뷰의 논리적 이름인 viewName을 가진 오브젝트이다.

public class ModelAndView {

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

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

    public String getViewName() {
        return viewName;
    }

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

Controller는 로직을 수행한 후 ModelAndView 객체를 반환한다.

public interface Controller {
    ModelAndView process(Map<String, String> paramMap);
}

MemberSaveContorller.java

public class MemberSaveController implements Controller {

    private final MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public ModelAndView 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);

        ModelAndView mv = new ModelAndView("save-result");
        mv.getModel().put("member", member);
        return mv;
    }
}

FrontController는 Controller로부터 ModelAndView를 반환받아서 viewName을 꺼내 View를 생성한다.

FrontContoller.java


@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {

    ...
    // 컨트롤러에 paramMap을 넘겨주고 ModelAndView 객체를 넘겨받음
    ModelAndView mv = controller.process(paramMap);
    String viewName = mv.getViewName();
    View view = viewResolver(viewName);
    ...
}

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

View는 model을 넘겨받아 request 객체에 화면을 그리기 위한 객체를 전달하고 JSP forward를 호출한다.

public class View {

    String viewPath;

    public View(String viewPath) {
        this.viewPath = viewPath;
    }

    public void render(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);
        requestDispatcher.forward(request, response);
    }

    public void render(Map<String, Object> model, HttpServletRequest request,
        HttpServletResponse response) throws ServletException, IOException {

        // request 객체에 Controller에서 세팅한 model을 세팅
        model.forEach((key, value) -> request.setAttribute(key, value));
        render(request, response);
    }
}

이로써 Controller에서 서블릿 종속성을 제거하고 viewPath의 중복을 제거함으로써 유지 보수하기 쉬운 설계를 만들었다.

V3는 물론 유지 보수 측면에서 좋은 설계이지만, 컨트롤러 인터페이스를 구현하는 개발자 입장에서 항상 ModelAndView 객체를 생성하고 반환해야 하는 것이 조금 번거롭게 느껴진다. 다음 포스팅에서는 컨트롤러를 구현하는 개발자가 더욱 편하게 사용할 수 있는 컨트롤러를 만들어보겠다.

728x90
반응형

댓글