MVI 패턴

eunsong·2024년 11월 28일

Android

목록 보기
8/9

1. MVVM 패턴의 한계?

1.1 복잡한 데이터의 흐름

MVVM은 View 와 ViewModel 간의 양방향 데이터 흐름을 허용합니다.

  • View에서 발생한 이벤트는 ViewModel로 전달되고, ViewModel에서 LiveData나 StateFlow 를 통해 상태를 업데이트하여 View 로 다시 전달됩니다.
  • 이 과정에서 데이터가 여러 방향으로 흐르며, 디버깅이 어려워지고, 코드의 유지보수성이 떨어질 수 있습니다.

1.2 상태 충돌 문제

MVVM에서는 ViewModel과 View가 각각 상태를 변경할 수 있습니다.

  • ex) 두 개의 View가 동일한 LiveData를 참조할 경우, 한쪽에서 상태를 업데이트하면 다른 쪽의 상태가 예상치 못하게 변경될 수 있습니다.

1.3 쓰레드 안전성 부족

  • MVVM 에서 LiveData나 MutableStateFlow 를 잘못 사용하면 다중 쓰레드 환경에서 비정상 동작을 유발할 수 있습니다.
  • UI 상태와 비즈니스 로직이 강하게 결합되어 있는 경우, 멀티스레드 동기화 문제를 겪을 수 있습니다.

2. MVI: MVVM의 한계를 개선하는 패턴

MVI는 이러한 문제를 해결하기 위해 단방향 데이터 흐름과 불변 상태 관리를 도입합니다. 모든 UI 상태는 단일 상태 객체(State)로 관리되고, 상태는 불변 객체로 유지된다.


3. MVVM vs MVI: 데이터 흐름 비교

MVVM의 데이터 흐름(양방향)

1. View → ViewModel (사용자 입력 전달)
2. ViewModel → View (상태 업데이트 전달)

데이터는 View와 ViewModel 사이에서 자유롭게 이동 (양방향).
class MainViewModel : ViewModel() {
    val text = MutableLiveData<String>()

    fun updateText(newText: String) {
        text.value = newText
    }
}
@Composable
fun MainScreen(viewModel: MainViewModel) {
    val text by viewModel.text.observeAsState("")

    Column {
        TextField(value = text, onValueChange = { viewModel.updateText(it) })
        Text(text = "You typed: $text")
    }
}

문제점

  • 양방향 데이터 흐름: View와 ViewModel 간의 데이터 교환이 많아 디버깅이 어렵습니다.
  • 상태 충돌 가능성: View 와 ViewModel이 동시에 상태를 변경할 수 있습니다.

MVI의 데이터 흐름(단방향)

1. View → Intent (사용자 이벤트 전달)
2. Intent → State (ViewModel에서 상태 업데이트)
3. State → View (View에서 상태를 구독하여 렌더링)

데이터는 항상 한 방향으로만 흐름 (단방향)
// 1. 상태(state) 정의
data class UiState(
	val isLoading: Boolean = false,
    val data: List<String> = emptyList(),
    val error: String? = null
)

// 2. Intent 정의
sealed class UiIntent {
	object LoadData : UiIntent()
    data class Search(val query: String) : UiIntent()
}

// 3. ViewModel에서 상태 관리
class MainViewModel : ViewModel() {
	private val _state = MutableStateFlow(UiState())
    val state: StateFlow<UiState> = _state
    
    fun processIntent(intent: UiIntent) {
        when (intent) {
            is UiIntent.LoadData -> loadData()
            is UiIntent.Search -> search(intent.query)
        }
    }
    
    private fun loadData() {
        viewModelScope.launch {
            _state.value = UiState(isLoading = true)
            // 데이터 로드 로직 (예: 네트워크 호출)
            val data = listOf("Item 1", "Item 2", "Item 3")
            _state.value = UiState(data = data)
        }
    }
    
    private fun search(query: String) {
        viewModelScope.launch {
            val filteredData = listOf("Filtered Item 1")
            _state.value = UiState(data = filteredData)
        }
    }
}

// 4. View에서 상태 구독 및 렌더링
@Composable
fun MainScreen(viewModel: MainViewModel) {
    val state by viewModel.state.collectAsState()

    Column {
        if (state.isLoading) {
            Text("Loading...")
        } else if (state.error != null) {
            Text("Error: ${state.error}")
        } else {
            state.data.forEach { item ->
                Text(item)
            }
        }

        Button(onClick = { viewModel.processIntent(UiIntent.LoadData) }) {
            Text("Load Data")
        }
    }
}

4. MVI의 장점

1. 단방향 데이터 흐름

  • 데이터가 한 방향으로만 흐르므로 상태 변경 과정을 쉽게 추적할 수 있습니다.
  • 디버깅과 유지보수가 용이합니다.

2. 상태 관리의 명확성

  • MVI는 모든 UI 상태를 단일 불변 객체(State)로 관리하며, 상태 변경은 항상 ViewModel에서만 처리됩니다.
    View나 다른 컴포넌트에서 상태를 직접 변경할 수 없으므로, 상태 충돌을 방지할 수 있습니다.
  • 제한 사항
    - 상태 관리의 명확성은 상태(State) 설계와 Intent 처리 로직에 의존합니다.
    - 잘못된 상태 모델링(예: 과도하게 큰 State 객체)이나 비효율적인 상태 갱신 방식은 코드 복잡도를 증가시킬 수 있습니다.
    - 상태 충돌을 방지하려면 명시적인 Intent-Reducer 패턴을 적용하고, 비동기 작업의 순서를 관리해야 합니다.

3. 쓰레드 안전성

  • 상태가 불변 객체로 관리되므로 멀티스레드 환경에서도 안전합니다.

  • Intent 처리 로직이 순차적으로 실행되므로, 멀티스레드 문제를 줄일 수 있습니다.

  • 제한 사항
    - Intent 처리 로직이 명시적으로 직렬화되지 않으면 상태가 비정상적으로 변경될 수 있습니다.
    - 예를 들어, ViewModel에서 여러 코루틴을 병렬로 실행하거나, 상태 갱신 로직에서 Mutable 객체를 사용할 경우 멀티스레드 문제가 발생할 수 있습니다.
    - MVI에서 멀티스레드 안전성을 보장하려면, 상태 갱신은 항상 단일 스레드 컨텍스트에서 처리되어야 하며, Intent 순서를 명시적으로 관리해야 합니다.

4. 테스트 용이성

  • Intent → State 변환 로직을 독립적으로 테스트할 수 있습니다.

상태 충돌 문제

상태 충돌(State Collision)이란, 여러 이벤트(Intent)가 동시에 발생하거나 비동기 작업의 결과가 순서대로 처리되지 않으면서 최종 상태(State)가 의도와 다르게 갱신되는 문제를 말합니다.

상태 충돌이 발생하는 상황

  • 비동기 작업 중복 처리
    동시에 여러 비동기 작업이 실행되면서 결과가 덮어씌워질 수 있습니다.
  • 이벤트 순서 왜곡
    Intent의 처리 순서가 보장되지 않아, 나중에 발생한 이벤트가 먼저 처리되는 문제가 발생할 수 있습니다.
  • 상태 갱신의 명확성 부족
    상태 변경이 여러 위치에서 발생하면, 최종 상태를 추적하기 어려워집니다.

MVI(Model-View-Intent) 패턴은 Intent-Reducer 패턴을 활용해 상태(State) 관리와 상태 충돌 문제를 근본적으로 해결할 수 있습니다.


Intent-Reducer 패턴의 핵심 원리

Intent

사용자의 동작(예: 버튼 클릭)이나 시스템 이벤트(예: 네트워크 요청 완료)를 명시적으로 표현한 객체입니다.
모든 이벤트는 Intent로 표현되며, ViewModel은 Intent를 받아 상태를 처리합니다.

Reducer

Intent를 기반으로 현재 상태를 변경하여 새로운 상태를 반환하는 순수 함수(Pure Function)입니다.
Reducer는 이전 상태를 받아 Intent에 따라 새로운 상태(State)를 생성합니다.
상태 갱신은 Reducer를 통해서만 이루어지므로, 상태 변경 과정을 예측 가능하게 만듭니다.


다음 글에 좀 더 자세히 다뤄보겠습니다.

profile
A place to study and explore my GitHub projects: github.com/freeskyES

0개의 댓글