티스토리 뷰

iOS

iOS 아키텍처 패턴 MVVM

관(Gwan) 2021. 6. 22. 15:40

JWT 샘플 앱을 MVC 패턴으로 만들었는데, rxSwift 없이 MVVM을 적용하면서 배운 것들을 블로그에 정리해보려 합니다.

 

Apple's MVC

 

좌측이 Classic MVC 우측이 apple's MVC인데,

iOS 앱 개발 관점에서는 ViewController가 View의 LifeCycle과 깊게 연관되어 있어 View와 Controller의 책임을 나누기가 어렵습니다.

그래서 우측과 같은 애플만의 MVC 패턴이 나오게 되었는데 이 경우 ViewController의 사이즈가 너무 커져서 재사용성이 떨어지고 테스트 관점에서 볼 때도 모델이야 따로 떨어져 있으니 테스트가 된다고 쳐도 View와 Controller를 각각 따로 테스트하기가 어려워집니다.

익숙한 패턴이라 개발 속도도 빠르고 새로운 개발자가 투입돼도 패턴에 대한 적응기간이 불필요해서 좋은 점도 있지만 뭐 하나 바꾸려고 하면 엮인 게 많아서 실수할 여지도 많은 패턴인 것 같네요.

 

이런 문제점을 해결하고자 MV(X)류의 디자인 패턴들이 등장하게 되었는데, 이중 MVVM에서 알아보겠습니다.

 

 

 

 

MVVM 패턴은 책임에 따라 Model, View, ViewModel로 나눠져 있는데, 장점이라고 한다면 제가 느끼기에는 구성요소들이 독립성을 가지게 돼서 테스트할 때 용이하다 정도인 것 같습니다. 

우선 하나씩 살펴보겠습니다.

 

Model

기존 패턴에서의 Model과 동일한 역할을 한다.

실제적인 데이터를 가지고 있으며 ViewModel에 의해 가공되고, ViewModel에 가공된 데이터를 전달해 준다.

ViewModel이 소유하며 View와는 독립적이다.

View

UIView(UIViewController)를 의미하며 UI와 관련된 처리를 하거나 유저 이벤트를 ViewModel에 전달하는 역할을 한다.

ViewModel을 소유하고 있고 ViewModel과의 data binding을 통해 ViewModel의 변경사항을 감지하고 그에 따라 UI를 업데이트한다.

 

* data binding

Model과 UI의 싱크를 맞추는 것, 한쪽이 바뀌면 다른 한쪽도 업데이트되어 데이터의 일관성을 유지할 수 있다.

 

ViewModel

View에서 전달받은 이벤트에 해당하는 로직을 처리하거나, Model을 가공하여 저장한다.

데이터가 저장되면 Data Binding에 의해 View가 자동으로 업데이트된다.

ViewModel도 일종의 모델이다.

APIManager와 같은 통신담당 클래스에서 받아온 원천데이터 = Entity

파싱하여 사용가능한 형태의 데이터 = Model

(ViewModel에서) View에 맞게 가공한 데이터 = ViewModel

 

* View와 ViewModel은 1:N 관계

다른 블로그에서는 View와 ViewModel이 N:1 이라고도 하는데 이렇게 하면 ViewModel이 너무 비대해져서 MVC와 다를 게 없으니 View와 ViewModel은 1:N 관계가 맞는 것 같습니다.

(추가)

MVVM은 View가 ViewModel의 값을 바라보다가 바뀌면 스스로 갱신하는 형태인데..

데이터는 같은데 보여지는 View 구성요소만 다른경우 (ex: 같은 데이터로 tableView, collectionView 쓰는경우) View와 ViewModel의 관계가 N:1이 될 수도 있을 것 같습니다.

정답이 뭔지 모르겠네요..

 

다른 패턴들과 비교해 봤을 때 MVVM 패턴은 데이터 바인딩이 주요 특징인 것 같습니다.

데이터 바인딩 때문에 rxSwift를 많이들 사용하시는 것 같던데 굳이 rxSwift를 사용하지 않더라도 아래의 방법들로 구현이 가능합니다.

  • KVO
  • Delegation
  • Property Observer

여기서는 Property Observer를 사용하여 구현해보겠습니다.

 

View의 역할을 담당할 RentListViewController 클래스를 만들고 그 안에 viewModel과 리스트에 뿌려줄 데이터를 담을 배열을 선언했습니다.

 

viewModel의 bind 메서드를 통해 completionHandler를 바인딩하고 requestRentList 메서드를 호출했습니다.

 

viewModel에는 api 요청을 통해 rentList를 받아오면 실행할 클로저를 담을 변수인 listener가 있고 detailModel에 값이 할당되면 listener를 호출하게 되어 있습니다.

이때 listener에는 bind 메서드를 통해 클로저를 넣어줍니다.

 

requestRentList에서 api 요청 후 success가 떨어지면 detailModel에 response값이 할당되고 프로퍼티 옵저버에 의해 listener가 실행되며 View에 선언된 listModel에 값이 할당되고 tableView가 reload 됩니다.

 

View의 cellForRowAt에서 listModel을 가지고 cell을 구성하면 화면에 리스트가 보이게 됩니다.

 

MVC에 비해 MVVM에서는 테스트가 용이하다고 하여 unitTest를 작성해 봤는데 비즈니스 로직이 View와 분리되어 있어 ViewModel 단독으로 간단하게 테스트가 가능했습니다.

비동기 API 호출 로직을 unitTest 하는 법은 [iOS] 비동기 API 호출 UnitTest 하는법 포스팅을 참고해 주세요.

 

사람마다 구현하는 게 다르다 보니 블로그마다 설명이 다른 패턴인 것 같아서 아직도 이게 맞는 건지 모르겠지만 다른 분들에게 참고가 되었으면 좋습니다.

댓글