MVI는 많이 들어봤지만 그동안 MVVM 아키텍쳐로 충분히 잘 써왔기 때문에 적용해본 적은 없는 아키텍쳐이다.
최신 아키텍쳐인 MVI 아키텍쳐(정확히 말하면 Mavericks)의 개념에 대해 정리해보려고 한다.
MVI는 Model-View-Intent의 약자이다. MVI는 MVC, MVP, MVVM과 매우 다른 방식으로 작동한다. 각 요소가 어떤 역할을 하는지 살펴보자~
모델은 상태를 나타낸다. MVI의 모델은 아키텍쳐의 다른 레이어와의 단방향 데이터 흐름을 보장하기 위해 변경이 불가능해야 한다.
View를 나타내며 하나 이상의 Activity나 Fragment로 구현된다.
사용자 또는 앱내 발생하는 Action을 나타낸다. 모든 Action에 대해 View는 Intent를 수신한다. Presenter는 Intent를 관찰하고 Model은 새로운 상태로 변환한다.
그럼 각 레이어를 더 자세하게 알아보자~
다른 아키텍쳐 패턴에서 모델은 DB나 API같은 백엔드와의 연결고리 역할을 하는 계층으로 구현된다.
하지만 MVI에서 모델은 앱의 상태를 나타낸다. 여기서 말하는 앱의 상태는 무엇일까?
reactive-programming에서 앱은 버튼 클릭이나 변수 값의 변화 등에 반응한다. 앱이 그런 변경 사항에 반응하면 새로운 상태로 전환된다. 새로운 상태는 프로그레스바나 새 영화 목록 같은 UI 변경이 있을 수 있다.
Model이 MVI에서 어떻게 동작하는지 한번 예시를 들어보자.
TMDB API를 이용하여 영화 순위 같은 것을 검색하길 원한다고 가정해보자. 만약 MVP 아키텍쳐로 만들어진 앱이라면 모델은 다음과 같은 클래스가 될 것이다.
data class Movie(
var voteCount: Int? = null,
var id: Int? = null,
var video: Boolean? = null,
var voteAverage: Float? = null,
var title: String? = null,
var popularity: Float? = null,
var posterPath: String? = null,
var originalLanguage: String? = null,
var originalTitle: String? = null,
var genreIds: List<Int>? = null,
var backdropPath: String? = null,
var adult: Boolean? = null,
var overview: String? = null,
var releaseDate: String? = null
)
Presenter는 위 모델을 사용하여 다음과 같은 코드로 동영상 목록을 표시한다.
→ Presenter는 뷰에게 로딩 액션을 시작하고 만약 영화 목록이 불러와진다면 로딩 액션을 멈추고 영화 목록을 띄우도록 명령하고 있다.
class MainPresenter(private var view: MainView?) {
override fun onViewCreated() {
view.showLoading()
loadMovieList { movieList ->
movieList.let {
this.onQuerySuccess(movieList)
}
}
}
override fun onQuerySuccess(data: List<Movie>) {
view.hideLoading()
view.displayMovieList(data)
}
}
하지만 여기에는 MVI로 해결할 수 있는 몇가지 이슈가 있다.
이런 이슈를 해결하기 위해 MVI는 모델이 데이터가 아닌 상태를 나타내도록 한다. 위에 사용된 예제의 경우 상태를 나타내는 데이터를 아래와 같이 표현할 수 있다.
sealed class MovieState {
object LoadingState : MovieState()
data class DataState(val data: List<Movie>) : MovieState()
data class ErrorState(val data: String) : MovieState()
data class ConfirmationState(val movie: Movie) : MovieState()
object FinishState : MovieState()
}
MovieState 라는 모델이 로딩, 받아온 데이터, 오류 등의 앱의 상태를 가지고 있다.
이런 식으로 Model을 만들 경우 더이상 상태를 View 또는 Presenter나 ViewModel과 같이 여러 곳에서 관리할 필요가 없다. Model 자체가 언제 프로그레스바를 표시하고 언제 아이템 리스트를 표시해야 할지를 가리키게 된다.
이런 Model을 적용하면 Presenter는 다음과 같이 바뀔 수 있다.
class MainPresenter {
private val compositeDisposable = CompositeDisposable()
private lateinit var view: MainView
fun bind(view: MainView) {
this.view = view
compositeDisposable.add(observeMovieDeleteIntent())
compositeDisposable.add(observeMovieDisplay())
}
fun unbind() {
if (!compositeDisposable.isDisposed) {
compositeDisposable.dispose()
}
}
private fun observeMovieDisplay() = loadMovieList()
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { view.render(MovieState.LoadingState) }
.doOnNext { view.render(it) }
.subscribe()
}
Presenter는 이제 View의 Model(상태)라는 하나의 아웃풋만을 갖는다.
여기서 주목해야할 것은 View의 render()라는 함수이다. 앱의 현재 상태를 가지고 있는 모델을 render() 함수가 전달 받아 뷰 그리기가 수행된다.
MVI에서의 Model이 갖는 또 다른 특징은 변경이 불가능하여 비즈니스 로직을 순수한 코드로 유지할 수 있다는 것이다. 이렇게 하면 여러 위치에서 모델이 수정되지 않고 앱의 전체 생명주기 동안 단일 상태를 유지한다.
아래 다이어그램은 다른 계층 간의 상호작용을 보여준다.

(흠 근데 이해가 안되는게 Presenter에서 View를 사용하고 있는데 그럼 의존하고 있는거 아닌가? render 하라고 알려주고 있는🤔 화살표 방향이 이해가 잘 안된다. MVI에는 Presenter의 존재가 없는데 MVP → MVI 로 발전되는 예시를 설명하다 보니 추가돼서 어색한 것 같기도 하당)
암튼 이 다이어그램에서 주목할 점은 바로 “순환 구조”라는 것이다.
모델의 불변성과 레이어 간의 순환구조 덕분에 다음과 같은 이점을 얻을 수 있다.
다음으로 View와 Intents에 대해서 살펴보자.
MVP와 같이 MVI는 일반적으로 Fragment나 Activity에서 구현되는 View의 Contract interface를 정의한다.
MVI의 Views는 렌더링할 하나의 상태를 허용하는 하나의 render() 함수를 구현하는 것이 일반적이다. MVP가 일반적으로 자세한 메소드 이름을 사용하여 입력과 출력을 정의하는 반면 MVI의 Views는 intent() 함수를 Observe하여 유저 반응에 응답한다.
MVI의 Intent는 android.content.Intent를 의미하는게 아니다. MVI의 Intents는 앱의 상태를 변화시킬 액션을 의미한다.
MVI의 Views가 어떻게 작동하는지 다음 코드로 봐보자.
class MainActivity : MainView {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
//1
override fun displayMoviesIntent() = button.clicks()
//2
override fun render(state: MovieState) {
when(state) {
is MovieState.DataState -> renderDataState(state)
is MovieState.LoadingState -> renderLoadingState()
is MovieState.ErrorState -> renderErrorState(state)
}
}
//4
private fun renderDataState(dataState: MovieState.DataState) {
//Render movie list
}
//3
private fun renderLoadingState() {
//Render progress bar on screen
}
//5
private fun renderErrorState(errorState: MovieState.ErrorState) {
//Display error mesage
}
}
각 세션을 차례대로 살펴보자
이 예제는 View의 render()가 Presenter로부터 어떻게 State를 수신하는지와 버튼 클릭으로 Intent가 발생하는 것을 보여준다. 그 결과는 에러메시지 출력이나 로딩화면과 같은 UI 변화로 보여지게 된다.
MVI를 제대로 이해하려면 멀티쓰레드, RxJava에 대한 지식을 요한다고 한다. 따라서 이번 글은 MVI의 대략적인 개념 정리 글이라고 보면 좋을 것 같다~!