-
Android) MVI 아키텍처 살펴보기Android 2021. 9. 26. 17:55
Android 개발자로서 일반적으로 사용되는 패턴으로 MVC, MVP, MVVM이 있습니다. 이 패턴들은 명령형 프로그래밍 접근 방식을 사용합니다. 이 접근 방식을 사용하면 안드로이드에서 발생하는 대부분의 문제가 해결되지만, thread safety 또는 state 관리에 관련해서 여전히 몇 가지 문제에 직면할 수 있습니다.
MVVM의 문제점?
- 기존에 많이 사용하는 MVVM 패턴을 적용하면서 직면하는 문제는 무엇일까요? 바로 상태 문제와 부수 효과입니다.
- Multiple Inputs : ViewModel은 많은 input, output을 관리해야 하는 경우가 있습니다. 이때 백그라운드 스레드를 사용하게 되면 thread safety 하지 못한 문제가 발생할 수 있습니다.
- Multiple States : 복잡하게 분산된 상태와 복잡한 비즈니스 로직을 가질 수 있습니다. 이를 Observer Pattern을 이용해 상태를 동기화 하는 방법을 사용하는데, 이 또한 행위의 충돌을 일으킬 수 있습니다. 상태 정보가 뷰와 모델 두 곳에 존재하기 때문에 다중 스레드 및 다양한 입력에 의해 모델에 대한 업데이트가 동시에 발생할 수도 있는 멀티스레드나 동적 환경에서는 이들 상태를 관리 및 보장하기 힘듭니다.
상태 문제
- 앱은 상태 제어와 큰 연관성이 있습니다. 상태를 관리하기 힘들어지고 의도하지 않은 방향으로 갈 때 이것을 상태 문제라고 합니다.
- 예를 들어 아래와 같이 API의 응답을 성공적으로 리스트에 출력했지만, 상태 문제가 발생해 계속해서 프로그래스 바가 화면에 보이는 상황이 있습니다.
부수 효과
- 부수 효과(Side Effect)는 원래의 목적과 다르게 다른 효과 또는 부작용이 나는 상태를 지칭합니다.
- 안드로이드에서 서버 호출, 데이터베이스 접근 등에서 부수효과가 발생해 그에 따른 상태 변경에 어려움을 겪을 수 있습니다.
MVI(Model-View-Intent)의 등장
- Hannes Dorfman에 의해 JavaScript용 Redux 라이브러리를 기반으로 한 MVI가 만들어졌습니다.
Key Concepts
- Unidirectional cycle of data
- Non-blocking
- Immutable state
- MVI는 MVP, MVVM 같은 다른 패턴과 유사하지만 의도(Intent)와 상태(State)라는 두 가지 새로운 개념이 도입되었습니다.
- MVI는 사용자 또는 시스템의 상호 작용을 기반으로 Model이 업데이트 되고 Model에서 방출된 상태를 기반으로 View가 업데이트되는 반응형 아키텍처 패턴입니다.
- MVI는 Cycle.js 자바스크립트 프레임워크에서 영감을 받았습니다.
- 단방향(Unidirectional) 및 순환(Circular Data Flow)의 원칙을 기반으로 작동합니다.
- MVI 아키텍처를 적용한 라이브러리는 대표적으로 Airbnb의 Mavericks가 있습니다.
- 사용자가 객체를 발행하는 행위가 기본적으로 앱의 상태를 변화시키고자 하는 의도를 나타낸다고 규정하고 이를 Intent라고 부릅니다.
State
- State는 Immutable한 데이터 구조
- 어느 시점이건 앱에는 현재의 시점을 표현하는 하나의 상태만 존재하고 그걸 바꿀 수 있는 유일한 트리거는 새로운 State를 만드는 Intent입니다.
- 즉 UI의 변화는 곧 Intent 실행의 결과가 됩니다.
- 만약 Configuratin Change(화면 전환, 언어 변경 등)이 발생하면 어떻게 처리할까? -> 가장 최근의 State를 표시하면 됩니다.
Redux
- React 기반 프런트엔드에서 주로 사용되는 상태 관리 패턴
- 복잡한 상태를 쉽게 관리하기 위해 사용
- 한 곳에서 모든 상태를 관리
- 상태는 불변 타입
- 상태 변화는 순수 함수로 작성
UDA(Uni-Directional-Architecture)
- UDA는 하나의 아키텍처로 이해하는 것보단, 앱을 디자인하는 방법 또는 아키텍처의 특성이라고 생각이 됩니다.
- 데이터의 흐름이 한 방향으로 진행되고, 상태는 모델에서만 관리된다면 UDA라고 할 수 있습니다.
- 대표적으로 MVC(Mode-View-Controller) 패턴이 가장 단순한 형태의 UDA입니다.
Model-View-Intent
Model
- MVC, MVP, MVVM 패턴에서 지칭되는 Model은 일반적으로 비지니스 로직 표현과 데이터를 저장하기 위한 공간, 데이터베이스나 API 같은 서버와의 통신에서 비즈니스 로직의 응답을 처리하기 위한 연결고리 역할을 합니다. 즉 View가 화면에 그려줘야 할 것들을 알려주는 Entity로 정의되었습니다.
- 반면에 MVI 패턴에서 Model은 데이터를 가지고 있으며 또한, UI의 상태를 나타냅니다. 예를 들어 UI는 Data Loading, Loaded, Change in UI with User Action, Error 등 다양한 상태를 가질 수 있습니다.
- 즉 Model은 상태에 대해 뷰가 어떤 것을 화면에 렌더링 해야 하는지를 말해주는 응답입니다.
- 아래의 코드처럼 Model을 데이터가 아닌 상태를 나타내도록 합니다. 이런식으로 Model을 정의해준다면 View, ViewModel 등 여러 곳에서 상태를 관리해줄 필요가 없어집니다.
sealed class MainState { object Idle : MainState() object Loading : MainState() data class Users(val user: List<User>) : MainState() data class Error(val error: String?) : MainState() }
View
- 새로운 State를 받아 화면에 표시하는 로직이 정의된 곳입니다.
- 사용자 상호작용에 대한 결과로 이벤트를 발생시켜 Intent에 전달합니다.
Intent
- Intent는 앱의 상태를 바꾸려는 의도를 의미합니다.
- 사용자의 버튼 클릭 등을 통한 모든 UI의 변화는 Intent 함수의 결과로 동작합니다.
- 이를 통해 앱에서 진행중인 작업에 대해 더 명확하게 이해할 수 있습니다.
그럼 MVI는 상태문제와 부수효과를 어떻게 해결할까?
- MVI에서 상태는 불변합니다. 이 말은 View를 업데이트 하기 위해서는 Intent()를 사용해 이벤트를 발생시켜 그에 따라 새로운 상태를 생성해 적용하는 것이 상태를 변경하기 위한 유일한 방법입니다.
- 또한, Model은 현재 뷰 상태에 대한 변경 불가능한 단일 소스(Single Source Of Truth)이므로 상태가 겹치지 않습니다.
- Single Source Of Truth로 비즈니스 로직을 유지하기 위해 Model은 불변성을 지니고 있어야 합니다. 이 방식은 앱의 전체 생명주기 동안 단일 상태를 유지할 수 있게 됩니다.
- 불변성을 가지고 있다는 의미는 Model을 수정할 수 없기 때문에, 내부의 property는 바꾸지 못하고 Model 자체를 copy() 메소드를 사용해서 새로 생성하는 방법을 사용합니다.
- Model이 Mutable 하다면 아래의 메서드를 호출해 앱의 상태를 쉽게 바꿀 수 있습니다.
-
viewModel.insert(items)
- Immutable 하다면 앱의 상태를 바꾸기 위해서 매번 새로운 Model을 만들어야 하는데, 만약 이전 상태에 대한 정보가 필요하다면?
State Reducer
- State Reducer는 이전 상태를 입력으로 받아 다음과 같이 이전 상태에서 새 상태를 계산하는 Reactive Programming의 개념입니다.
- 이 아이디어는 reduce() 메소드가 이전 상태를 foo와 결합하여 새 상태를 계산한다는 것입니다.
public State reduce( State previous, Foo foo ){ State newState; // ... compute the new State by taking previous state and foo into account ... return newState; }
State Reducer를 RxJava를 사용하여 리팩터링 한다면
- 앱의 새로운 상태를 나태내는 PartialState라는 새로운 상태가 필요합니다.
- 이전 상태 정보가 필요한 새로운 Intent가 있을 때, 완료된 상태로부터 새로운 PartialState를 만듭니다.
- reduce() 메서드에서 이전 상태와 PartialState를 사용하여 화면에 표시할 새로운 상태로 merge 합니다.
- RxJava의 scan() 메서드를 사용하여 앱의 초기 상태에 reduce() 메서드를 적용하고, 새 상태를 반환합니다.
MVI의 흐름
- 사용자가 의도(Intent)가 될 행동(Action)을 수행.
- Intent는 Model에 대한 입력인 상태.
- Model은 상태를 저장하고 요청된 상태를 View에 보낸다.
- View는 Model로부터 상태(State)를 로드.
- 사용자에게 표시합니다. 만약 관찰하고 있다면 데이터의 흐름은 항상 사용자로부터 오고 Intent를 통해 사용자와 함께 끝난다.
- 다른 방식으로는 불가능하기 때문에 단방향(Unidirectional) 아키텍처. 여기서 사용자가 한 번 더 작업을 수행하면 동일한 주기가 반복되므로 순환(Cyclic).
- 위의 예시를 보면 Intent(Increase) 이벤트를 발생시키면 기존 count 값에 + 1을 해 새로운 상태를 생성합니다.
- 그리고 새로운 상태를 화면에 render() 합니다.
- 사용자가 또 다른 이벤트를 발생시킨다면 같은 순환(Circular)을 거칩니다.
- MVI에서 상태는 불변성을 가지기 때문에 예상 가능한 값을 얻을 수 있습니다. 부수 효과가 발생하지 않는 함수형 프로그래밍의 장점이 반영됩니다.
- 하지만, 위의 순수 함수로 이루어진 cycle을 통해서는 API 콜, 데이터 베이스 작업 등에서 발생하는 부수 효과를 피하기는 힘듭니다.
- 그래서 기존 cycle에 Side Effect가 추가되었습니다. Intent()를 사용해 새로운 상태를 생성함과 동시에 부수 효과를 실행(Dispatch)합니다. 실행이 끝났다면 결과를 바로 render() 하는 것이 아니라, Event로써 결과를 전달합니다.
- 부수 효과의 결과를 Event로 전달합니다. 이는 부수 효과의 결과를 성공적으로 전달받았다면, 그 데이터를 특정한 이벤트를 통해 상태에 적용할 수 있습니다.
이런 흐름을 유지한다면?
- Single State - Immutable 한 데이터 구조는 한 곳에서만 데이터를 관리할 수 있다는 이점을 가져 관리하기 유용하고, 앱의 모든 레이어에서 단일 상태를 보장.
- Thread Safety - 비동기 처리를 위한 라이브러리(RxJava, Coroutines)를 사용할 때, 어떤 함수도 모델을 수정할 수 없기 때문에 모델은 항상 한 곳에서 다시 만들어지고 유지. 이런 점이 다른 스레드에서 모델을 수정하여 일어나는 충돌을 방지.
MVI의 장점
- 상태에 초점을 맞추기 때문에 상태를 유지하는 더 이상 문제 되지 않고 상태의 충돌이 없다.
- 단방향 데이터 흐름을 통해 앱의 로직을 명확하게 이해하고 쉽게 추적 및 예측할 수 있다.
- 상태 개체는 변경할 수 없으므로 스레드 안정성을 보장.
- 디버그 하기 쉽고 오류가 발생했을 때 개체의 상태를 알고 있다.
- 각 구성 요소가 자체 책임을 수행함에 따라 결합도가 낮아진다.
MVI의 단점
- 각 사용자 작업에 대한 상태를 유지하고, 모든 상태에 대한 많은 객체를 생성해야 하므로 많은 상용구 코드로 이어진다.
- 단일 이벤트 문제가 발생. 예를 들어 Toast, SnackBar 메시지 등. 이 또한 상태로 접근하게 되는데, 메시지를 출력하고 다시 상태를 변경하지 않는다면 다른 이벤트가 발생할 때마다 메시지가 출력되는 문제가 발생한다. 이를 위해 SingeLiveEvent 등의 방법이 필요.
- 러닝 커브가 높을 수 있다.
Preferecne
MVI Architecture - Android Tutorial for Beginners - Step By Step Guide
반응형'Android' 카테고리의 다른 글
Android) CodeLab - Jetpack Compose basics (0) 2021.11.02 Android) Jetpack Compose를 시작해보자 (0) 2021.10.02 Android) 새로워진 Mavericks 2.0을 알아보자 (0) 2021.09.22 Android) 테스트 코드 왜 작성 해야 할까? 예제로 알아보자 (1) 2021.08.29 Android) 안드로이드 네트워크 프로파일러 사용해보기 feat) 웹 파싱 방법 (0) 2021.08.11