MVC, MVP, MVVM과 같은 패턴들에 대해 다루려고한다.
이 패턴들을 '디자인 패턴' 이라고 말하는 사람도 있고, '아키텍처' 라고 말하는 사람도 있다.
먼저 디자인 패턴과 아키텍처 패턴의 정의를 간단하게 정리하자면 아래와 같다.
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides 作 - 'Design Patterns: Elements of Reusable Object-Oriented Software'에서는 디자인 패턴을 '특정 상황에서 자주 발생하는 소프트웨어 디자인 문제를 해결하기 위한 재사용 가능한 솔루션' 이라고 설명한다.
이들이 제시한 23개의 디자인 패턴은 아래와 같다.
Name | Description |
---|---|
Abstract Factory | 관련 객체들의 구상 클래스들을 지정하지 않고도 관련 객체들의 모음들을 (families) 생성할 수 있도록 하는 패턴 |
Builder | 복잡한 객체들을 단계별로 생성할 수 있도록 하는 패턴 |
Factory Method | 상위 클래스에서 객체들을 생성할 수 있는 인터페이스를 제공하지만, 하위 클래스가 생성될 객체들의 유형을 변경할 수 있도록 하는 패턴 |
Prototype | 코드를 그들의 클래스들에 의존시키지 않고 기존 객체들을 복사할 수 있도록 하는 패턴 |
Singleton | 클래스에 인스턴스가 하나만 있도록 하면서 이 인스턴스에 대한 전역 접근 (access) 지점을 제공하는 패턴 |
Name | Description |
---|---|
Adapter | 호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 하는 패턴 |
Bridge | 큰 클래스 또는 밀접하게 관련된 클래스들의 집합을 두 개의 개별 계층구조 (추상화 및 구현)로 나눈 후 각각 독립적으로 개발할 수 있도록 하는 패턴 |
Composite | 객체들을 트리 구조들로 구성한 후, 이러한 구조들과 개별 객체들처럼 작업할 수 있도록 하는 패턴 |
Decorator | 객체들을 새로운 행동들을 포함한 특수 래퍼 객체들 내에 넣어서 위 행동들을 해당 객체들에 연결시키는 패턴 |
Facade | 라이브러리 or 프레임워크에 대한 또는 다른 클래스들의 복잡한 집합에 대한 단순화된 인터페이스를 제공하는 패턴 |
Flyweight | 각 객체에 모든 데이터를 유지하는 대신 여러 객체들 간에 상태의 공통 부분들을 공유하여 사용할 수 있는 RAM에 더 많은 객체들을 포함할 수 있도록 하는 패턴 |
Proxy | 다른 객체에 대한 대체 또는 자리표시자를 제공할 수 있는 패턴 |
Name | Description |
---|---|
Chain of Responsibility | 핸들러들의 체인 (사슬)을 따라 요청을 전달할 수 있게 해주는 패턴 |
Command | 요청에 대한 모든 정보가 포함된 독립실행형 객체로 변환하는 패턴 |
Interpreter | 언어의 문장을 해석하기 위해 표현을 사용하는 해석기와 함께 문법에 대한 표현을 정의 |
Iterator | 컬렉션의 요소들의 기본 표현 (리스트, 스택, 트리 등)을 노출하지 않고 그들을 하나씩 순회할 수 있도록 하는 패턴 |
Mediator | 객체 간의 혼란스러운 의존 관계들을 줄일 수 있는 패턴 (객체 간의 직접 통신을 제한하고 중재자 객체를 통해서만 협력하도록 함) |
Memento | 객체의 구현 세부 사항을 공개하지 않으면서 해당 객체의 이전 상태를 저장하고 복원할 수 있게 해주는 패턴 |
Observer | 자신이 관찰 중인 객체에 발생하는 모든 이벤트에 대하여 알리는 구독 메커니즘을 정의할 수 있도록 하는 패턴 |
State | 객체의 내부 상태가 변경될 때 해당 객체가 그의 행동을 변경할 수 있도록 하는 패턴 |
Strategy | 알고리즘들의 패밀리를 정의하고, 각 패밀리를 별도의 클래스에 넣은 후 그들의 객체들을 상호교환할 수 있도록 하는 패턴 |
Template Method | 상위 클래스에서 알고리즘의 골격을 정의하지만, 해당 알고리즘의 구조를 변경하지 않고 하위 클래스들이 알고리즘의 특정 단계들을 오버라이드 할 수 있도록 하는 패턴 |
Visitor | 알고리즘들을 그들이 작동하는 객체들로부터 분리할 수 있도록 하는 패턴 |
ref. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides 作 - Design Patterns: Elements of Reusable Object-Oriented Software
R. N. Taylor, N. Medvidović, E. M. Dashofy 作 - 'Software Architecture: Foundations, Theory, and Practice'에서는 소프트웨어 아키텍처를 '소프트웨어 시스템의 고수준 구조를 설명하는 구성 요소와 그들 간의 상호 작용을 정의하는 것' 이라고 설명한다.
즉, 정리하자면
모든 아키텍처들은 디자인 패턴이지만, 모든 디자인 패턴은 아키텍처가 될 수 없다.
ref. [Medium] Divesh Singh - Differences between Architecture and design pattern
그러므로 MVC, MVP, MVVM은 디자인 패턴이라고 볼 수 있으며, 이 디자인 패턴들은 아키텍처 관점에서 보았을 때 GUI 아키텍처 라고도 한다.
이제 디자인 패턴 (GUI 아키텍처)들의 특징에 대해 정리해보고자 한다.
iOS에서의 MVC패턴은 크게 2가지로 나뉜다.
순수 객체지향 언어 Smalltalk 기반으로 정립한 위의 Traditional MVC는 디자인 패턴의 Composite, Strategy, Observer 패턴으로 구성된다.
View (Composite): View는 뷰 계층구조로 작동하는 중첩 뷰의 복합체로, 사용자 입력부터 UI (display)는 composite structure의 모든 수준에서 작동된다.
Controller (Strategy): Controller는 하나 이상의 뷰 객체에 대한 전술 (strategy)을 구현하며, 뷰는 시각적인 측면을 유지하는데 국한한다.
즉, 뷰는 무상태성
또한, 애플리케이션의 모든 인터페이스 행동들에 대한 결정은 컨트롤러에 위임한다.
:: View로부터 이벤트가 발생되면 Controller가 이벤트에 대한 적절한 행동 (전술)을 취하며, 이로 인해 Model의 상태가 변경 (update) 되었다면 View에게 자신의 상태를 알리고 View는 Model의 상태를 통해 UI를 갱신하게 된다.
View와 Model이 서로 직접적으로 정보를 주고받음
그러나 이 Traditional MVC에서의 문제점은 View, Controller, Model이 밀접하게 연관되어 있어 서로가 독립적이지 못하다. (= 독립적인 수준에서의 단위 테스트가 불가능)
Cocoa MVC는 Traditional MVC를 개선한 MVC패턴이다. 바인딩을 사용하면 View가 Model을 직접 관찰하여 상태 변경 알림을 수신하는 Cocoa MVC를 구현할 수 있지만 이 디자인에는 이론적 문제가 있다.
"View와 Model은 Reusable한 객체여야 한다."
View는 애플리케이션의 "look and feel" (외관과 느낌)을 나타내며, 'View의 형태와 동작의 일관성은 필수적' 이기 때문에 높은 재사용성을 필요로 한다. 디자인 측면에서 Model과 View를 분리함으로써, 재사용성이 향상된다.
View (Command, Composite): Composite패턴과 target-action 메커니즘을 구현하여 Command패턴을 통합.
Controller (Mediator, Strategy): View와 Model 사이에 중재자 역할로서 View-Model의 직접적인 의존관계 (인터랙션)를 줄임.
즉, Model과 View간의 데이터 흐름을 양방향으로 중재함.
Model (Observer): 데이터와 그 데이터와 관련된 기능을 하나의 단위로 묶어 캡슐화함.
Model의 상태 변경 사항은 Controller를 통해 View에게 전달됨.
:: View로부터 이벤트가 발생되면 Controller가 이벤트에 대한 적절한 행동을 취하고, 이로 인해 Model의 상태가 변경 되었다면 Controller에게 자신의 상태를 알린다. 그리고 난 뒤, View는 Controller에 의해 새롭게 UI를 갱신하게 된다.
만약 바인딩을 사용하지 않는 경우라면 View는 NotificationCenter와 같은 메커니즘을 통해 알림을 수신 받는다.
View와 Model이 서로 간접적으로 정보를 주고받고, 서로의 존재를 모르게 됨. (즉, 서로가 독립적)
ref. Apple Documentation Archive
Cocoa MVC는 Apple에서 추구하는 가장 이상적인 디자인 패턴이었으나, 이 또한 문제가 발생한다.
Cocoa MVC의 Controller는 Model과 View를 중재하기에 Model, View를 모두 알아야한다. (단, 재사용성을 위해 Model과 View는 서로에 대해 모름)
그러나 Controller의 경우, 아래와 같은 cases로 인해 Massive한 특성을 갖게된다.
이러한 특성 때문에 우스갯 소리로 Massive View Controller라고도 한다.
💡 Controller 내부에 View에 대한 인스턴스가 이미 존재하기 때문에, Controller와 View는 의존관계를 이루고 있다. (즉, View + Controller)
이러한 View-Controller간의 인터랙션은 단위 테스트 또한 불가능하다.
Distribution: View와 Model은 분리가 잘 되지만, View와 Controller는 의존관계로 강하게 결합된다.
Testability: Model에 대해서만 Testable하다.
Ease of use: 다른 패턴에 비해 가장 적은 양의 코드로 빠른 개발 가능.
Controller가 Massive한 특징을 갖게 되면서 이를 개선한 MVP패턴이 등장하게 된다.
MVP의 각 요소는 아래와 같다.
Model: 서비스에 사용되어지는 원천 (source) 데이터
View: Controller와 View (Button, Label 등)를 하나의 View로 취급함.
View = UIView (Button, label etc.) + UIViewController
Presenter: View와 Model의 중재자 역할로서, View의 Life Cycle에 관여하지 않음.
Presenter = UI와는 관련이 없는, UI를 관리하는 요소
:: View가 이벤트를 받으면, 반드시 Presenter에게 알린다. Presenter는 Model과 소통하여 action에 대한 로직을 처리하고, View에게 그려야 될 요소들을 알려준다.
즉, View는 UI를 보여주는 역할만 하고, Event에 대한 모든 것들은 Presenter가 하게 되지만, View와 Presenter는 반드시 1:1 대응이다.
Distribution: 대부분의 책임이 Presenter와 Model로 나뉘어지고, View와 Model은 Dumb해짐.
Testability: 대부분의 비즈니스 로직들을 테스트 할 수 있다.
Presenter와 Model은 Plain Object로서, Testable하다.
Ease of use: MVC에 비해 많은 양의 코드를 요구하지만, 각 요소에 대해서는 역할을 분명히 할 수 있음.
MVP패턴에서는 View와 Presenter가 1:1의 관계를 가지면서 View가 여러개만 되어도 매번 Presenter를 생성해주어야 했다. 이러한 단점을 보완하고자 등장한 패턴이 MVVM이다.
Model: 서비스에 사용되어지는 원천 (source) 데이터
View: Controller와 View (Button, Label 등)를 하나의 View로 취급함.
ViewModel: View와 Model의 중재자 역할로서, View의 Life Cycle에 관여하지 않음.
💡 Presenter vs ViewModel
: Presenter는 View와 1:1 대응관계로 View를 참조하지만, ViewModel은 View와 N:1 대응관계를 가지면서 View를 참조하지 않음!
:: ViewModel은 Model의 변경을 일으키고, 변경된 Model을 사용하여 자신의 상태를 갱신한다. View는 ViewModel과 바인딩을 통해 UI를 갱신한다.
Model의 상태를 변경하는 모든 동작들은 ViewModel이 갖게된다.
MVVM패턴에서 중요한 것은 바인딩이다.
iOS 환경에서 바인딩을 하기 위해서는 KVO, NotificationCenter, Property Observers 등을 사용해야한다.
그러나 바인딩이라고 표현하기에는 애매한 감이 있고 사용법 또한 불편, 이때부터 사용하는 것이 바로 Reactive Programming (반응형 프로그래밍) 이다.
단, MVVM패턴에서 바인딩을 편하게 하고자 Reactive Programming (e.g. Combine, RxSwift 등)을 적용한다면 러닝 커브가 높다는 점과 앱을 디버깅하는데 많은 시간을 날릴수도 있다 라는 단점이 있다.
즉, View는 ViewModel을 지켜보고 (Observed, 관찰) 하고, ViewModel은 View가 보여지는 데이터를 갖고 있으면서 (Observable, 방출), View와 ViewModel은 N:1 관계를 가짐.
Distribution: MVP의 View보다 MVVM의 View가 가지는 책임이 더 크다.
MVVM에서 View는 바인딩을 통해 ViewModel의 상태에 따라 UI를 갱신하지만, MVP에서는 View에서 발생하는 이벤트를 Presenter에게 넘기고 View는 자신의 UI를 직접 갱신하지 않는다.
Testability: View와 ViewModel은 서로 의존관계가 아니므로 테스트하기 쉽다.
ViewModel과 Model은 Plain Object로서, Testable하다.
Ease of use: MVP와 비슷한 양의 코드를 요구하지만, View의 이벤트를 Presenter를 통해 알리고 수동으로 View를 갱신하는 MVP와 달리 바인딩을 사용하면 MVVM이 훨씬 더 간편해짐.
Reference