ABOUT ME

-

  • 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가 업데이트되는 반응형 아키텍처 패턴입니다.
    • MVICycle.js 자바스크립트 프레임워크에서 영감을 받았습니다. 
    • 단방향(Unidirectional) 순환(Circular Data Flow)의 원칙을 기반으로 작동합니다.
    • MVI 아키텍처를 적용한 라이브러리는 대표적으로 AirbnbMavericks가 있습니다.
    • 사용자가 객체를 발행하는 행위가 기본적으로 앱의 상태를 변화시키고자 하는 의도를 나타낸다고 규정하고 이를 Intent라고 부릅니다.

     

    State

    • StateImmutable한 데이터 구조
    • 어느 시점이건 앱에는 현재의 시점을 표현하는 하나의 상태만 존재하고 그걸 바꿀 수 있는 유일한 트리거는 새로운 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를 사용하여 리팩터링 한다면

    1. 앱의 새로운 상태를 나태내는 PartialState라는 새로운 상태가 필요합니다.
    2. 이전 상태 정보가 필요한 새로운 Intent가 있을 때, 완료된 상태로부터 새로운 PartialState를 만듭니다.
    3. reduce() 메서드에서 이전 상태와 PartialState를 사용하여 화면에 표시할 새로운 상태로 merge 합니다.
    4. RxJavascan() 메서드를 사용하여 앱의 초기 상태에 reduce() 메서드를 적용하고, 새 상태를 반환합니다.

     

    State Reducer 코드 스니펫

     

    MVI의 흐름

     

    1. 사용자가 의도(Intent)가 될 행동(Action)을 수행.
    2. IntentModel에 대한 입력인 상태.
    3. Model은 상태를 저장하고 요청된 상태를 View에 보낸다.
    4. ViewModel로부터 상태(State)를 로드.
    5. 사용자에게 표시합니다. 만약 관찰하고 있다면 데이터의 흐름은 항상 사용자로부터 오고 Intent를 통해 사용자와 함께 끝난다.
    6. 다른 방식으로는 불가능하기 때문에 단방향(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 by Hannes Dorfmann

    Realm과 함께 하는 안드로이드의 단방향 아키텍처

    MVI 패턴에 대한 고찰

    MVI 아키텍처 튜토리얼

    MVI 소개

    MVI Architecture - Android Tutorial for Beginners - Step By Step Guide

     

    반응형

    댓글

Designed by Me.