디자인패턴 (4) - MVC, MVP, MVVM

2ㅣ2ㅣ·2024년 10월 15일

CS

목록 보기
7/13
post-thumbnail

8. MVC 패턴

MVC 사진

  • Model + View + Controller로 이루어진 디자인 패턴
    • Model : 상수, 변수, 데이터베이스 등의 데이터를 담당
    • View : 화면을 담당
    • Controller : 모델 - 뷰를 잇는 다리, 메인 로직을 담당
  • 애플리케이션 구성요소를 3가지로 구분함으로써 각 구성요소에만 집중하여 개발할 수 있음

한마디로 구성요소별로 관심사를 분리한다는 것인데 이런 방식이 왜 재사용성확장에 유리하다는 것일까?

MVC가 보편화되지 않던 시절로 거슬러 가보자. 태초엔 Servlet이 있었다.

  • Servlet이란?
    • 웹서버에서 동적으로 HTML을 처리하고 생성하는 서버 측 프로그램
    • MVC의 M+V+C가 한 서블릿 안에 존재함

회원을 저장하고 회원목록을 조회하는 회원관리 애플리케이션을 만든다고 가정해보자.

  • 폼 입력(MemberFormServlet), 저장(MemberSaveServlet), 조회(MemberListServlet)하는 코드가 필요할 것이고 각각의 Servlet 마다 컨트롤러가 필요할 것이다.

MemberListServlet

    package hello.servlet.web.servlet;
    
    @WebServlet(name="memberListServlet", urlPatterns = "/servlet/members")
    public class. MemberListServlet extends HttpServlet {
        private MemberRepository memberRepository = MemberRepository.getInstance();
    
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            List<Member> members = memberRepository.findAll();
            response.setContentType("text/html;charset=utf-8");
            PrintWriter w = response.getWriter();
            w.write("<html>");
            w.write("<head>");
            w.write("    <meta charset=\"UTF-8\">");
            w.write("    <title>Title</title>");
            w.write("</head>");
            w.write("<body>");
            w.write("<a href=\"/index.html\">메인</a>"); w.write("<table>");
    
            w.write("<thead>");
            w.write("<th>id</th>");
            w.write("<th>username</th>");
            w.write("<th>age</th>");
            w.write("</thead>");
            w.write("<tbody>");
    
            for (Member member : members) {
                w.write("<tr>");
                w.write("       <td>"+member.getId()+"</td>");
                w.write("       <td>"+member.getUsername()+"</td>");
                w.write("       <td>"+member.getAge()+"</td>");
                w.write("</tr>");
    
            }
            w.write("	</tbody>");
            w.write("</table>");
            w.write("</body>");
            w.write("</html>");
        }
    }

각 서블릿마다 html 작성하는 것도 번거롭고 비즈니스 로직을 한 코드에서 처리하는 것은 유지보수나 협업에 어려움이 있었을 것이다. 이러한 문제를 해결하기 위해 JSP가 등장했다.

  • JSP에 html 코드를 작성하여 코드 가독성을 향상시키고 역할을 분리했다.
    JSP(html)
    <%@ page import="hello.servlet.domain.member.MemberRepository" %>
    <%@ page import="hello.servlet.domain.member.Member" %>
    <%@ page import="java.util.List" %>
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <%
        MemberRepository memberRepository = MemberRepository.getInstance();
        List<Member> members = memberRepository.findAll();
    %>
    
    <html>
    <head>
        <title>Title</title>
    </head>
    <body>
    <a href="/index.html">main</a>
    <table>
        <thead>
        <th>id</th>
        <th>username</th>
        <th>age</th>
        </thead>
        <tbody>
    
        <%
            for (Member member : members) {
                out.write(" <tr>");
                out.write("<td>" + member.getId() + "</td>");
                out.write("<td>" + member.getUsername() + "</td>");
                out.write("<td>" + member.getAge() + "</td>");
                out.write(" </tr>");
            }
        %>
        </tbody>
    </table>
    </body>
    </html>

Servlet(Controller + Model)

    @WebServlet(name = "memberListControllerServlet", urlPatterns = "/members")
    public class MemberListControllerServlet extends HttpServlet {
        private MemberRepository memberRepository = MemberRepository.getInstance();
    
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            // 데이터 조회 (모델 역할)
            List<Member> members = memberRepository.findAll();
            
            // 조회된 데이터를 request 객체에 저장하여 JSP로 전달
            request.setAttribute("members", members);
            
            // JSP로 포워딩 (뷰를 호출)
            RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/views/memberList.jsp");
            dispatcher.forward(request, response);
        }
    }

전에 비해 유지보수가 쉬워졌지만, 여전히 비즈니스 로직을 서블릿 안에서만 구현하고 있다.

  • 더 나은 유지보수와 가독성을 위해 비즈니스 로직을 사용자 요청만 받는 Controller + 데이터를 담는 Model로 분리했다.
    회원 목록 조회 (Controller)
    package hello.servlet.web.servletmvc;
    
    @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);
        }
    }

회원 목록 조회 (View)

    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    
    <html>
    <head>
        <title>Title</title>
    </head>
    <body>
    <a href="/index.html">main</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>
    </table>
    </body>
    </html>

회원 목록 조회(Model)

    @Getter @Setter
    @AllArgsConstructor
    public class Member{
    	private Long id;
    	private String username;
    	private int age;
    }

이로써 3가지 구성요소로 역할을 분리한 MVC가 완성되었다.

MVC 패턴을 사용하면 뷰나 모델이 늘어나도 컨트롤러 로직은 변하지 않은채 해당 뷰나 모델과 1:n 연결만 해주면 되기 때문에 편리하다.

현재 사용되는 MVC 패턴은 Front-Controller를 도입하여 사용자 요청을 하나의 컨트롤러에서 받고 있다.

하지만 이러한 MVC 패턴은 모델과 뷰 사이의 의존성이 높아서 애플리케이션 규모가 커질수록 복잡하고 유지보수가 어려워질 수도 있다는 단점이 있다.

이러한 MVC패턴에 기반을 둔 패턴으로 MVP, MVVM이 있다.

9. MVP 패턴

MVP 사진

  • Model + View + Presenter로 이루어진 디자인 패턴
  • Model - Controller 간 의존성이 높은 MVC의 한계를 극복하고자 Presenter라는 개념이 등장함
    • Model : 데이터 담당
    • View : 화면 담당
    • Presenter : View에서 요청한 정보를 가공하여 모델에 전달, 메인 로직을 담당

Presenter는 Model과 View의 인스턴스를 가지고 있어서 둘을 연결하는 접착제 역할을 함

  • Presenter와 View는 1:1 ← Controller와 View가 1:1이었던 MVC와 차이점

View

    public interface UserView {
        void showUsers(List<User> users); // Presenter 요청마다 호출됨
    }
    
    public class UserConsoleView implements UserView {
        @Override
        public void showUsers(List<User> users) {
            for (User user : users) {
                System.out.println("User: " + user.getName());
            }
        }
    }

Presenter

    public class UserPresenter {
        private UserView view;
        private UserRepository repository;
    
        public UserPresenter(UserView view, UserRepository repository) {
            this.view = view;
            this.repository = repository;
        }
    
        public void loadUsers() { // Model에서 가져온 데이터를 View로 전달 
            List<User> users = repository.getUsers();
            view.showUsers(users);
        }
    }

사용 예시

    public class Main {
        public static void main(String[] args) {
            UserView view = new UserConsoleView();
            UserRepository repository = new UserRepository();
            UserPresenter presenter = new UserPresenter(view, repository);
    
            presenter.loadUsers(); // 데이터를 로드하고 뷰를 업데이트합니다.
        }
    }

View - Model 간 의존도는 낮아졌지만 View - Presenter 간 의존도가 높아져서 여전히 애플리케이션이 커질수록 복잡도가 증가한다는 단점이 있다.

10. MVVM 패턴

MVVM 사진

  • Model + View + ViewModel 로 이루어진 디자인 패턴
  • 데이터 바인딩을 통해 MV - V이 자동으로 연동됨
    • Model : 데이터 제공
    • View : UI 담당, ViewModel을 통해 데이터를 표시
    • ViewModel : View를 처리하기 위한 View를 위한 Model을 담당, 데이터 바인딩 역할을 수행
    import java.beans.PropertyChangeListener;
    import java.beans.PropertyChangeSupport;
    
    public class UserViewModel { // 데이터를 가공하여 View에 제공
        private UserRepository repository;
        private List<User> users;
        private PropertyChangeSupport support;
    
        public UserViewModel(UserRepository repository) {
            this.repository = repository;
            this.support = new PropertyChangeSupport(this);
        }
    
        public List<User> getUsers() {
            return users;
        }
    
        public void loadUsers() { // Model에서 데이터를 가져와 users 리스트를 업데이트하고 변경 사항 알림
            List<User> newUsers = repository.getUsers();
            support.firePropertyChange("users", this.users, newUsers);
            this.users = newUsers;
        }
    
        // Observer 패턴을 이용한 바인딩 
        public void addPropertyChangeListener(PropertyChangeListener listener) {
            support.addPropertyChangeListener(listener);
        }
    }

View

    import java.beans.PropertyChangeEvent;
    import java.beans.PropertyChangeListener;
    
    public class UserConsoleView implements PropertyChangeListener {
        private UserViewModel viewModel;
    
        public UserConsoleView(UserViewModel viewModel) {
            this.viewModel = viewModel;
            this.viewModel.addPropertyChangeListener(this);
        }
    
        @Override
        public void propertyChange(PropertyChangeEvent evt) { // 데이터가 변경되면 해당 메서드가 호출되어 View를 갱신
            if ("users".equals(evt.getPropertyName())) {
                showUsers((List<User>) evt.getNewValue());
            }
        }
    
        public void showUsers(List<User> users) {
            for (User user : users) {
                System.out.println("User: " + user.getName());
            }
        }
    }

사용 예시

    public class Main {
        public static void main(String[] args) {
            UserRepository repository = new UserRepository();
            UserViewModel viewModel = new UserViewModel(repository);
            UserConsoleView view = new UserConsoleView(viewModel);
    
            viewModel.loadUsers(); // 데이터를 로드하고 뷰를 자동으로 업데이트합니다.
        }
    }

ViewModel 설계가 쉽지 않다는 단점이 있다.

💡 MVC, MVP, MVVM 패턴의 가장 큰 차이점은 View와 비즈니스 로직 간 관계를 어떻게 설정했느냐?

  • MVC : Controller가 사용자 요청을 받아 Model을 업데이트하고 View에 데이터를 전달하여 화면을 갱신.
  • MVP : PresenterModelView 사이를 중재하며 모든 로직을 처리하고 View를 직접 갱신.
  • MVVM : ViewModelModel의 데이터를 View에 제공하며 데이터 바인딩을 통해 자동으로 View를 갱신
    즉, MVC와 MVP는 명시적으로 View를 제어하고, MVVM은 데이터 바인딩으로 View와 ViewModel을 연결해 자동으로 동기화한다.

💅🏻💅🏻💅🏻 핵심 차이 요약

패턴View와 Logic의 연결 방식View의 역할비즈니스 로직 위치테스트 용이성 및 독립성
MVCControllerView를 업데이트Controller에 의존적, 일부 로직 포함 가능ControllerModelViewModel 간 참조로 테스트가 어려울 수 있음
MVPPresenter가 명시적으로 View를 제어최대한 단순하게 유지, 사용자 입력 전달Presenter에 집중View는 인터페이스를 통해 Presenter에 의존, 테스트 가능
MVVMViewModelView 간의 데이터 바인딩데이터 바인딩을 통해 자동으로 업데이트ViewModel이 중재 역할을 하며, 데이터 가공 및 상태 관리ViewModelView의 독립성으로 테스트가 매우 용이

0개의 댓글