MVC 패턴이란?

소프트웨어를 Model-View-Controller의 세 요소로 나누어 개발하는 디자인 패턴을 말한다. 앞서 지적했던 비즈니스 로직과 뷰 영역이 혼재되어 있는 문제점을 극복하기 위해 등장했다.

  • Controller : 클라이언트의 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 View에 전달할 데이터를 조회해서 Model에 담는다.
  • Model : View에 출력할 데이터를 담는다. View가 필요한 데이터는 모두 Model에 담겨 전달되기 때문에 View와 Controller를 완전히 분리할 수 있다.
  • View : Model에 담겨있는 데이터를 사용해서 화면을 렌더링 한다.

컨트롤러에 비즈니스 로직을 둘 수도 있지만, 이렇게 되는 경우 컨트롤러가 너무 많은 역할을 담당한다. 일반적으로 비즈니스 로직은 서비스 계층을 별도로 만들어서 처리한다. 그리고 컨트롤러는 비즈니스 로직이 있는 서비스를 호출한다.

 

회원 등록 폼 - 컨트롤러

@WebServlet(name = "MvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String viewPath = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
  • dispatcher.forward() : 다른 Servlet이나 JSP로 이동할 수 있는 기능. 서버 내에서 다시 호출하는 방식으로 작동한다.
  • /WEB-INF : 이 경로 안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. MVC패턴 적용을 통해 기대하는 것은 반드시 컨트롤러를 통해 JSP가 호출되는 것이다.

회원 등록 폼 - 뷰

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <!-- form 태그의 action에 슬래시(/)가 없으면 상대경로 사용, [현재 URL이 속한 경로 + /save] -->
    <form action="save" method="post">
        username: <input type="text" name="username" />
        age: <input type="text" name="age" />
        <button type="submit">전송</button>
    </form>
</body>
</html>
  • form의 action 경로가 상대경로(경로가 '/'로 시작하지 않음)다.
  • 상대경로로 form을 전송하면 현재 URL이 속한 계층 경로에 이어서 save가 호출된다.

Forward vs Redirect

Forward는 서버 내에서 다음에 이동할 URL로 요청 정보를 그대로 전달한다. 즉, 서버 내에서 해당 요청에 대한 제어권을 다른 Servlet이나 JSP로 넘긴다. 그래서 시스템의 변화가 없는 조회 요청의 경우에만 Forward로 응답하는 것이 적절하다. 다음 URL에서도 사용자의 이전 요청 정보는 유효하기 때문이다. 만약 게시글 등록과 같은 시스템에 변화가 있는 요청에 대해 Forward로 응답한다면 사용자가 응답 페이지에서 실수로 새로고침을 눌렀을 때 중복하여 게시글이 등록될 것이다. Forward 방식은 서버 내부에서 일어나기 때문에 속도가 빠르고 브라우저 상에서 URL의 변화가 없기 때문에 사용자가 인지할 수 없다.
반면, Redirect는 다음에 요청할 URL을 반환하고 브라우저에게 해당 URL로 재요청할 것을 명령한다. 그러면 브라우저는 URL을 기반으로 새롭게 서버에 요청하기 때문에 이전과는 다른 요청 정보를 전달한다. 그래서 시스템에 변화가 발생하는 요청의 경우에는 Redirect로 응답하는 것이 적절하다. 앞선 예시와 같은 경우 Redirect로 응답했다면 이전 요청 정보는 존재하지 않기 때문에 사용자가 새로고침을 여러 번 실행하더라도 게시글이 등록되지 않을 것이다. 브라우저 상에서 재요청하는 방식이기 때문에 Forward 방식보다 느리고 사용자가 인지할 수 있다.

 

회원 저장 - 컨트롤러

@WebServlet(name = "MvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(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);

        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
  • HttpServletRequest를 Model로 사용한다.
  • Request 객체가 제공하는 setAttribute()를 사용하여 데이터를 Request 객체에 보관한 후, 뷰에 전달할 수 있다.
  • 새로 저장한 회원정보를 Model에 담아서 뷰에 전달한다.
  • 뷰는 Reqeust 객체의 getAttribute()를 사용하여 데이터를 조회한다.

회원 저장 - 뷰

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% %>
<html>
<head>
    <title>Title</title>
</head>
<body>
성공
    <ul>
        <li>id=${member.id}</li>
        <li>username=${member.username}</li>
        <li>age=${member.age}</li>
    </ul>
    <a href="/index.html">메인</a>
</body>
</html>
  • <%= request.getAttribute("member")%> 로 모델에 저장한 member 객체를 조회할 수 있지만 코드가 너무 복잡해지기 때문에 JSP의 ${} 문법을 사용하여 조회한다.

 

회원 목록 조회 - 컨트롤러

@WebServlet(name = "MvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        request.setAttribute("members", members);

        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
  • request 객체를 사용하여 members(회원목록)을 모델에 보관한다.

회원 목록 조회 - 뷰

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <a href="/index.html">메인</a>
    <table>
        <thead>
            <th>id</th>
            <th>username</th>
            <th>age</th>
        </thead>
        <tbody>
        <c:forEach var="item" items="${members}">
        <tr>
            <td>${item.id}</td>
            <td>${item.username}</td>
            <td>${item.age}</td>
        </tr>
        </c:forEach>
        </tbody>
</body>
</html>
  • 모델에 담은 members(회원목록)을 JSP가 제공하는 taglib 기능을 사용해서 반복출력했다.
  • members 리스트에서 member를 순서대로 꺼내 변수 item에 담고 member의 속성을 조회한다.

 

MVC 패턴의 한계

MVC 패턴을 적용한 덕분에 컨트롤러 뷰의 역할을 명확히 구분할 수 있게 되었다. 컨트롤러는 사용자의 요청을 처리하기 위해 비즈니스 로직을 실행하고, 뷰는 모델에서 데이터를 조회하여 화면을 렌더링 하는 데 집중한다. 그런데 컨트롤러에는 다음과 같이 중복되는 코드가 있고 필요하지 않은 코드도 존재한다.

  • Forward 중복
    • RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); dispatcher.forward(request, response);
    • View로 이동하는 코드가 항상 중복 호출된다. 이 부분을 메서드로 공통화해도 되지만 메서드 또한 직접 호출해야 한다.
  • ViewPath 중복
    • String viewPath = "/WEB-INF/views/new-form.jsp";
    • jsp의 파일명을 제외한 앞,뒷 부분이 계속 중복된다. 또한, jsp가 아닌 다른 뷰 템플릿으로 변경하고자 한다면 전체 코드를 수정해야 한다.
  • 사용하지 않는 코드
    • HttpServletRequest request, HttpServletResponse response
    • Request 객체는 사용하지 않을 때도 있고, Response 객체는 현재 코드에서 사용하지 않는다.

정리하면, 공통 기능의 처리가 어렵다는 문제가 있다. 기능이 복잡해질수록 공통으로 처리해야 하는 부분이 증가할 것이다. 공통 기능을 메서드로 묶어낸다 해도 메서드를 호출해야 한다. 호출하는 것도 결국 중복이다. 이러한 문제를 해결하기 위해서는 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 한다.

프론트 컨트롤러 패턴을 도입하면 이 문제를 해결할 수 있고, 스프링 MVC의 핵심이 바로 여기에 있다.

+ Recent posts