Hyeyeon blog

MVI Architecture Pattern in Android 본문

개발/Android

MVI Architecture Pattern in Android

Hyeyeon.P 2025. 3. 1. 20:05
반응형

1. MVI 패턴이란?

MVI (Model-View-Intent) 패턴은 단방향 데이터 흐름을 기반으로 하는 아키텍처 패턴입니다.

사용자의 입력(Intent)을 받아 애플리케이션의 상태(State)를 변경하고, 그 상태를 기반으로 UI(View)를 렌더링하는 구조를 갖습니다.

이를 통해 복잡한 상태 관리를 단순화하고 UI의 동작을 예측 가능하게 만들어 디버깅과 유지보수를 용이하게 만듭니다.  

2. Compose + MVI 

1) 단방향 데이터 흐름

Compose의 UI는 상태(State)에 따라 변하며 상태 변화가 있을 때마다 UI가 재구성됩니다.

MVI는 단방향 데이터 흐름을 사용하여 상태를 관리하기 때문에 Compose의 흐름과 잘 맞습니다. 

2) 불변 상태 관리

Compose는 상태를 불변(immutable)하게 관리하며, 상태가 변경될 때마다 새로운 상태 객체를 생성하여 UI를 업데이트합니다.

MVI 또한 상태를 불변 객체로 관리하며 상태 변화를 Intent를 통해 처리합니다. 

3. MVI의 구성 요소

1) Model

  • State와 State 업데이트 로직을 관리합니다. 

2) View

  • Model에서 전달받은 State를 기반으로 UI를 렌더링합니다. 

3) Intent

  • 사용자의 이벤트를 의미합니다. (ex: 버튼 클릭) 

4) ViewModel (옵션)

  • Intent를 처리하여 State를 업데이트 합니다. 
    ViewModel을 사용하지 않으면, Reducer 클래스에서 State를 업데이트합니다. 

4. MVI 패턴의 장점

1) 단방향 데이터 흐름

  • 데이터가 한 방향으로만 흐르므로 상태 관리가 명확해지고 디버깅이 용이합니다. 

2) 상태 기반 UI

  • View는 상태(State)만을 기반으로 UI를 렌더링하여 UI와 비즈니스 로직이 분리됩니다. 

3) 불변성 (Immutability)

  • 상태를 불변 객체로 관리하여 앱의 안정성이 높아집니다. 

4) 테스트 용이성

  • 비즈니스 로직과 UI가 분리되어 있어 ViewModel과 Model을 단위 테스트하기 쉬워집니다. 
  • Intent → State 변화 과정을 쉽게 테스트할 수 있습니다. 

5. MVI 패턴의 단점

1) 복잡성 증가

  • 단순한 앱에서는 과도한 아키텍처가 될 수 있습니다. 
  • 초기 세팅이 복잡하며 러닝 커브가 있습니다. 

2) 성능 이슈

  • 상태 업데이트가 많을 경우 불필요한 리렌더링이 발생하여 성능 저하가 발생할 수 있습니다. 
  • 최적화된 상태 관리 전략이 필요합니다. 

6. MVI 패턴 구현 예제

버튼을 클릭하여 카운트업/다운 하는 예제입니다. 

🔹 Step 1: Model 정의 (State)

data class CounterState(val count: Int = 0)

🔹 Step 2: Intent 정의

사용자 이벤트를 나타내는 Intent 클래스를 생성합니다.

sealed class CounterIntent {
    object Increment : CounterIntent()
    object Decrement : CounterIntent()
}

🔹 Step 3: ViewModel 구현

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class CounterViewModel : ViewModel() {
    private val _state = MutableStateFlow(CounterState())
    val state: StateFlow<CounterState> = _state

    fun processIntent(intent: CounterIntent) {
        viewModelScope.launch {
            when (intent) {
                is CounterIntent.Increment -> _state.value = CounterState(_state.value.count + 1)
                is CounterIntent.Decrement -> _state.value = CounterState(_state.value.count - 1)
            }
        }
    }
}

🔹 Step 4: View 구현 (Jetpack Compose)

class MainActivity : ComponentActivity() {
    private val counterViewModel: CounterViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MVIDemoTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    CounterScreen(viewModel = counterViewModel)
                }
            }
        }
    }
}

@Composable
fun CounterScreen(viewModel: CounterViewModel) {
    val state by viewModel.state.collectAsState()

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(text = "Count: ${state.count}")
        Spacer(modifier = Modifier.height(16.dp))
        Row {
            Button(onClick = { viewModel.processIntent(CounterIntent.Decrement) }) {
                Text(text = "-1")
            }
            Spacer(modifier = Modifier.width(16.dp))
            Button(onClick = { viewModel.processIntent(CounterIntent.Increment) }) {
                Text(text = "+1")
            }
        }
    }
}
728x90
Comments