[CS] 소프트웨어 디자인 패턴 (MVC, MVP, MVVM) - TODO List로 살펴보기

wha1e·2025년 2월 7일
1

TIL

목록 보기
8/8

📍 기초 디자인 패턴 정리

지난 번에 작성한 생성, 구조, 행동으로 분류한 기초 디자인 패턴 정리 글입니다.
[CS] 소프트웨어 디자인 패턴 - 기본편


📍MVC 패턴

`MVC (모델-뷰-컨트롤러)는 사용자 인터페이스, 데이터 및 논리 제어를 구현하는데 널리 사용되는 소프트웨어 디자인 패턴입니다. 소프트웨어의비즈니스 로직화면을 구분하는데 중점을 두고 있습니다. 이러한 "관심사 분리`" 는 더나은 업무의 분리와 향상된 관리를 제공합니다.
출처 : https://developer.mozilla.org/ko/docs/Glossary/MVC

MVC 패턴이 생겨나게 된 가장 근본적인 이유는 결국 유지보수의 용이성이다. 초기 프로그래머들의 개발 과정에서 더욱 편리하고 유용하게 사용할 수 있는 패턴을 분석하다보니, Model, View, Controller(후술 예정)의 구조를 가진 패턴이 편리하게 사용된다는 것을 알게 되었고, 이를 논문으로 정리하여 발표한 것이 현재 우리가 확인하고 있는 MVC … 등의 디자인 패턴이라는 것이다.

MVC 패턴은 애플리케이션의 구성 요소를 모델, , 컨트롤러 형태로 나누어 각 구성 요소에 집중하여 개발할 수 있도록 만든 디자인 패턴이다. 해당 디자인 패턴에서 가장 주목할만한 키워드는 관심사 분리로 각 역할 별로 나누어 구성한다는 것이 핵심이라고 볼 수 있다.

MVC 패턴은 의식적으로 사용하지 않더라도 상당히 친숙한 형태라고 볼 수 있는데, 웹 초창기부터 사용되었으며, 사용자의 화면과 컨트롤러, 데이터를 관리하는 모델로 나누어 관리한다. MVC의 장점으로는 재사용성확장성이 용이하다는 점이 있다. 반면, 애플리케이션이 복잡해질수록 모델의 관계가 복잡해지며 컨트롤러의 역할이 커져, 의존성이 커진다는 단점을 가진다.

(1) MVC의 구성 요소

  • 모델(Model) : 애플리케이션의 데이터인 데이터베이스, 상수, 변수
  • 뷰(View) : input, checkbox, textarea 등 사용자 인터페이스 요소
  • 컨트롤러(Controller) : 하나 이상의 모델과 하나 이상의 를 잇는 다리 역할로 이벤트 등 메인 로직 담당

(2) M, V, C 간의 관계

검색 과정을 통해 MVC 모델의 표현 방식이 다르게 표현되고 있음을 볼 수 있었다. 사실, 그림에서 어떻게 표현하고 보여주는지에 따라 생김새는 다를 수 있지만, 기본적으로 같은 원리로 작동한다.

  1. 사용자는 View를 통해 화면(UI)을 확인한다.
  2. 사용자가 UI를 통해 유저 이벤트를 발생시키면, Controller에서 관련 데이터를 변경하기 위하여 Model에 데이터를 전달(갱신)한다.
  3. ModelController로부터 전달받은 데이터를 조작하고 반환한다. 이때 ModelControllerView에서 일어나는 상황에 대해서는 알지 못한다. (관심사 분리)
  4. 변경되는 데이터를 받은 ControllerView에서 변경이 필요한 요소에 데이터를 전달하며 화면을 갱신한다.

MVC 패턴에서 가장 중요하다고 생각하는 부분은 3번이다.

근본적으로 관심사를 분리하며, Model이 다른 역할에서 어떤 변경이 일어나는지에 대해 신경쓰지 않음으로서, 집약된 기술로 관리가 가능한 것이다.

(3) MVC 패턴의 단점

MVC 패턴의 단점으로는 View와 Model 사이의 의존성이 높다는 것이다. 즉, 애플리케이션의 크기가 커지면서 다양한 View와 Model이 생성되고, MVC 패턴의 경우, Controller가 여러 개의 View를 선택할 수 있는 1:n 구조를 가지기 때문에, 복잡도가 증가한다.


📍MVP 패턴

MVP 패턴은 MVC 패턴으로부터 파생되었으며, MVC에서 C에 해당하는 컨트롤러가 프레젠터(presenter)로 교체된 패턴입니다.
프레젠터일대일 관계이기 때문에 MVC 패턴보다 더 강한 결합을 지닌 디자인 패턴이라고 볼 수 있습니다.
출처 : 면접을 위한 CS 전공지식 노트 p.55

MVP 패턴의 특징으로는 ViewPresenter1 : 1 관계를 가진다는 것이다. 또한, 반드시 Presenter를 통해서만 데이터를 전달 받기 때문에 ViewModel 간의 의존성이 없다.

(1) MVC와의 주요 차이점

항목MVCMVP
Controller/Presenter사용자의 이벤트 처리View에 모든 로직 위임
View와의 관계View와 Controller는 직접 통신View는 Presenter와만 통신
이벤트 처리 방식Controller가 사용자 이벤트 처리Presenter가 모든 이벤트와 로직 처리
UI 갱신 방식View가 직접 갱신Presenter가 View 갱신 지시

결국 MVC와의 차이는 View, Presenter/Controller, Model의 의존성 여부와 어떤 관계성을 지니고 있는지에 대한 차이라고 볼 수 있다.

(2) 그럼에도 불구하고 MVP를 쓰는 이유?

MVC 패턴에서 가장 중요한 개념이었던 관심사 분리, 즉, 책임을 강하게 분리함으로써 코드의 가독성과 유지보수성을 향상시킬 수 있다.

MVC의 경우 Model과 View가 서로 연결되어있다는 의존 관계를 지니지만, MVP의 경우, Presenter를 통해서 상태와 변화를 전달하기 때문에 MVC의 단점인 의존성 문제를 해결할 수 있다.

(3) MVC/MVP 패턴을 지키며 코딩하는 방법

편의를 위해 Presenter와 Controller를 Controller라고 통칭하겠습니다.

1. Model은 Controller와 View에 의존하지 않아야 한다.

// Model
const TodoModel = {
  todos: [],
  addTodo(text) {
    this.todos.push({ id: Date.now(), text, completed: false });
  },
  removeTodo(id) {
    this.todos = this.todos.filter((todo) => todo.id !== id);
  },
};

위 코드는 Vanilla JS로 간단하게 구현한 TodoModeltodos를 관리하는 정보만 포함되어있다. 즉, Controller나 화면과 관련된 View 정보는 존재하지 않는다.


2. View는 Model에만 의존해야 하고, Controller에는 의존하면 안된다.

// View
const TodoView = {
  renderTodos(todos) {
    const todoList = document.getElementById('todo-list');
    todoList.innerHTML = ''; // 기존 목록 초기화

    todos.forEach((todo) => {
      const listItem = document.createElement('li');
      listItem.textContent = todo.text;

      const deleteButton = document.createElement('button');
      deleteButton.textContent = '완료';
      deleteButton.style.marginLeft = '2rem';
      deleteButton.dataset.id = todo.id;

      listItem.appendChild(deleteButton);
      todoList.appendChild(listItem);
    });
  },
};

View 역할로 구현한 TodoView 객체이다. View에서는 렌더링 과정에서 정보를 보여주기 위해, Model에서 가지고 있는 todos에 의존하고 있다. 하지만, 뒤에 나올 Controller에는 의존하지 않고, 단순히 실행되는 이벤트 리스너를 통해 렌더링 역할만 수행하고 있음을 알 수 있다.


3. View가 Model로부터 데이터를 받을 때는, 사용자마다 다르게 보여주어야 하는 데이터에 대해서만 받아야 한다.

예를 들어 화면이 위와 같은 형태를 띄고 있다고 할 때, ‘누구나 쉽게 사용할 수 있는 TODO LIST’라는 제목은 모든 사용자에게 동일하게 보여진다.

즉, 해당 정보는 Model에서 관리하는 것이 아닌 View (현재 JS 코드에서는 HTML이라고 볼 수도 있겠다.)에서 따로 저장하고 보여주는 요소여야 한다는 의미이다.

반면 아래의 요소인 ‘빨래, 청소 등등’ 사용자마다 다르게 입력하고, 사용할 정보에 대해서는 Model에서 관리하며 사용할 수 있으며, 현재 코드에서는 todos 배열에 저장되는 정보라고 볼 수 있다.

이처럼, ViewModel로부터 데이터를 받을 때에는 사용자마다 다르게 보여지고 관리되어야 하는 정보에 대해서만 받아야 한다.


4. Controller는 Model과 View에 의존해도 된다.

const TodoController = {
  setupEventListeners() {
    const addButton = document.getElementById('add-todo');
    addButton.addEventListener('click', this.handleAddTodo);

    const todoList = document.getElementById('todo-list');
    todoList.addEventListener('click', this.handleRemoveTodo);
  },

  handleAddTodo() {
    const input = document.getElementById('todo-input');
    if (input.value.trim() === '') return;
    TodoModel.addTodo(input.value);
    TodoView.renderTodos(TodoModel.todos);
    input.value = '';
  },

  handleRemoveTodo(event) {
    if (event.target.tagName === 'BUTTON') {
      const id = parseInt(event.target.dataset.id, 10);
      TodoModel.removeTodo(id);
      TodoView.renderTodos(TodoModel.todos);
    }
  },
};

Controller 코드에서는 ViewModel에 의존하고 있음을 볼 수 있다.

handleAddTodo()handleRemoveTodo()에서는 TodoModelTodoView에 의존하여, 각 정보를 확인하고 ViewvalueModeltodos객체에 접근하고 있다.


5. View가 Model로부터 데이터를 받을 때, 반드시 Controller에서 받아야 한다.

4번과 연계되는 내용으로, MVC 패턴에서 지켜주어야 하는 것은 View가 데이터를 받을 때, Model에게 직접 받는 것이 아닌, Controller를 거쳐서 받아야 한다는 것이다.

MVC(MVP) Todo List 전체 코드

  • index.html
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="utf-8" />
    <title>JavaScript MVC 패턴</title>
  </head>
  <body style="width:30%; display:flex; justify-content: center; flex-direction: column; margin:auto;">
    <h1>누구나 쉽게 사용할 수 있는 TODO LIST</h1>
    <div>
      <input id="todo-input" type="text"></input>
      <button id="add-todo">추가</button>
    </div>
    <ul id="todo-list" style="display:flex; flex-direction: column; gap: 16px;"></ul>
    <script src="./main.js"></script>
  </body>
</html>
  • main.js
// Model
const TodoModel = {
  todos: [],
  addTodo(text) {
    this.todos.push({ id: Date.now(), text, completed: false });
  },
  removeTodo(id) {
    this.todos = this.todos.filter((todo) => todo.id !== id);
  },
};

// View
const TodoView = {
  renderTodos(todos) {
    const todoList = document.getElementById('todo-list');
    todoList.innerHTML = ''; // 기존 목록 초기화

    todos.forEach((todo) => {
      const listItem = document.createElement('li');
      listItem.textContent = todo.text;

      const deleteButton = document.createElement('button');
      deleteButton.textContent = '완료';
      deleteButton.style.marginLeft = '2rem';
      deleteButton.dataset.id = todo.id;

      listItem.appendChild(deleteButton);
      todoList.appendChild(listItem);
    });
  },
};

// Controller
const TodoController = {
  setupEventListeners() {
    const addButton = document.getElementById('add-todo');
    addButton.addEventListener('click', this.handleAddTodo);

    const todoList = document.getElementById('todo-list');
    todoList.addEventListener('click', this.handleRemoveTodo);
  },

  handleAddTodo() {
    const input = document.getElementById('todo-input');
    if (input.value.trim() === '') return;
    TodoModel.addTodo(input.value);
    TodoView.renderTodos(TodoModel.todos);
    input.value = '';
  },

  handleRemoveTodo(event) {
    if (event.target.tagName === 'BUTTON') {
      const id = parseInt(event.target.dataset.id, 10);
      TodoModel.removeTodo(id);
      TodoView.renderTodos(TodoModel.todos);
    }
  },
};

TodoController.setupEventListeners();
TodoView.renderTodos(TodoModel.todos);
  • 실행 결과

위 코드의 경우, 어떤 관점에서 보느냐에 따라 MVP가 될 수도, MVC가 될 수도 있습니다. 개인적인 생각으로는 View가 이벤트 처리 로직을 가지고 있지 않기 때문에, MVP 패턴에 가까운 구조로 해석할 수 있다고 생각합니다. (해당 관점에 대해 명확한 기준이 있다면 댓글로 알려주세요!)

(4) MVP 패턴의 단점

MVC 패턴의 단점인 ViewModel 사이의 의존성은 해결되었지만, ViewPresenter1:1 관계로 의존성이 높다는 단점이 있다. 따라서, 애플리케이션이 커지고 복잡해질수록 ViewPresenter 사이의 의존성이 강해지는 문제가 존재한다.


📍MVVM 패턴

MVVM 패턴MVC의 C에 해당하는 컨트롤러가 뷰모델(view model)로 바뀐 패턴입니다.
여기서 뷰모델은 뷰를 더 추상화한 계층이며, MVVM 패턴은 MVC 패턴과는 다르게 커맨드데이터 바인딩을 가지는 것이 특징입니다.
출처 : 면접을 위한 CS 전공지식 노트 p.55

그림을 살펴보면, 뷰와 뷰모델 사이의 양방향 데이터 바인딩을 지원하는 것을 볼 수 있다. 이로서, MVVM은 UI를 별도의 코드 수정 없이 재사용할 수 있고, 단위 테스팅이 편리해진다는 장점을 가진다. View ModelView는 1:N 관계를 가진다.

MVVM 패턴 또한 ViewModel 사이의 의존성이 없다는 장점이 존재한다. 또한, Command 패턴Data Binding을 사용하여 ViewView Model 사이의 의존성 또한 없앤 디자인 패턴이다.

커맨드 : 여러 가지 요소에 대한 처리를 하나의 액션으로 처리할 수 있게 하는 기법

데이터 바인딩 : 화면에 보이는 데이터와 웹 브라우저의 메모리 데이터를 일치시키는 기법을, 뷰모델을 변경하면 뷰가 변경된다.

더 상세한 내용은 위키피디아에서,,

커맨드 - 위키피디아

데이터 바인딩 - 위키피디아

(1) MVVM 패턴을 사용하는 프레임워크

MVVM 패턴을 가진 대표적인 프레임워크로 Vue.js를 들 수 있다. 반응형이 특징인 Vue.js는 watch와 computed 등으로 쉽게 반응형 값을 구축할 수 있다.

이 덕분에 함수를 사용하지 않고 값을 대입하는 것 만으로 변수가 변경되고, 양방향 바인딩, html을 토대로 컴포넌트를 구축할 수 있다는 특징이 있다. 이로서 재사용 가능한 컴포넌트의 기반으로 UI를 구축할 수 있도록 한다.

위와 같은 형식은 React, Angular, 모바일(Android) 등 다양한 프론트엔드 프레임워크에서 사용된다. (사용 방식과 구조가 다르다)

(2) MVVM으로 구현한 Todo List

  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>MVVM Example</title>
</head>
<body>
  <h1>Todo List</h1>
  <input id="todo-input" type="text" placeholder="Add a todo" />
  <button id="add-todo">Add Todo</button>
  <ul id="todo-list"></ul>

  <script>

  </script>
</body>
</html>
  • main.js
// Model
class TodoModel {
  constructor() {
    this.todos = [];
  }

  addTodo(text) {
    this.todos.push({ id: Date.now(), text });
  }

  removeTodo(id) {
    this.todos = this.todos.filter((todo) => todo.id !== id);
  }
}

// ViewModel
class TodoViewModel {
  constructor(model) {
    this.model = model;
    this.todoListElement = document.getElementById('todo-list');
    this.todoInputElement = document.getElementById('todo-input');
    this.addButtonElement = document.getElementById('add-todo');

    this.bindEvents();
  }

  bindEvents() {
    this.addButtonElement.addEventListener('click', () => this.addTodo());
    this.todoListElement.addEventListener('click', (event) => this.removeTodoHandler(event));
  }

  addTodo() {
    const text = this.todoInputElement.value.trim();
    if (!text) return;
    this.model.addTodo(text);
    this.todoInputElement.value = '';
    this.render();
  }

  removeTodoHandler(event) {
    if (event.target.tagName === 'BUTTON') {
      const id = parseInt(event.target.dataset.id, 10);
      this.model.removeTodo(id);
      this.render();
    }
  }

  render() {
    this.todoListElement.innerHTML = '';
    this.model.todos.forEach((todo) => {
      const li = document.createElement('li');
      li.textContent = todo.text;

      const deleteButton = document.createElement('button');
      deleteButton.textContent = '완료';
      deleteButton.dataset.id = todo.id;

      li.appendChild(deleteButton);
      this.todoListElement.appendChild(li);
    });
  }
}

// 앱 초기화
const model = new TodoModel();
const viewModel = new TodoViewModel(model);

위 코드와 같이 View는 HTML로 UI 요소로만 존재하며 로직을 포함하고 있지 않다. 또한, 데이터 바인딩의 대상이 되어, 데이터가 변경되면 자동으로 업데이트 하는 것을 볼 수 있다.

Model인 TodoModel은 데이터인 todos와 관련된 로직을 처리하고 있으며, ViewModel인 TodoViewModel은 Model과 View 간의 중재자 역할을 하고 있다. 여기서 이벤트 핸들링과 렌더링 로직을 모두 포함한다.

(3) MVVM 패턴의 장단점

  • 장점

: MVVM 패턴은 ViewModel 사이의 의존성이 없다. (MVC의 단점 극복) 또한, MVP 패턴의 단점이었던 ViewPresenter(View Model) 사이의 의존성 또한 CommandData Binding을 사용하여 극복할 수 있다.

  • 단점

: MVVM 패턴은 View Model의 설계가 쉽지 않다는 특징을 가진다. MVC, MVP에 비해 다양한 실행 요소와 이벤트 핸들링 로직, 렌더링 로직 등을 포함하기 때문이다.


참고자료

주홍철 (2023). 면접을 위한 CS 전공지식 노트 (초판 6쇄). (주)도서출판 길벗

https://www.youtube.com/watch?v=ogaXW6KPc8I

https://velog.io/@eddy_song/mvc

https://f-lab.kr/insight/understanding-mvc-and-state-management-in-javascript

https://beomy.tistory.com/43

https://velog.io/@kyeun95/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-MVP-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80

https://velog.io/@sensecodevalue/VUE%EC%9D%98-MVVM-%ED%8C%A8%ED%84%B4-%EC%9D%B4%ED%95%B4%EC%99%80-MVC-MVP%EC%97%90%EB%8C%80%ED%95%9C-%EA%B0%84%EB%8B%A8-%EC%84%A4%EB%AA%85

https://012.vuejs.org/images/data.png

profile
상상을 현실로 만드는 FE

0개의 댓글

관련 채용 정보