최근에 KMM(Kotlin MultiPlatform Mobile)에 관심이 생겨 깃허브에서 이와 관련한 프로젝트들을 찾아보다가 Orbit MVI라는 프레임워크를 알게 되었고,
MyRealTrip에서 개발한 Box라는 MVI 프레임워크 또한 최재호님의 드로이드 나이츠 2020 MVI 아키텍처 적용기 영상을 시청하다가 알게 되었습니다.
MVC
, MVP
, MVVM
등 많은 아키텍처 패턴에 대한 블로그글과 프로젝트들이 난무하는 가운데, 왜 유명한 기업들이 MVI 아키텍처를 선택하는지에 대한 의문이 생겼고, MVI
라는 새로운 아키텍처 패턴에 대한 학습을 진행하고자 해당 포스팅을 시작하였습니다.
MVI를 설명하기에 앞서, MVP, MVVM 패턴이 왜 쓰이게 되었는지부터 먼저 설명드리도록 하겠습니다.
특정한 아키텍처 가이드 없이 앱을 개발하는 것은 굉장히 어려운 일입니다.
가이드 없이 앱을 개발하면서 생기는 상태 관리(State Management) 문제 혹은 점점 복잡해지는 코드는 아마도 개발을 때려치고 싶게 만들 것입니다.
하지만, 적절한 아키텍처를 선택하는 것은 이런 골칫덩어리 문제들을 해결해 줄 수 있습니다.
먼저 MVP(Model-View-Presenter)
패턴과 MVVM(Model-View-ViewModel)
패턴은 그동안 많은 안드로이드 개발자들에게 선택이 되어왔습니다.
이유가 뭘까요?
간단히 말하자면, 저 2가지 패턴, MVP
와 MVVM
은 UI
와 비즈니스 로직(business logic)
을 디커플링(decoupling)
해주기 때문입니다.
그러나 MVP 패턴
에서 여전히 상태(state)
를 고려하면 복잡해지는데, 그 이유는 보통 MVP 패턴
에서 Presenter
들은 stateless
성격을 띕니다. 이런 Presenter
들을 stateful
하게 만들더라도, 여전히 MVP 패턴
대로 동작하는 방식은 뷰의 상태(state)
와 싱크를 맞추는 작업을 힘들게 만든다는 문제가 있었습니다.
그래서 상태 관리(state management)
측면에서 더 유리한 MVVM 패턴
이 새롭게 주목을 받게 되었습니다.
기본적으로 뷰(View)
는 뷰의 상태 변화를 구독함으로써 뷰모델(ViewModel)
과 소통을 합니다.
그럼에도 MVVM 패턴
에서 만약 뷰(View)
가 서로 다른 여러 개의 상태들을 구독하는 경우, 특히 이러한 상태들이 서로 의존하는 경우에는 구현하기가 까다로울 수 있다는 문제가 있었습니다.
class MyViewModel() {
val isLoading: StateFlow<Boolean>
val items: StateFlow<List<Item>>
}
위 코드와 같이 밀접하게 결합된 프로퍼티들(isLoading, items)을 방출하는 서로 다른 StateFlow
인스턴스들이 있는 ViewModel
코드를 많이 접해봤을 것입니다.
isLoading
이true
일 때에는items
가 보여지면 안되기 때문에 밀접하다고 볼 수 있습니다.
만약 이러한 프로퍼티들을 잘못 처리한다면, 이미 리사이클러뷰(뷰 기반) or 레이지컬럼(컴포즈 기반)에 아이템 목록들이 있을 때 진행바(ProgressBar)
가 표시되는 등의 있어서는 안될 일들이 벌어질 수 있습니다.
또한 개발을 하면서 이러한 프로퍼티들이 증가함에 따라 코드의 복잡성도 증가합니다.
많은 사람들이 착각하고 있는 점이
ViewModel
컴포넌트를 사용하면 무조건 MVVM 패턴을 사용하고 있다고 착가을 합니다. 그러나ViewModel
컴포넌트를 사용한다고 해서 반드시 MVVM 패턴을 따르라는 법은 없습니다.ViewModel
은 다른 다양한 방법들로 사용될 수 있습니다.
그래서 MVI(Model-View-Intent)
패턴이 새롭게 등장합니다.
우선 MVI가 무엇의 약자일까요?
MVI = Model - View - Intent
처음에는 MVI의 Intent가 안드로이드에서 이야기하는 Intent
컴포넌트인 줄 알았습니다.
그러나 그게 아니였습니다. 계속 이야기하면서 설명하도록 하겠습니다.
상태(state)
를 나타냅니다. MVI의 Model
은 아키텍처의 다른 계층 간의 단방향 데이터 흐름을 보장하기 위해 변경할 수 없어야(immutable)
합니다.의도(intent)
를 나타냅니다. 모든 사용자 작업에 대해 View
로부터 의도(intent)
를 수신하고 Presenter
에서 이를 관찰하고 Model
에서 새로운 상태로 변환합니다.MVP
에서와 마찬가지로 인터페이스로 표시되며, 하나 이상의 Activity
또는 Fragment
에서 구현됩니다.이 패턴은 다음 몇가지 규칙들을 필수적으로 따릅니다.
Immutable state
(불변 상태 값) : 상태(state)
를 변경하는 대신 상태(state)
의 업데이트된 복사본을 새로 만듭니다. 이렇게 하면 가변성으로 인한 버그를 피할 수 있습니다.One single view state per screen
(하나의 화면에는 하나의 뷰 상태 정보만) :뷰의 상태(View's State)
는 모든 상태의 프로퍼티들을 포함한 데이터 클래스(Data Class)
가 될 수도 있고, 혹은 표현 가능한 여러 상태들을 나타내는 봉인 클래스(Sealed Class)
의 집합이 될 수도 있습니다. 봉인된 클래스를 사용하면 불가능한 상태(Impossible States - 예를 들어, 한 화면에 진행바와 아이템이 동시에 보이는 상태)
문제가 해결됩니다.Unidirectional data flow
(단방향 데이터 흐름) : 상태를 결정론적으로 만듭니다. (테스트를 하는 것이 쉽고 재밌어집니다.)여기서 단방향 화살표는 데이터 흐름을 나타내고, 양방향 화살표는 상속 관계를 나타냅니다.
MVI
는 Model-View-Intent
의 약자입니다.MVI
의 Model
은 앱의 상태(state)
를 나타냅니다.상태(state)
는 로딩 화면, 리스트에 표시될 새로운 데이터, 네트워크 오류와 같이 주어진 순간에 앱이 어떻게 동작하거나 반응하는지를 나타냅니다.MVI
의 View
에는 사용자 작업을 처리하는 여러 개의 intent()
메서드와 앱의 상태를 렌더링하는 하나의 render()
메서드가 있을 수 있습니다.Intent
는 API 호출 또는 데이터베이스 쿼리와 같은 사용자의 작업을 수행하려는 의도를 나타냅니다. 일반적인 android.content.Intent
를 나타내지 않습니다.MVI
는 앱에 대한 단방향 데이터 흐름을 제공합니다.MVI
를 이해하기 위해서는 반응형 프로그래밍(reactive programming)
, 멀티스레딩(multi-threading)
, RxJava
와 같은 중급/고급 안드로이드 개념을 잘 알아야 합니다. 따라서 MVC
나 MVP
와 같은 다른 아키텍처 패턴에 비해 초보 개발자에게는 진입장벽이 높을 수 있습니다.개인적으로 저는 MVVM이나 MVI 패턴 둘 중 하나를 무조건적으로 따르려고 한다기 보다는, MVVM과 MVI 패턴 그 중간 어딘가에 위치한 그런 아키텍처를 선호합니다. (안드로이드 커뮤니티에서는 이를
단방향 데이터 흐름 아키텍처 (Unidirectional Data Flow Architecture)
라고 부릅니다.) 또 구글 아키텍처 가이드 문서를 보면 이 방법으로 추천해주는 것 같습니다.