MVI 패턴을 이해하기

thsamajiki·2024년 8월 1일
0

디자인 패턴

목록 보기
6/6

MVI 패턴에 관심을 가지게 된 계기

토이 프로젝트에서 Jetpack Compose를 다루기 시작하면서 Compose에 대해 공부하는 시간이 늘어나고 있다.

Compose로 개발을 하면서 느낀 건 바로 State 관리의 중요성이다. Compose는 기존의 안드로이드 View(xml)과 같은 명령형 UI가 아닌 선언형 UI다. 이미 선언된 UI에 표시되는 데이터를 변경하기 위해서는 State를 변경하여 Recomposition(재구현)을 진행해야 한다.

즉, Compose를 사용하면서 State를 다루지 않겠다는 건 화면 UI에 초기 데이터 이후 어떤 데이터 변경도 표시하지 않겠다는 이야기다.

이렇게 State 관리의 중요성을 알다보니 생각보다 꽤 머리가 복잡해졌다. 여러 이유가 있지만 그 중 하나는 가장 보편적으로 쓰이는 MVVM 패턴이 Compose에 과연 잘 맞나? 하는 의문이 들기 시작한 것이다.

내가 가장 크게 느낀 부분은 바로 생명주기의 차이이다.

Composable 구성요소들은 State가 변경되는 Recomposition이 일어나면서 재구성이 일어나면서 새로 생성된다. 하지만 ViewModel은 종속된 Activity나 Fragment가 완전히 종료될때까지 동일한 인스턴스를 호출한다.

그래서 만약 ViewModel에서 특정 Composable의 생명주기에 의존하는 값이나 메서드를 가지고 있을 경우 의도치 않은 다른 결과를 불러올 수도 있다.

실제로 안드로이드 공식 문서에서도 이 점에 대해서 주의할 것을 명시하고 있다.

class GreetingActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MaterialTheme {
                Column {
                    GreetingScreen("user1")
                    GreetingScreen("user2")
                }
            }
        }
    }
}

@Composable
fun GreetingScreen(
    userId: String,
    viewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
) {
    val messageUser by viewModel.message.observeAsState("")
    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

위처럼 GreetingScreen이 서로 다른 userId 값을 값고 2번 호출되어도 GreetingViewModel은 GreetingActivity가 완전히 종료되기 전까지 동일한 인스턴스를 반환하기 때문에 2번의 GreetingScreen은 모두 "user1"에 대한 인사말을 출력하게 된다.



MVI 패턴의 이해

찾아보니 MVI 패턴을 사용하면 State(상태) 관리를 좀 더 쉽게 다룰 수 있다는 이야기를 들었고 Compose로 UI를 구성한 앱에서 효과적인 디자인 패턴이라는 아닐까하는 생각이 들었다.

MVI 패턴은 Model, View, Intent 크게 3가지 구성요소로 이루어져 있다.
이 패턴은 사용자의 의도(Intent)를 명확하게 표현하여 앱의 상태를 관리한다.

Model: UI에 반영될 상태를 의미한다. 데이터를 의미하는 MVP, MVVM에서의 정의와는 다르다.
View: UI 그 자체. View, Activity, Fragment, Compose 등이 될 수 있다.
Intent: MVI에서는 사용자 액션 및 시스템 이벤트에 따른 결과라고 해석할 수 있다. (안드로이드에서 흔히 다루는 그 Intent와는 다르다.)

쉽게 설명하자면, User(사용자)가 View를 클릭하거나 하는 등의 행동(Action)을 취하면 이 행동이 Intent(의도)가 되어서 Model에 전달된다. 이 Intent가 Model, 즉 상태를 업데이트한다. 그리고 이 변경된 상태가 다시 View에 반영된다.



MVI 패턴의 특징

MVI의 핵심은 단방향 흐름(Uni-directional Flow) 구조이다. 그래서 다음 그림과 같이 표현하기도 한다.

Intent는 사용자의 액션을 Model로 전달하고, Model은 이를 처리하여 새로운 상태를 생성한 후, 이 상태는 View를 통해 사용자에게 표시되기 때문이다.

이 과정에서 모든 상태 변화는 예측 가능하며, 앱의 상태는 항상 일관된 상태를 유지한다.

또한, MVI 패턴은 상태의 변화를 중앙에서 관리함으로써 디버깅과 테스트가 용이해진다.

예를 들어, 사용자의 클릭과 같은 이벤트가 Intent로 변환되어 Model에 전달되고, Model은 이를 처리하여 새로운 View 상태를 생성한다.



MVI 패턴의 장단점

장점

  1. 상태 객체는 불변이므로 스레드로부터 안전하다.
  2. State, Event, Effect 등의 모든 액션이 같은 파일에 있어 화면에서 일어나는 일을 한눈에 쉽게 이해할 수 있다.
  3. 상태를 유지하는 것이 쉽다.
  4. 데이터 흐름이 단방향으로 흐르기 때문에 추적이 쉽다.

단점

  1. 많은 보일러 플레이트 코드가 발생한다.
  2. 많은 객체를 생성해야 하기 때문에 높은 메모리 관리가 필요하다.
  3. 하나의 화면에서 많은 뷰와 복잡한 로직을 가지게 될 경우,
    State는 거대해지고 이 State를 단지 하나를 사용하는 대신 StateFlow를 추가로 사용하여 더 작은 것으로 분할할 수 있다.



결론

MVI 패턴은 MVP, MVVM과 같은 MVx 패턴에 가장 최근에 추가된 패턴이다.
MVVM과 공통점이 많지만 상태 관리 측면에서 좀 더 구조화된 방법을 갖고 있어 Compose와 좀 더 맞는 패턴이지 않을까 생각된다. 좀 더 공부해봐야겠다!

profile
안드로이드 개발자

0개의 댓글