아키텍처패턴은 변천사가 참 많다. MVC -> MVP -> MVVM에 이어 MVI패턴이다. 사실상 MVI패턴을 늦게 공부를 시작한 감이 없지않아 있지만 깨닫고 느낀 부분을 간단하게 정리하려 한다.
[참고]
자세한 사항은 이전에 정리해놓은 MVC, MVP, MVVM아키텍처 패턴의 변천사를 보면 도움이 될듯 합니다.
https://velog.io/@squart300kg/mvvmComplete
[MVC]
Model View Controller로 이뤄진 이 패턴은 안드로이드의 View와 Controller가 거의 동일하다고 보면 된다. ViewSystem기준, View의 경우는 xml의 뷰를 의미하며 Controller의 경우는 Activity
나 Fragment
를 의미한다. 따라서 View와 Controller가 거의 같다고도 볼 수 있다.
Model의 경우는 비즈니스 로직이 정의되는 영역이다. MVC패턴 초기 개발시엔 Agent
라는 클래스를 설계하고 이 곳에 비즈니스 로직을 정의하는 경우가 많았다. 또는 Activity
or Fragment
에 비즈니스 로직이 그냥 정의되는 경우가 많았다.
이는 아래와 같은 문제점들을 야기할 수 있다.
[MVP]
MVC패턴의 단점을 보안하기 위해 나온 패턴이다. 이는 하나의 View만의 전용 UI갱신 모듈인 Presenter를 둔다. 이를 통해 하나의 화면에서 UI가 어떤 이벤트를 받고 UI를 어떻게 갱신하는지를 파악하는데 좀 더 용이하다는 장점이 있다.
하지만 MVP패턴의 치명적인 단점은 View와 Presenter의 강력한 결합으로 인해 UI의 단위테스팅이 매우 어렵다는 문제를 갖고 있다. MVP패턴을 설계할 땐, Presenter인터페이스를 두고 이 하위에 View와 Presenter클래스를 설계하게 된다. 그리고 이들은 각각 Activity
와 Presenter
클래스에 오버라이딩된다. 그 후, 이들은 상호 의존을 가지게 된다.
사용자의 이벤트를 Presenter에게 알리기 위해 View는 Presenter의 의존이 필요하며, 그 반대로 UI를 갱신하기 위해 Presenter는 View를 의존하게 되는 것이다.
이런 문제로 인해 View와 Presenter의 단위테스팅이 어렵다는 단점이 있다.
[MVVM]
MVP패턴과 거의 유사하다고 볼 수 있다. 하나의 View는 한개의 ViewModel을 갖게 된다. 이로 인해 MVP패턴처럼 ViewModel에서 한 화면의 사용자 이벤트를 받을 수 있고 이를 통해 UI를 갱신할 수 있다는 점에서는 매우 비슷하다고 볼 수 있다.
하지만 결정적인 차이는 단방향 데이터 흐름을 염두한 DataBinding을 사용하고 옵저빙 방식으로 UI를 갱신한다는 점이 큰 차이점이다. 이러한 아키텍처는 View는 ViewModel에 의존성을 갖지 않게 되며, 이로 인해 View와 ViewModel의 단위테스팅이 매우 용이해진다는 장점이 있다.
즉, MVP패턴과 MVVP패턴의 차이점은 아래와 같다고 생각한다.
단방향 데이터 흐름인 DataBinding을 사용하여 UI를 갱신한다. 이는 옵저빙 방식으로 UI를 갱신하며 ViewModel의 View를 참조하지 않음으로 인해, View와 ViewModel의 단위테스팅이 가능하다는 점이다.
MVVM패턴을 무지몽매하게 사용하는 입장에서 MVVM패턴의 단점을 생각해보진 않았다. 다만, MVI패턴을 공부하고나니 MVVM패턴의 단점이 보이기 시작했다.
앱이 커짐에 따라 화면이 복잡해진다. 그로 인해 어느 부분이 사용자의 이벤트인지, 그 이벤트에 따라 UI의 어떤 부분을 갱신하는지를 알기가 어려워진다는 문제가 있다. 이를 이해하기 위해선 앱을 클릭하며 하나하나 힘들게 코드를 읽으며 따라가야 한다.
하지만 MVI패턴은 이를 용이하게 해준다. 사용자가 어떤 이벤트를 발생시켰는지를 모델링하며, 그로 인해 화면의 상태가 어떻게 바뀔 수 있는지도 모델링을 한다. 게다가 이렇게 모델링한 클래스들이 하나의 파일에 들어감에 따라 코드를 읽으며 앱을 파악하는데에도 더욱 용이하게 된다는 장점이 있다.
이제부터 공부한 MVI패턴을 최대한 사실에 입각하여 정리해보려 한다.
MVI패턴은 3가지 핵심 요소를 가지고 있다.
[UiEvent] : 사용자의 이벤트를 모델링한 컴포넌트를 의미한다. 해당 컴포넌트를 통해 하나의 화면에서 사용자가 어떤 이벤트를 트리거했는지 알 수 있다. 아래와 같이 말이다.
sealed interface UiEvent {
data object ThumbnailClicked: UiEvent
data object ContentClicked: UiEvent
data object ChattingInputClicked: UiEvent
}
[UiState]
사용자가 UiEvent를 통해 앱 이벤트를 트리거 한다. 그러면 앱은 일련의 비즈니스 로직을 실행시키고 최종적으로 UI를 갱신하게 된다. 이렇게 갱신되는 UI의 경우의 수를 정의한 컴포넌트가 바로 UiState이다. 아래와 같이 말이다.
sealed interface UiState {
data object Idle: UiState
data object Loading: UiState
data class Loaded(
val communityList: List<Community>
): UiState {
data class Community(
val name: String
val maxManCount: Int
val currentManCount: Int
)
}
}
[UiEffect]
사실상 해당 컴포넌트는 개인적으로 이해가 잘 안되기도 한다. 내가 공부한 바에 따르면 이는 UI에서 단 1번만 발생할 수 있는 경우를 정의한다고 한다. 예를 들어, 오류가 발생하여 토스트 메시지를 띄워주는 경우가 있다고 한다. (참고 자료) 아무튼 해당 컴포넌트는 아래와 같이 사용된다고 한다.
sealed interface UiEffect {
data object: UiEffect
}
MVI패턴으론 간접적으로만 경험해 보았다. Now In Android를 겪어보며 UI의 상태를 모델링하고 그에 맞게 로직을 분기처리하는식으로 개발을 해보았다. 다만, MVI패턴은 사용자의 이벤트까지 모델링하여 앱을 좀 더 구조화하며 상태관리를 좀 더 신경쓰는 패턴임을 알게되었다.
참고 : https://proandroiddev.com/mvi-architecture-with-kotlin-flows-and-channels-d36820b2028d
참고 : https://velog.io/@squart300kg/mvvmComplete