MVI가 도대체 뭔데!

berry·2024년 9월 19일

MVI는 많이 들어봤지만 그동안 MVVM 아키텍쳐로 충분히 잘 써왔기 때문에 적용해본 적은 없는 아키텍쳐이다.

최신 아키텍쳐인 MVI 아키텍쳐(정확히 말하면 Mavericks)의 개념에 대해 정리해보려고 한다.


MVI는 뭘까?

MVI는 Model-View-Intent의 약자이다. MVI는 MVC, MVP, MVVM과 매우 다른 방식으로 작동한다. 각 요소가 어떤 역할을 하는지 살펴보자~

Model

모델은 상태를 나타낸다. MVI의 모델은 아키텍쳐의 다른 레이어와의 단방향 데이터 흐름을 보장하기 위해 변경이 불가능해야 한다.

View

View를 나타내며 하나 이상의 Activity나 Fragment로 구현된다.

Intent

사용자 또는 앱내 발생하는 Action을 나타낸다. 모든 Action에 대해 View는 Intent를 수신한다. Presenter는 Intent를 관찰하고 Model은 새로운 상태로 변환한다.

그럼 각 레이어를 더 자세하게 알아보자~

Models

다른 아키텍쳐 패턴에서 모델은 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로 해결할 수 있는 몇가지 이슈가 있다.

  • Multiple inputs: MVP와 MVVM에서 Presenter와 ViewModel은 많은 수의 입출력을 관리해야 하는 경우가 많다. 이건 많은 백그라운드 태스크가 있는 앱에서는 문제를 일으킬 수 있다.
  • Multiple states: MVP와 MVVM에서 비즈니스 로직과 View는 언제든 다른 상태를 가질 수 있다. 개발자는 자주 Observable 변수와 Observer 콜백의 상태를 동기화시킨다. 하지만 이건 행위의 충돌을 야기할 수 있다.

이런 이슈를 해결하기 위해 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 로 발전되는 예시를 설명하다 보니 추가돼서 어색한 것 같기도 하당)

암튼 이 다이어그램에서 주목할 점은 바로 “순환 구조”라는 것이다.

모델의 불변성과 레이어 간의 순환구조 덕분에 다음과 같은 이점을 얻을 수 있다.

  • 단일 상태: 불변 데이터 구조는 다루기 쉽고 한 곳에서 관리되기 때문에 앱 내 모든 레이어 간 하나의 단일 상태를 보장할 수 있다.
  • Thread Safety: 이 부분은 RxJava나 LiveData와 같은 라이브러리를 사용하는 Reactive앱에서 특히 유용하다. 어떤 함수도 모델을 수정할 수 없기 때문에 모델은 항상 한곳에서 다시 만들어지고 유지된다. 이런 점은 다른 쓰레드에서 모델을 수정하여 일어나는 충돌을 방지한다.

다음으로 View와 Intents에 대해서 살펴보자.

Views 와 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
    }
}

각 세션을 차례대로 살펴보자

  1. displayMovieIntent: UI 이벤트를 적합한 Intents에 바인드한다. 이 경우엔 Intents에 버튼 클릭 이벤트가 바인드 되었다. 이는 MainView 인터페이스에 정의되었고 MainView를 구현한 MainActivity에서 override되었다. 위 코드에선 RxBinding을 사용하여 버튼 클릭 리스너를 RxJava Observables로 변환했다고 한다. <- 뭔 소리지 ㅎ
  2. render: ViewState를 View의 알맞은 메소드와 매핑한다. 이 함수 역시 MainView에 정의되어 있다.
  3. renderDataState: Model의 데이터를 View에 그린다. 데이터는 날씨, 영화 또는 에러에 관한 내용일 수 있다. 일반적으로 내부 메소드로 정의되며 State에 기반하여 화면을 갱신한다.
  4. renderLoadingState: 로딩화면을 View에 그린다.
  5. renderErrorState: 에러 메시지를 View에 그린다.

이 예제는 View의 render()가 Presenter로부터 어떻게 State를 수신하는지와 버튼 클릭으로 Intent가 발생하는 것을 보여준다. 그 결과는 에러메시지 출력이나 로딩화면과 같은 UI 변화로 보여지게 된다.


MVI를 제대로 이해하려면 멀티쓰레드, RxJava에 대한 지식을 요한다고 한다. 따라서 이번 글은 MVI의 대략적인 개념 정리 글이라고 보면 좋을 것 같다~!


참고 문서

https://jaehochoe.medium.com/%EB%B2%88%EC%97%AD-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EB%A5%BC-%EC%9C%84%ED%95%9C-mvi-model-view-intent-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-165bda9dfbe7

profile
공부 내용 기록

0개의 댓글