MVC(Model-View-Controller)의 문제점
MVC, MVP 패턴에 대해서 자세하게 알아보고, MVC가 가진 문제를 MVP 패턴으로 바꾸며 해결하는 식으로 포스팅을 진행했던 기억이 있습니다.
이번에도 MVC에서 어떤 문제점이 있었는지 한 번 더 짚고 갈까요?
초반에 애플에서 Cocoa MVC 패턴 사용을 지향할 때에는 MVC가 business logic과 user interface 사이의 문제를 잘 분리시켜줬습니다. 하지만, 애플리케이션이 점점 더 커지면서 MVC에 문제가 발생했습니다.
Cocoa MVC에서는 UIViewController가 View와 Controller의 역할을 겸해야 하는 문제가 있습니다.
Controller는 비즈니스 로직을 가지고 있는 부분입니다. View와 Model 사이의 다리 역할을 해주는 곳이죠.
그런데, 이 부분에 UI 관련 로직이 섞이게 됩니다. UI 로직과 더불어 View의 life cycle과 얽히게 됩니다. 애플리케이션이 커지면서 비즈니스 로직도 크고 복잡해지는데, UI 로직마저 껴있는 상태인거죠.
Massive View Controller(MVC)
Massive View Controller는 혼자 많은 책임을 지게 됩니다.
그 많은 책임들 덕분에 ViewController가 너무 똑똑해져 버립니다.
Networking code, Data parsing code, UI Presentation를 위한 Data adjustment code, App statement notification, UI State change code가 모두 View Controller 안에 들어있으니깐요.
각자의 역할이 있는 로직들이 UIViewController에 한 데 모여있다보니 테스트를 진행할 수 없습니다. 비즈니스 로직을 테스트하고 싶어도 UI 관련 로직과 얽혀 있어 UIKit 없이는 테스트를 할 수 없습니다.
또한, View, Model은 재사용이 가능해야 하는데, View와 Controller가 얽혀 있다보니 View를 재사용할 수 없습니다. Model만 재사용 가능합니다. 결국, 리소스가 낭비됩니다.
과부하를 방지하고 Testable한 아키텍쳐를 선택하여 코드를 작성할 필요가 있습니다.
이번엔 MVVM 패턴을 사용해서 MVC의 문제점을 해결해봅시다.
MVVM
MVVM은 Model, View, ViewModel 세 개의 개별 그룹으로 구분하는 패턴입니다.
다이어그램을 봤을 때, 언뜻 보면 MVC와 크게 다를 바가 없어 보입니다.
MVC와 크게 다르지 않기 때문에 MVC와 호환이 가능하며 MVC에서 MVVM으로 아키텍쳐 변경이 쉽습니다.
한 가지 눈에 띄게 바뀐 부분이 있다면 View Model 입니다. Controller가 View Model로 바뀌었습니다. MVVM은 MVC와 유사하지만 ViewController의 역할이 최소화됩니다. MVC에서는 ViewController의 역할이 컸습니다. Controller로서 일을 해야 했기 때문이죠.
MVVM에서는 아닙니다. ViewController의 역할을 최소화하기 위해서 View Model이라는 그룹이 생겼기 때문입니다.
MVVM에 한 걸음 가까워지기 위해서 각 그룹이 가지고 있는 특징과 역할을 알아봅시다.
Model
Model은 MVC 패턴에서의 Model과 동일한 역할을 합니다.
즉, 단순한 구조(구조체, 간단한 클래스)를 가진 앱에서 작동하는 데이터 입니다.
Model은 MVC 패턴에서와 동일하게 UI와 관계가 없고 재사용 가능한 형태여야 합니다. 그렇기 때문에 비즈니스 로직과도 관련이 없습니다.
View
View도 MVC 패턴에서의 View와 동일한 역할을 가집니다.
즉, 화면에 시각적인 요소, 컨트롤을 표시하고 Presentation logic를 가지고 있습니다.
MVVM에서도 View는 Update된 모델로부터 온 데이터를 표시하는 일을 합니다. Controller, Presenter가 모델을 업데이트하고 View에게 업데이트된 모델을 전달해줬듯이, MVVM에서는 그 일을 View Model이 맡아서 진행합니다.
MVC와 크게 다른 점은 UIViewController의 역할입니다.
ViewController는 앞에 UIView가 붙지만 뒤의 Controller에 초점을 맞춰서 MVC에서는 Controller의 역할로 일했습니다. 하지만, UIViewController는 뷰의 계층 구조를 관리하는 클래스입니다. 그렇기 때문에, View 코드가 UIViewController 내부에 쓰여질 수 밖에 없습니다. UIViewController가 View 코드도 관리하면서 Controller의 역할도 맡아야 하다보니 View와 Controller의 코드가 엉켜버리는 겁니다.
이를 해결하기 위해서 MVVM에서는 UIViewController가 UIView와 함께 View의 역할을 맡도록 했습니다.
“Control the View”
MVVM에서는 UIViewController가 뷰를 제어하는 일을 한다는 걸로 이해했습니다. Controller로서의 역할을 바라는 것이 아니고 View로서 화면을 제어하는 역할을 부여했습니다.
그러면 Controller에 있는 로직들은 다 어디로 가야할까요?
바로, ViewModel로 이동합니다.
ViewModel
ViewModel은 MVC 패턴에서 Controller가 맡고 있던 역할을 합니다.
View의 input를 통해서 Model를 업데이트하고 업데이트된 Model를 View로 다시 넘겨주는 일을 하는거죠. View와 Model 사이의 다리 역할을 하는 겁니다.
그렇기 때문에 View와 Model 사이에서 처리해야 하는 로직들을 포함하고 있습니다.
비동기 네트워크 코드, Model 정보를 View에 display할 수 있도록 변환해주는 코드, 모델 변경을 위한 listener 코드 같은 게 ViewModel에 있을 수 있겠네요.
ViewModel의 등장으로 ViewController는 View로서의 역할만 하면 되기 때문에 간단해졌습니다.
MVC를 사용했을 때는 Model이 업데이트되면 ViewController에서 updateUI같은 메서드를 만들어 호출했습니다. 굳이 갱신할 필요없는 부분까지 불필요한 갱신이 생겼고, 그로 인해서 버그가 발생했죠.
불필요한 갱신없이 매순간 Model의 최신 상태를 표시할 수 있게 해주는 Controller가 필요했습니다. Model이 업데이트 되는 즉시, 업데이트된 Model과 일치한 데이터를 바로 View로 보내줄 수 있는 Controller 말이죠.
MVVM에서 그 역할을 해주는 게 바로 ViewModel 입니다.
ViewModel은 Model 데이터를 View로 전달해주어야 하기에 ViewController를 잘 알겠네요?
아닙니다. ViewModel은 View나 ViewController에 대해서 아무것도 몰라요.
ViewController를 실수로 제거해도 ViewModel에 있는 로직이 제대로 동작할 수 있을 정도로 ViewModel은 ViewController를 모릅니다.
왜냐하면, ViewModel 내부에 ViewController를 직접적으로 참조하는 부분이 없기 때문입니다. 프로퍼티로 UIViewController 타입 인스턴스를 참조하고 있지 않죠.
그렇다면, 어떻게 View로부터 input를 가져오는 걸까요?
ViewController가 ViewModel를 참조하고 있기 때문에 가능합니다.
ViewModel은 ViewController를 모르지만 ViewController는 ViewModel를 소유하고 있습니다. 클래스 내부에 ViewModel를 선언하고 ViewModel 메서드를 호출하는 식으로 사용합니다.
그렇기 때문에, ViewController에서 ViewModel로 input 값이나 이벤트를 전달할 수 있는겁니다.
반대로, 어떻게 View에게 값이 바뀌었다는 걸 알려주는 걸까요?
이것도 ViewController가 ViewModel를 소유하고 있기 때문에 가능합니다.
ViewController는 ViewModel를 Observation하고 있습니다. 계속 관찰하다가 관찰하던 값이 바뀌면 화면에 바뀐 값을 표시하는겁니다. 덕분에 매순간 Model의 최신 상태를 뷰에 표시할 수 있는거죠.
관찰하는 값은 여러 개일지 몰라도, 한 프로퍼티를 기반으로 관찰합니다. ViewModel 안에 있는 값을 ViewController가 개별적으로 지켜보고 있다가 해당 프로퍼티 값이 바뀌면 해당 프로퍼티와 관련된 UI도 바뀐 값으로 표시되는 겁니다.
개별적으로 연결되어 있으니 모든 프로퍼티 값이 바뀔 때까지 기다리지 않아도 됩니다. 원하는 값이 바뀌는지만 확인하고 그 값만 불러와서 화면에 반영하면 되니깐요.
근데, ViewModel이 View를 알면 안되나요?
ViewModel이 View를 알게 되면 View에 의존성이 생깁니다.
ViewModel은 비즈니스 로직을 가지고 있습니다. 해당 로직들은 UI와는 아무런 관련이 없습니다. 그렇기 때문에 UIKit를 ViewModel에서는 임포트하지 않아도 되고, UI 코드에 영향을 받지 않기 때문에 쉽게 테스트가 가능합니다. 비즈니스 로직 테스트 말이죠.
ViewModel은 그 자체로 테스트할 수 있어야 합니다. 하지만, View에 의존하게 된다면 ViewModel에서 비즈니스 로직을 테스트하기 위해서 View가 필요해집니다.
만약, View에 있는 라벨에 값을 할당해주는 메서드를 ViewModel에 만든다고 합시다.
func setLabel(_ view: UIViewController) {
view.titleLabel.text = data
}
Swift
복사
그리고 해당 메서드를 테스트한다고 생각해볼게요. 해당 메서드에 값이 제대로 들어갔는지는 View에 있는 라벨 text를 봐야만 알 수 있습니다. View에 있는 라벨을 확인하기 위해서 UIViewController나 UIView 타입의 인스턴스를 만들어야 하고 해당 인스턴스를 만들기 위해서 UIKit를 임포트해야 합니다.
결국, 취약한 테스트가 되어 버립니다.
위에서 말했지만, ViewController를 실수로 제거해도 ViewModel에 있는 로직이 제대로 동작할 수 있을 정도로 ViewModel은 ViewController를 몰라야 합니다.
ViewModel과 View의 역할이 완벽하게 분리되어야 각자에게 득이 됩니다.
ViewController(View)는 많은 작업을 수행하지 않아도 되기 때문에 간단해지고, 비즈니스 로직과 UI 로직이 꼬일 걱정없이 자유롭게 뷰 계층을 수정할 수 있습니다.
ViewModel은 UI와 상관없는 비즈니스 로직만 가지고 있기 때문에 UI에 의존하지 않고 테스트할 수 있겠네요.
어떻게 해야 ViewModel이 View와 더 원활하게 이벤트와 데이터를 주고 받을까요?
바로, View와 ViewModel이 서로 동기화된 상태로 유지될 수 있도록 바인딩 매커니즘을 사용하는겁니다.
Binding
View와 ViewModel 사이에서는 유저 액션이나 데이터를 주고 받습니다.
유저 액션과 데이터를 주고 받기 위해서는 View와 ViewModel 사이에 바인딩이 필요합니다. 바인딩을 통해서 View에서는 유저 액션을 보내 Model에서 변경이 일어날 수 있도록 하고, ViewModel에서는 업데이트된 Model 데이터를 보내서 변경된 값이 화면에 표시될 수 있도록 합니다.
바인딩을 통해서 View와 ViewModel은 동기화됩니다.
ViewModel이 View와 Model 사이를 중재하면서 View를 업데이트하기 위해 자기 자신도 업데이트합니다.
결국, View와 ViewModel은 같은 상태를 유지하게 됩니다.
ViewModel에서 변경된 값을 View에 바인딩하기 위한 매커니즘을 제공해주는 Utility가 필요합니다.
Key-Value-Observing이나 Delegate, Notification를 사용하는 방법이 있긴 하겠지만, 많은 기반 코드를 작성해야 할 겁니다. 물론 간단한 바인딩만 구현하는거라면 해당 방식으로 간단한 Observable 클래스만 갖춰서 바인딩을 구현해도 됩니다.
이렇게만 해도 충분히 MVC보다 나은 구조가 될 겁니다.
KVO, Notification이 있지만 바인딩을 쉽게 수행 가능하도록 도와주는 반응형 프로그래밍 라이브러리가 존재합니다.
Reactive Cocoa, RxSwift, PromiseKit 등이 있겠네요.
반응형 프레임워크로는 더 좋은 MVVM 구조를 만들 수 있지만 그만큼 큰 책임과 큰 에너지를 필요로 합니다. 굉장히 혼잡해지기 쉽고 문제를 디버깅하기 힘들기 때문이죠.
iOS에서도 Combine이라는 프레임워크를 만들었습니다. Apple 버전 RxSwift입니다. 따로, 프레임워크를 프로젝트에 추가하지 않아도 사용할 수 있습니다. 퍼스트 파티이기 때문에 다른 라이브러리들처럼 임포트를 위한 수많은 작업하지 않아도 됩니다.
위의 방식을 사용해서 MVVM 예제 코드를 작성해보려고 합니다.
각 방식이 어떤 특징을 가졌는지 알고 싶기 때문에, 예제를 여러개 만들 생각입니다. 글 작성 후에 링크 달아두도록 하겠습니다.
•
Why MVVM?
왜 MVVM를 사용해야 할까요?
MVC 패턴에서 발생했던 문제점을 해결해주면서도 Testable하게 만들어 주니깐요!
ViewController는 더이상 비즈니스 로직을 가지지 않습니다. ViewController는 단순해지고 책임이 적습니다.
물론, MVP에서 사용하는 View보다는 MVVM에서 사용하는 View가 더 많은 책임을 지고 있을겁니다. View에서 바인딩을 세팅해주어야 하기 때문이죠.
ViewController에서 분리된 비즈니스 로직은 View Model로 들어갑니다.
View Model은 View를 전혀 모릅니다. 이 특징이 ViewModel이 View 구현에 상관없이 비즈니스 로직을 쉽게 테스트할 수 있도록 만들어줍니다.
MVVM이 모든 문제를 해결한 것처럼 보이지만 MVVM도 상황에 따라 맞지 않는 아키텍쳐가 될 수도 있습니다.
앱이 점점 커지면서 View와 ViewModel 사이의 데이터 바인딩이 복잡해져 디버깅이 어려워지는 문제가 발생할 수 있는 거 처럼요.
좋은 아키텍쳐라는 건 정답이 없습니다. 한 아키텍쳐로 모든 문제를 해결할 순 없으니깐요. 그렇기 때문에 우리는 항상 우리의 목적에 맞는 아키텍쳐가 무엇인지 고민해야 합니다.
목적에 맞는 아키텍쳐를 선택할 때, 단일 아키텍쳐 패턴만 선택할 필요도 없습니다. MVVM를 사용하면서도 상황에 따라 MVP, VIPER 같은 다른 아키텍쳐 패턴을 사용해서 문제를 해결할 수도 있으니깐요.
그럼 MVVM 예제와 함께 돌아오겠습니다. ♪( 'ω' و(و”