MVI 아키텍처 패턴!

wonseok·2023년 1월 10일
0
post-thumbnail

최근에 KMM(Kotlin MultiPlatform Mobile)에 관심이 생겨 깃허브에서 이와 관련한 프로젝트들을 찾아보다가 Orbit MVI라는 프레임워크를 알게 되었고,

MyRealTrip에서 개발한 Box라는 MVI 프레임워크 또한 최재호님의 드로이드 나이츠 2020 MVI 아키텍처 적용기 영상을 시청하다가 알게 되었습니다.

MVC, MVP, MVVM 등 많은 아키텍처 패턴에 대한 블로그글과 프로젝트들이 난무하는 가운데, 왜 유명한 기업들이 MVI 아키텍처를 선택하는지에 대한 의문이 생겼고, MVI라는 새로운 아키텍처 패턴에 대한 학습을 진행하고자 해당 포스팅을 시작하였습니다.

MVP, MVVM 아키텍처 패턴 되짚어보기

MVI를 설명하기에 앞서, MVP, MVVM 패턴이 왜 쓰이게 되었는지부터 먼저 설명드리도록 하겠습니다.

특정한 아키텍처 가이드 없이 앱을 개발하는 것은 굉장히 어려운 일입니다.
가이드 없이 앱을 개발하면서 생기는 상태 관리(State Management) 문제 혹은 점점 복잡해지는 코드는 아마도 개발을 때려치고 싶게 만들 것입니다.

하지만, 적절한 아키텍처를 선택하는 것은 이런 골칫덩어리 문제들을 해결해 줄 수 있습니다.

먼저 MVP(Model-View-Presenter) 패턴과 MVVM(Model-View-ViewModel) 패턴은 그동안 많은 안드로이드 개발자들에게 선택이 되어왔습니다.

이유가 뭘까요?

간단히 말하자면, 저 2가지 패턴, MVPMVVMUI비즈니스 로직(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 코드를 많이 접해봤을 것입니다.

isLoadingtrue일 때에는 items가 보여지면 안되기 때문에 밀접하다고 볼 수 있습니다.

만약 이러한 프로퍼티들을 잘못 처리한다면, 이미 리사이클러뷰(뷰 기반) or 레이지컬럼(컴포즈 기반)에 아이템 목록들이 있을 때 진행바(ProgressBar)가 표시되는 등의 있어서는 안될 일들이 벌어질 수 있습니다.

또한 개발을 하면서 이러한 프로퍼티들이 증가함에 따라 코드의 복잡성도 증가합니다.

많은 사람들이 착각하고 있는 점이 ViewModel 컴포넌트를 사용하면 무조건 MVVM 패턴을 사용하고 있다고 착가을 합니다. 그러나 ViewModel 컴포넌트를 사용한다고 해서 반드시 MVVM 패턴을 따르라는 법은 없습니다. ViewModel은 다른 다양한 방법들로 사용될 수 있습니다.

그래서 MVI(Model-View-Intent) 패턴이 새롭게 등장합니다.

MVI?

우선 MVI가 무엇의 약자일까요?

MVI = Model - View - Intent

처음에는 MVI의 Intent가 안드로이드에서 이야기하는 Intent 컴포넌트인 줄 알았습니다.
그러나 그게 아니였습니다. 계속 이야기하면서 설명하도록 하겠습니다.

  • Model : 상태(state)를 나타냅니다. MVI의 Model은 아키텍처의 다른 계층 간의 단방향 데이터 흐름을 보장하기 위해 변경할 수 없어야(immutable) 합니다.
  • Intent : 사용자가 작업을 수행하려는 의도(intent)를 나타냅니다. 모든 사용자 작업에 대해 View로부터 의도(intent)를 수신하고 Presenter에서 이를 관찰하고 Model에서 새로운 상태로 변환합니다.
  • View : 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 (단방향 데이터 흐름) : 상태를 결정론적으로 만듭니다. (테스트를 하는 것이 쉽고 재밌어집니다.)

여기서 단방향 화살표는 데이터 흐름을 나타내고, 양방향 화살표는 상속 관계를 나타냅니다.

요약

  • MVIModel-View-Intent의 약자입니다.
  • MVIModel은 앱의 상태(state)를 나타냅니다.
    = 상태(state)는 로딩 화면, 리스트에 표시될 새로운 데이터, 네트워크 오류와 같이 주어진 순간에 앱이 어떻게 동작하거나 반응하는지를 나타냅니다.
  • MVIView에는 사용자 작업을 처리하는 여러 개의 intent() 메서드와 앱의 상태를 렌더링하는 하나의 render() 메서드가 있을 수 있습니다.
  • Intent는 API 호출 또는 데이터베이스 쿼리와 같은 사용자의 작업을 수행하려는 의도를 나타냅니다. 일반적인 android.content.Intent를 나타내지 않습니다.
  • MVI는 앱에 대한 단방향 데이터 흐름을 제공합니다.
  • MVI를 이해하기 위해서는 반응형 프로그래밍(reactive programming), 멀티스레딩(multi-threading), RxJava와 같은 중급/고급 안드로이드 개념을 잘 알아야 합니다. 따라서 MVCMVP와 같은 다른 아키텍처 패턴에 비해 초보 개발자에게는 진입장벽이 높을 수 있습니다.

개인적으로 저는 MVVM이나 MVI 패턴 둘 중 하나를 무조건적으로 따르려고 한다기 보다는, MVVM과 MVI 패턴 그 중간 어딘가에 위치한 그런 아키텍처를 선호합니다. (안드로이드 커뮤니티에서는 이를 단방향 데이터 흐름 아키텍처 (Unidirectional Data Flow Architecture)라고 부릅니다.) 또 구글 아키텍처 가이드 문서를 보면 이 방법으로 추천해주는 것 같습니다.

0개의 댓글