어플리케이션 아키텍쳐 패턴들의 설명과 구현

개요

개발 중 발생했던 반복적인 문제점들을 정리하여 해결책을 제시하는 일종의 솔루션을 ‘디자인 패턴’이라고 부릅니다.

우리가 흔히 언급하는 어플리케이션 아키텍쳐 패턴들인 MVC와 MVP, MVVM 모두 디자인 패턴들 중 하나이며, 이번 글에서는 이들에 대한 설명과 구현 예시를 정리해보려 합니다.

MVC

MVC란 Model, View, Controller의 약자이며, 프로젝트의 구성 요소들이 다음과 같이 세가지의 역할로 구분되는 패턴을 말합니다.

MVC Structure

Model: 어플리케이션의 정보(데이터)를 나타내며 비즈니스 로직을 수행한다.

View: 텍스트나 체크박스 항목 등과 같이 사용자에게 보여지는 인터페이스 요소를 나타낸다.

Controller: 데이터와 비즈니스 로직 사이의 상호 동작을 관리한다.

다만, 각 구성요소들을 설계할 때 주의해야 할 몇 가지 규칙들이 존재하는데요?

먼저 Model의 경우,

  1. 사용자가 편집하길 원하는 모든 데이터를 가지고 있어야 한다.
  2. 데이터의 변경이 일어나면, 변경 통지에 대한 처리 방법을 구현해야 한다.

View

  1. 모델이 가지고 있는 정보를 따로 저장해서는 안 된다.

마지막으로 Controller

  1. 모델에 대해 알고 있어야 한다.
  2. 모델이나 뷰의 변경을 모니터링 해야 한다.

와 같은 점들입니다.

MVC의 경우 ‘역할에 따라 구분해서 프로그램을 나눠보자.’가 주된 목적이기 때문에 구현 방법이 달라질 수도 있고 이에 따라 지켜야 할 규칙 역시 바뀔 수 있습니다.

실제로 패턴을 가시화한 그림들을 보면 컨트롤러가 뷰와 모델 모두를 참조하는 등 구성 요소간의 관계가 위의 그림과는 다른 점도 있지만, 틀린 구조는 아니라는 것을 알아주시면 좋을 듯 합니다.

보편적으로 많이 사용되는 디자인 패턴이며 이에 걸맞게 구현이 단순하다는 장점을 지니지만, 뷰와 모델 간의 높은 의존성으로 인해 어플리케이션의 규모가 커질수록 복잡해지고 유지보수가 어려워질 수 있다는 단점 역시 존재합니다.

MVC 구현

전체적인 플로우는 아래의 형식을 따랐고, 모델이 뷰를 참조하는 형태는 옵저버 패턴을 활용하여 구현하였습니다.

또한, 뷰가 여러 개의 컨트롤러를 가질 수 있다는 점, 모델 역시 여러 개의 뷰를 가질 수 있다는 점과 같은 특징들을 고려하여, 인터페이스와 이를 구현한 실제 구현체로 분리해봤습니다.

  1. 사용자는 모델의 상태를 변경하기 위해 컨트롤러를 호출한다.
  2. 컨트롤러는 모델의 ‘상태 변경 함수’를 호출하여 상태를 변경한다.
  3. 모델은 뷰에게 자신의 상태가 변경되었다는 것을 알린다.
  4. 뷰는 사용자에게 변경된 모델의 상태를 보여준다.

Model

어플리케이션의 데이터를 다루며 비즈니스 로직을 수행하는 곳 입니다.

옵저버 패턴 중 Subject 역할을 하여, notify 함수를 통해 데이터가 변경되었을 때 뷰에게 알림을 보냅니다.

그리고 컨트롤러가 모델의 함수를 호출하여 상태를 변경하기 때문에 mutate 의 접근지정자는 public 으로 설정하였습니다.

Subject

ConcreteSubject - InputModel

View

사용자에게 보여질 인터페이스 요소들입니다.

사용자가 Input에 값을 입력하면, 컨트롤러를 호출합니다.

모델의 상태가 변경될 때 호출되는 update 함수로 인해 Input은 value가 변경되고, Box는 엘리먼트 내의 textContent가 변경됩니다.

Observer

ConcreateObserver - Input

ConcreteObserver - Box

Controller

데이터와 비즈니스 로직 사이의 상호 동작을 관리하는 곳입니다.

뷰가 실제로 참조하는 곳이며, callMutator 함수를 통해 모델의 mutate 함수를 호출합니다.

Controller

ConcreteController - InputController

Main

실제 동작을 위해 구성 요소들이 모인 곳입니다.

Index

결과

MVC Implementation Result

MVP

기존 MVC에서 뷰와 모델 간의 높은 의존성을 해결하기 위해 나온 디자인 패턴으로서, Controller가 Presenter로 변경되었다는 것이 특징입니다.
(Model - View - Presenter)

MVP Structure

프레젠터의 경우 본질적으로 MVC의 컨트롤러와 같다고 할 수 있지만, 모델과 뷰를 모두 매개체로 삼아 서로 간의 전체적인 상호 작용을 관리하기 때문에 ‘Supervising Controller’ 라고 부르기도 합니다.

MVC 때와 마찬가지로 구현 시 주의해야 할 사항들이 존재하는데, 그 중 하나는 뷰와 프레젠터가 1:1의 관계를 지닌다는 것입니다. (구성요소 간의 복잡도 저하를 위해 설정된 규칙이 아닌가 싶습니다.)

뷰는 프레젠터를 참조하고, 프레젠터는 뷰를 참조하기 때문에 서로 간의 연결(결합) 고리가 강하며, 각각의 뷰마다 프레젠터가 존재하기 때문에 MVP 패턴으로 어플리케이션을 제작할 경우 코드의 수는 증가하게 됩니다.

뷰와 모델 간의 의존성이 없기 때문에 새로운 기능을 추가하거나 변경하고 싶을 때 해당 부분에 대한 코드만 수정하면 되어 확장성이 좋아진다는 장점이 있지만, 결국 어플리케이션이 복잡해질 수록 뷰와 프레젠터 사이의 의존성이 강해진다는 점은 여전히 문제로 존재합니다.

시간이 지나면서 프레젠터가 담당하는 역할 역시 커지기 때문에, 확장 및 분리에 어려움이 없도록 초기에 설계를 잘하는 것이 중요합니다.

MVP의 경우 ‘화면과 로직을 분리하자.’를 주 목적으로 볼 수 있으며, 구현 방법은 조금씩 달라질 수 있지만 뷰와 모델간의 의존성이 없어야 한다는 기본 원칙은 동일합니다.

MVP 구현

프레젠터는 모델을 알지만 모델은 프레젠터를 알지 못 한다는 점, 프레젠터는 뷰와 모델 사이에서 중간자 역할(Middle Man)을 하는 등의 특징들을 잘 드러내는데에 집중하여 구현하였습니다.

  1. 사용자는 버튼을 클릭하며 이 때 프레젠터가 호출된다.
  2. 프레젠터는 모델의 비즈니스 로직을 호출하고 받은 데이터를 가공한다.
  3. 가공된 데이터를 뷰에게 전달한다.
  4. 뷰는 사용자에게 변경된 모델의 상태를 보여준다.

Model

어플리케이션의 데이터를 다루며, 비즈니스 로직을 수행하는 곳 입니다.

여기에서는 서버로 부터 데이터를 받아와 반환하는 역할을 합니다.

ColorModel

getColors - Fetching Data

View

사용자에게 보여질 인터페이스 요소들입니다.

버튼의 클릭 이벤트 발생 시 프레젠터를 호출합니다.

프레젠터가 호출된 후 모델로 부터 전달받은 데이터를 기반으로 뷰가 업데이트됩니다.

Button

ColorList

Presenter

모델과 뷰를 매개체로 삼아 서로 간의 상호 작용을 관리하는 곳입니다.

유저로 부터 이벤트를 전달받으면 모델을 업데이트 하며, 업데이트 된 데이터를 바탕으로 뷰를 업데이트합니다.

ColorPresenter

Main

실제 동작을 위해 구성 요소들이 모인 곳입니다.

Index

결과

MVP Implementation Result

MVVM

용이한 테스트와 규모가 큰 프로젝트라도 상대적으로 관리하기 수월하도록 프로젝트의 구조를 Model - View - ViewModel로 분리한 디자인 패턴입니다.

MVVM Structure

뷰모델은 뷰와 모델 사이의 매개체로서, 화면 상의 디자인 구성을 위한 프레젠테이션 로직과 뷰를 위한 상태를 다룹니다.

모든 뷰와 관련된 로직은 이곳에 들어가게 되며, 뷰가 잘 이해할 수 있도록 데이터를 가공하는 역할 역시 담당합니다.

MVP와 동일하게 뷰와 모델 간의 의존성이 존재하지 않으며, 하나 다른 점은 뷰모델 역시 뷰에 대한 참조를 가지지 않는다는 점입니다.

뷰모델과 뷰는 데이터 바인딩 형태로 연결되기 때문에 의존성이 존재하지 않고 그 결과, 뷰모델은 UI가 아닌 데이터 변경에만 신경을 쓸 수 있게 됩니다.

UI와 비즈니스 로직, 프레젠테이션 로직이 분리된다는 것이 MVVM의 가장 큰 장점이며, 각 구성 요소간의 의존성이 낮춰지기 때문에 구조를 확장하거나 수정하는데에 있어서 용이하다는 점(+ 보다 수월한 유닛테스트 작성) 역시 큰 이점으로 작용합니다.

다만, 시간이 지남에 따라 뷰 바인딩을 위해 관계 없는 프레젠테이션 로직이 늘어날 수 있어 유지보수적인 측면에서 어려움이 있을 수 있고, 데이터 바인딩을 위해 작성해야 하는 방대한 양의 코드는 규모가 작은 어플리케이션에서는 오히려 ‘배보다 배꼽이 크다.’라는 느낌을 주기도 합니다.

MVVM 구현

MVVM 패턴에서는 기본적으로 뷰모델이 뷰를 알지 못해야 한다는 전제가 깔려있기 때문에, 데이터를 어떤식으로 바인딩할지에 대한 방법을 많이 고민했습니다.

Handlebars, Knockout, Vue.js 등 MVVM 패턴 구현에 도움을 줄 수 있는 여러 프로젝트들을 둘러보며 고민한 결과 전반적인 구조가 잡혔고, 그 형태는 아래와 같습니다.

MVVM Implementation Structure
  1. 뷰모델에 데이터와 메서드를 넘겨주고, 바인더에는 바인딩 될 대상의 DOM 요소를 넘겨준다.
  2. 바인더와 뷰모델을 연결한다.
  3. 뷰모델의 데이터가 변경될 때 자신을 구독하고 있는 주체인 바인더에게 알림이 간다.
  4. 뷰모델로부터 받은 정보를 토대로 구문 파싱과 렌더링이 진행된다.
  5. 변경된 UI가 사용자에게 보여진다.

* 위에서 설명했던 MVC와 MVP와 다르게 뷰와 모델이 별도로 분리되어 있지 않습니다.

index.html을 뷰로, 뷰모델에 넘겨주는 데이터를 모델로 생각하면 이해가 좀 더 수월하실 듯 합니다.

ViewModel

데이터를 담고 업데이트하며(getter, setter) 뷰에 제공하는 주체입니다.

구조를 나타낸 그림처럼 Watcher가 별도로 분리되어 있진 않지만, 데이터를 하이재킹하여 변경이 있을 때 마다 자신을 구독하고 있는 주체(Binder)에게 알림을 보냅니다.

ViewModel

Binder

뷰모델과 뷰 사이의 데이터 바인딩을 위한 주체입니다.

일종의 라이브러리처럼 뷰모델을 인스턴스로 직접 가지지 않고, 실제 바인딩 시 파라미터로 뷰모델을 전달받도록 설계했습니다.

Binder

HTML의 mustache에 대한 구문 분석과 더불어 데이터, 이벤트 등의 전반적인 파싱 및 렌더링을 진행하는 주체입니다.

바인더에서 핵심 기능을 담당하는 요소라고 봐도 무방하며, HTML 원본 문서와 파싱된 문서를 구분짓기 위해 파서가 복제된 문서를 지니고 있습니다.

재귀적으로 HTML 구문을 순회하면서 대상이 텍스트 노드일 때는 Mustache 파싱을 진행하고 엘리먼트 노드일 때는 Input 바인딩이나 이벤트와 같은 속성들을 파싱합니다.

코드로 다 표현되진 않았지만 실제 Mustache 구문만을 집중적으로 분석하는 주체가 있으면 좋을 듯 하여 별도의 파서 클래스로 분리하였습니다.

Main Parser

Mustache Parser

Main

실제 동작을 위해 구성 요소들이 모인 곳입니다.

HTML 파일에서는 data-bind 속성으로 Input과 같은 'Inputable'한 엘리먼트의 데이터 바인딩을 지원하며, @{event} 속성으로 클릭이나 키 누름과 같은 이벤트를 설정합니다.

index.ts에서는 뷰모델과 바인더를 별도로 선언하며, 뷰모델에는 데이터와 더불어 관련된 이벤트를 넘겨주고 바인더에는 실제 바인딩 될 DOM 요소(예제에서는 #app)를 넘겨줍니다.

바인더의 bind 메서드 실행 시 파라미터로 뷰모델을 넘겨줌으로써 실질적인 데이터 바인딩이 시작됩니다.

index.html

index.ts

결과

MVVM Implementation Result

마치며

여느 디자인 패턴과 마찬가지로 어플리케이션의 규모가 작고 그렇게 복잡하지 않다면 MVC나 MVP, MVVM 등의 패턴들은 그렇게 큰 구조적인 향상을 가져다 주진 않을 수 있습니다.

다만, 규모가 거대해지고 복잡해짐에 따라 발생할 수 있는 구성 요소들간의 높은 의존성과 복잡도는 아키텍쳐 패턴의 필요성을 더욱 증진시킨다고 생각합니다.

구조를 설계하거나 보다 나은 구조를 생각하는데에 있어 이 글이 도움이 되기를 바랍니다.

P.S. 예제로 들었던 코드는 해당 링크에서 모두 확인하실 수 있습니다!

--

--