
Model + View + Controller로 이루어진 디자인 패턴한마디로 구성요소별로 관심사를 분리한다는 것인데 이런 방식이 왜
재사용성과확장에 유리하다는 것일까?
MVC가 보편화되지 않던 시절로 거슬러 가보자. 태초엔 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가 등장했다.
<%@ 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);
}
}
전에 비해 유지보수가 쉬워졌지만, 여전히 비즈니스 로직을 서블릿 안에서만 구현하고 있다.
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이 있다.

Model + View + Presenter로 이루어진 디자인 패턴Model - Controller 간 의존성이 높은 MVC의 한계를 극복하고자 Presenter라는 개념이 등장함Presenter는 Model과 View의 인스턴스를 가지고 있어서 둘을 연결하는 접착제 역할을 함
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 간 의존도가 높아져서 여전히 애플리케이션이 커질수록 복잡도가 증가한다는 단점이 있다.

Model + View + ViewModel 로 이루어진 디자인 패턴 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 : Presenter가 Model과 View 사이를 중재하며 모든 로직을 처리하고 View를 직접 갱신.
- MVVM : ViewModel이 Model의 데이터를 View에 제공하며 데이터 바인딩을 통해 자동으로 View를 갱신
즉, MVC와 MVP는 명시적으로 View를 제어하고, MVVM은 데이터 바인딩으로 View와 ViewModel을 연결해 자동으로 동기화한다.
| 패턴 | View와 Logic의 연결 방식 | View의 역할 | 비즈니스 로직 위치 | 테스트 용이성 및 독립성 |
|---|---|---|---|---|
| MVC | Controller가 View를 업데이트 | Controller에 의존적, 일부 로직 포함 가능 | Controller와 Model | View와 Model 간 참조로 테스트가 어려울 수 있음 |
| MVP | Presenter가 명시적으로 View를 제어 | 최대한 단순하게 유지, 사용자 입력 전달 | Presenter에 집중 | View는 인터페이스를 통해 Presenter에 의존, 테스트 가능 |
| MVVM | ViewModel과 View 간의 데이터 바인딩 | 데이터 바인딩을 통해 자동으로 업데이트 | ViewModel이 중재 역할을 하며, 데이터 가공 및 상태 관리 | ViewModel과 View의 독립성으로 테스트가 매우 용이 |