본문 바로가기

Software architecture and design pattern/iOS architecture pattern

[iOS architecture] Swift에서 사용되는 MVVM 아키텍쳐 패턴 탐구하기 | MVVM vs MVP | Coordinator 개념

 

안녕하세요. 이번 포스트는 MVVM 패턴의 역사와 MVVM에서 사용되는 뷰 바인딩 패턴인 Input/Output pattern에 대해 공부했던 개념을 정리해보려고 합니다.

 

 

2000s 당시...

옛날에는 디자이너가 사용자의 interface 요소를 통해 뷰를 생성하고, 끌어다 놓으면 개발자가 각각의 뷰에 대해 세세하게 코드로 작성(이벤트 처리, 레이아웃 조절 등)했다고 합니다. 이 결과 view 와 business logic이 매우 타이트하게 연결 됬습니다. 그래서 확장성, 유지보수에 어려웠다고 합니다. 그래서 디자이너가 뷰의 레아이웃 바꾸는 걸 꺼려했다고 합니다. (개발자들이 다량의 코드를 다시 작성해야 하기 때문입니다.)

 

그래서 마이크로소프트는 view business logic 분리하는 MVVM architecture pattern을 새롭게 소개했습니다. 물론 iOS개발은 피그마 같은 디자인 툴로 사용해서 디자이너가 작업하면 iOS 개발자들이 컴포넌트들을 XCode로 만들죠. 중요한 것은 view와 business logic을 분리함으로 view의 user interface를 자유롭게 바꿀 수 있습니다.

 

 

MVP vs MVVM

 

MVC pattern 개념 포스트에서 realistic mvc pattern 그림을 볼 수 있었는데, View와 Controller가 결합되어 있습니다. View와 Controller의 분리가 완벽하지 않습니다.

 

https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/Model-View-Controller/Model-View-Controller.html#//apple_ref/doc/uid/TP40010810-CH14-SW1

 

Original MVC pattern을 보면 model이 뷰에 바인딩 되어 있습니다. ( 이럴경우 뷰를 다른 것으로 바꿀 때 model도 바인딩 관련 작업을 다시 처리해야 합니다. - 복잡..)

 

 

이런 MVC의 어려움을 해결하기 위해 MVC를 변형한 MVP 패턴이 있습니다. 겉 보기엔 MVVM과 비슷하지만 다릅니다.

 

https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52

 

MVP에서 View는 View or viewController입니다. Persenter에는 UIView layout code가 전혀 없습니다. 즉 Presenter는 UIKit에 의존적이지 않습니다. Presenter는 User interaction이나 UI를 업데이트 하고 View의 데이터 전달을 담당합니다(..? ViewModel과 다른점이 뭐지?). View, ViewController를 View layer로 분리했기 때문에, view의 lifecycle에 대해 관여하지 않습니다. 

 

단점으로 View 와 Presenter를 1대1로 만들어야 합니다. 원래 Presenter는 View의 user interaction을 받아야하고, Presneter는 View의 특정 액션에 대한 로직을 구현해서, 다시 delegate로 view에게 알려주어야 합니다. 이 부분에서 의존성이 생깁니다.(View가 presenter의 특정 delegate 메서드를 실행합니다.) 결국 Presenter는 View의 상태를 직접 변경하고 View는 Presenter에게 델리게이트 패턴으로 이벤트를 전달하기 때문에 앱이 커질수록 의존성이 높아집니다. 

 

MVVM's concept

 

https://www.objc.io/issues/13-architecture/mvvm/

 

MVP와 마찬가지로 View/ViewController를 View layer로 봅니다. View와 Model을 분리했고 MVP처럼 View에서 일어나는 user interaction을 ViewModel에서 처리 후 view에게 notify합니다. ViewModel이란 말 그대로 View에서 필요하는 데이터를 model에서 가져와 가공한 후에 view한테 제공하는 역할입니다.

MVP와 비슷한데, 다른점은? "reactive"라는 것입니다. 데이터 양방향 바인딩을 한다는 것이 MVP와 MVVM 차이의 핵심입니다. View는 ViewModel의 변경된 state를 반응합니다. 뷰 모델은 모델에서 가져온 데이터를 기반으로 상태를 업데이트합니다. 이 업데이트 된 상태에 따라 뷰가 update됩니다. MVVM 패턴의 주 목적은 view와 business logic을 분리하는 것입니다.

 

 

Model layer

 

  • 앱에서 사용되는 데이터 담당입니다.
  • 단순한 데이터 저장 뿐 아니라 dao(data access object. Like CURD), 데이터 검증(유효성 검사)

 

View(ViewController) layer

 

  • 앱에서 보여지는 UI component를 담당합니다.
  • View는 데이터를 보여주고, 사용자의 입력(input)을 받아서 그대로 ViewModel에게 알려줍니다.
  • 화면 표시 및 레이아웃 관리합니다.
  • ViewModel의 state 변화에 대한 UI render
  • view lifecycle 관리
  • VC transition 담당 ( 추후 coordinator pattern 담당..)
  • 애니메이션 로직 등

 

View는 ViewModel의 state 변화에 따른 (output 신호를 받으면), UI render를 하거나 다른 화면으로 넘어갑니다.

 

ViewModel state 변화는 binding을 통해 알 수 있습니다. Input/Output ViewModel binding이나 kvo, closure, delegate를 사용한 바인딩 등 다양합니다. 이 중에서 Input/Output binding은 View의 state 변화가 생기면, ViewModel에게 Input 신호로 전달합니다. ViewModel의 state 변화가 생기면 View한테 ViewModel의 output신호를 전달합니다.

추가로 reactorKit을 사용한 reactor 패턴은 ViewModel의 역할을 수행합니다. state는 View의 상태를 말하는데, action이 발생할 때마다 reduce함수를 통해 새로운 상태를 반환합니다.

 

사실 뷰는 UI render이외에도 화면 전환을 ViewController에서 담당하지만(네비게이션 컨트롤러를 갖고 있기 때문입니다.) 이렇게 되면, 넘어갈 화면에 대한 데이터를 갖고 있어야 합니다(전환될 화면에 대한 의존성 주입을 하기 위해). 이럴경우 전환하기 전의 VC는 전환해야 할 VC에 대한 정보를 갖고 있어야 함(특정 VC 인스턴스 생성해야 하기 때문)으로 의존성이 생깁니다.

또한 화면 전환코드를 View에서 갖고 있음으로 코드가 복잡해집니다. 그래서,, ViewController이 맡고 있던 화면 전환을 Coordinator 라는 새로운 layer가 등장했다는 사실이 있습니다. 화면 전환에 대한 로직을 Coordinator가 담당하게 된다면 ViewController는 순수하게 UI에 대한 로직 처리를 할 수 있습니다.

 

ViewModel layer

 

  • user interaction으로 View에서 발생한 모든 이벤트를 input으로 받아, 처리 후 ViewModel state를 변화합니다.
  • model에게 데이터 업데이트 요청
  • 데이터 가공 및 변환(업데이트 된 model 이 뷰의 데이터를 갱신해야 한다면) viewModel에게 알려주어 viewModel의 state 변경
  • Model로부터 데이터 가져오기
  • 모델이 업데이트 되어 ViewModel의 state 변경
  • 비즈니스 로직 처리

 

ViewModel은 View에서 발생한 input(user interaction, View에서 발생하는 이벤트. ex lifecycle) 신호를 받아서, 관련된 로직을 처리합니다. 서버의 데이터 요청을 보내던가, view의 action event에 대한 handler를 한다던가, 데이터의 상태를 관리 후 ViewModel state를 변경해서 view에게 output 신호를 보낸다던가, 복잡한 비즈니스 로직을 처리한다던가...

이때 앱의 핵심 비즈니스 로직 또한 ViewModel에서 처리하게 됩니다.
"앱의 핵심로직은 use cases에서 담당하는 거 어때!!" 하고 Domain layer가 등장했습니다. Clean Architecture를 기반으로 한 VIPER 등장!! (Clean Architecture + MVVM개념도 있습니다.)

 

화면 전환의 경우 MVC, MVVM, MVVM+C 에서 발생하는 flow

예를들어 A화면에서 nextScene 버튼을 클릭했을 때 B화면으로 넘어가는 경우가 있다고 해보겠습니다.

 

MVC의 flow는

A화면의 gotoBScene 버튼 클릭-> A화면VC(B화면으로 이동해!!) -> B화면VC

 

Input/Output 바인딩을 사용한 MVVM의 flow는

A화면의 gotoBScene 버튼(Input 발생) -> AViewModel(State변화 후 Output으로 VC에게 알림) -> A화면의 VC(B화면으로 이동해!!) -> B화면VC

 

Input/Output 바인딩 + MVVM  + Coordinator 패턴..을 사용할 경우 flow

A화면의 gotoBScene 버튼(Input 발생) -> AViewModel(State변화 후 Output으로 VC에게 알림) -> A화면의 VC에서 코디네이터 호출.

 

코디네이터에서 다음화면VC에 필요한 VM 인스턴스 생성, 의존성 주입 등 VC인스턴스 생성 후 B화면으로 화면 이동.

 

이때 ViewModel에서 직접적으로 처리하지 않는 이유는 Input/Output을 ViewModel의 input에 대한 Output state는 View에서 받기  때문입니다. ViewModel은 View를 참조하지 않습니다. View와 ViewModel간 종속성이 낮아진다는 것은 변경사항을 적용하기 쉬워지고, 단위 테스트가 좋아집니다.

 

참고로 Coordinator는 ViewModel보단 View에 위치합니다. ViewModel의 주 역할은 View에서 필요한 데이터를 Model에서 가져와 가공 후 다시 View에게 전달하는 것이기 때문입니다. 일반적으로는 ViewController에서 Coordinator의 인스턴스를 주입받습니다. ViewModel에서 Coordinator 인스턴스를 보유하는 것보다 VC에서 Coordinator 인스턴스를 주입받는게 좋은 이유는 ViewModel은 Coordinator에 대한 의존성을 주입받지 않기 때문입니다. 이럴 경우 ViewModel의 결합도가 낮아져 유지보수에 좋습니다. ViewModel은 Coordinator를 갖고 있지 않기 때문에, 순수한 로직 처리에 좋습니다(SPR 단일 책임의 원칙,, 화면 전환과 같은 UI 관련 처리르 담당하기 때문입니다.)

 

물론 ViewModel에서 데이터 가공 후에 Coordinator를 통해 다음 화면으로 전환할 경우 간편하긴 합니다. ( Coordinator 관련 내용은 정확하지 않을 수 있습니다... )

 

 

내용이 길어져서 ViewModel의 Input, Output 바인딩 적용은 다음 글에서 작성해야겠습니다.

 

 

References

 

https://learn.microsoft.com/ko-kr/archive/msdn-magazine/2009/february/patterns-wpf-apps-with-the-model-view-viewmodel-design-pattern

 

https://www.kodeco.com/books/advanced-ios-app-architecture

 

https://saad-eloulladi.medium.com/ios-swift-mvp-architecture-pattern-a2b0c2d310a3