[번역] 상태 지향 아키텍처 MVI를 소개합니다- MVI on Android

Jaeho Choe
15 min readApr 17, 2020

--

지난번 소개했던 MVI 튜토리얼에 이어 Android MVI에 대해 좀 더 알아봅시다! 다음 편에서는 MVI 시리즈의 마지막으로 마이리얼트립에서 사용하는 실전 MVI 이야기를 다뤄볼 계획입니다.

이 글에는 다수의 오역/의역이 포함되어 있습니다. 피드백은 언제나 환영합니다.

원문: https://proandroiddev.com/mvi-a-new-member-of-the-mv-band-6f7f0d23bc8a

대부분의 Android 개발자는 MVP 와 MVVM 패턴에 대해 알고 있습니다. 따라서 “V”나 “P” 또는 “VM”과 같은 줄임말이 의미하는것을 쉽게 알 수 있습니다. “V”는 View와 같이화면에 보이는 모든것을 의미하고 Presenter (“P”)와 ViewModel (“VM”)은 각각 프레젠테이션 로직이나 뷰를 모델링 하는 방법을 의미하죠. 하지만 “M” 이라고 부르는 모델의 목적에 관해서는 생각해 본적이 있나요?

구글링을 해본다면 Model에 대해 일반적으로 이렇게 설명합니다.

  • 비즈니스 로직의 응답을 처리하기 위한 것.
  • 비즈니스 로직과 앱 데이터를 저장하기 위한 공간.
  • 데이터 관리를 담당하는 인터페이스.
  • 앱 데이터와 비즈니스 로직을 표현하는 것.

일반적으로 이것은 다음 한 문장으로 설명 할 수 있습니다 — 모델은 도메인 계층 또는 비즈니스 로직에 대한 게이트웨이 역할을 해야한다

이것 만으로 Model의 정의가 ViewModel이나 Presenter와 어떻게 다른지 이해 할 수 있나요? Model이 비즈니스 로직을 나타낸다면 Model은 왜 “Model”로 불릴가요? 왜 “Business” 또는 “Logic” 또는 자신을 표현할 수 있는 다른 단어로 부르지 않을까요?

이런 질문들에 대한 대답을 찾기 위해 Model의 기원을 살펴보겠습니다.

Model의 컨셉은 새로운것이 아닙니다. 그것은 1979년 MVC 아키텍쳐의 일부로 트리베 린스카우그(Trygve Reenskaug)에 의해 처음 정의 되었습니다.

“Model은 상태, 구조, 그리고 유저의 행동을 나타내는 역할을 합니다"

“View 는 하나 또는 여러개의 Model로부터 받은 정보를 표현합니다.”

“Model에 View를 등록하고 Model이 변경 될때마다 View에게 적절한 메시지를 보내도록 합니다.”

대략적으로 말하면 본래 Model은 View가 화면에 그려야 할것들을 알려주는 Entity로 정의되었습니다. Model이 변경되면 View는 변화에 대한 알림을 받고 그 변화가 화면에 표시됩니다.

그리고 MVI가 등장합니다.

아무 방해도 받지 않는 두 사람간의 전통적인 대화방식을 상상해 봅시다. 각자 듣고 들은 것에 대해 반응 합니다.

두 사람 중 한사람을 컴퓨터로 바꾸고 싶다면 무엇을 해야할까요? 사람-컴퓨터 간의 상호작용을 위해서 우리는 마우스나 키보드 또는 모니터와 스피커와 같은 인터페이스를 추가해야합니다.

말하는것 대신 사용자는 마우스나 키보드로 컴퓨터에게는 입력이 되는 출력을 만들어 낼것입니다. 그럼 컴퓨터는 정보를 분석하고 사용자에게는 입력이 되는 정보를 모니터에 출력으로 표시하게 되겠죠. 그럼 그걸 본 사용자는 다시 다른 순환을 시작할지 여부를 판단할 것입니다.

일반적으로 수학적인 관점에서 입력과 출력을 연결하기 위해는 함수가 필요합니다. 그 결과 우리는 각각의 입력과 출력의 쌍을 함수로 바꿔볼 수 있습니다.

MVI 아키텍쳐 그래프

위 원의 모든 컴포넌트들은 각각의 출력이 다음 함수의 입력이 되는 형태입니다. 모든 데이터는 한쪽 방향으로만 갈 수 있습니다. user() 의 결과는 intent() 의 입력으로 전달됩니다. 다른 방향으로는 갈 수 없습니다. 원안의 다른 단계도 동일한 규칙이 적용됩니다. intent() 의 결과는 model() 의 입력이 됩니다. model()의 유일한 결과인 새로운 state (나중에 언급됩니다)는 view() 의 입력으로 전달됩니다. 그리고 view() 의 출력은 다시 user() 를 호출하는데 사용되며 이 순환은 아래와 같이 계속 됩니다.

intent(user(view(model(intent(user())))))

user는 코딩의 대상이 아니기 때문에 최종 함수는 이런 모습을 하게 됩니다.

view(model(intent()))

위의 함수가 함수형 프로그래밍에서 말하는 순수 함수라고 가정해봅시다. 주요 특성은 사이드 이펙트가 없다는 점입니다. 함수의 결과는 다른 사이드 이펙트 없이 유일하게 입력값에 의해서만 영향받습니다. 같은 입력에는 항상 같은 결과를 얻습니다.

결과적으로 다음 세 가지 함수가 MVI 아키텍처의 주요 구성 요소를 만들어 낸다고 말할 수 있습니다.

  • Intent
  • Model
  • View

Intent

인텐트는 우리가 알고있는 Android의 intent가 아닙니다. 여기서 intent는 앱의 상태를 바꾸려는 의도를 의미합니다. intent() 는 유저의 버튼 클릭이나 화면에 출력하고 싶은 API 호출의 결과로 발생 할 수 있습니다. 모든 UI의 변화는 일반적으로 하나의 파일에 정의된 intent() 함수의 결과로 동작합니다. 이것을 통해 우리는 앱에서 진행 중인 작업에 대해 명확하게 이해 할 수 있습니다. 이 파일을 열면 기본적으로 모든 유저 케이스에 대해 알 수 있습니다.

Side effects

순수한 함수로 이뤄진 기본 MVI 그래프를 아직 기억하시나요? API 호출이나 DB 작업, 또는 원격 로깅 등 Android 앱이 사이드 이펙트로 가득하다는 점을 생각하면 글쎄요.. 순수한 함수라는 것은 사실이 아닙니다. 이러한 모든 행동들은 순수 함수에서는 기술적으로 금지된 사이드 이펙트들입니다. 어떻게 해야할까요?

intent()model() 사이에는 사이드 이펙트를 처리하는 숨겨진 컴포넌트가 있습니다. 사용자가 intent() 를 발송하면 보통 intent() 의 결과는 model() 의 입력값으로 전달됩니다. 동시에 intent() 는 다른 구성요소에 사이드 이펙트를 실행하도록 요청합니다. 이 사이드 이펙트의 결과는 아무것도 아니거나 새로운 intent() 가 될 수 있으며, 이는 다른 사이드 이펙트를 일으키거나 model() 의 입력값이 될 수 있습니다.

MVI에서 사이드 이펙트 다루기

Model

model() 는 state에 대해 뷰가 어떤걸 화면에 랜더링 해야 하는지를 말해주는 응답입니다. 여기에는 프로그래스의 진행률 표시부터 서버로 부터 받은 데이터 목록, 또는 오류 상태에 이르기까지 모든 것이 포함될 수 있습니다. View는 model과 state에 포함된 모든 것을 랜더링 합니다.

State

State는 immutable한 데이터 구조입니다. 어느 시점이건 앱에는 현재의 시점을 표현하는 하나의 상태만 존재하고 그걸 바꿀 수 있는 유일한 트리거는 새로운 state를 만드는 intent() 입니다. 그게 바로 각각의 UI의 변화가 intent() 실행의 결과인 이유 입니다. 근데 언제 어떻게 새로운 state가 생성될까요? 그걸 이해하기 위해서는 Redux라는 새로운 컨셉에 대해서 알아야 합니다.

Redux

Redux는 자바스크립트 앱의 상태 관리를 위한 오픈소스 라이브러리입니다. Redux는 다음 세가지 컴포넌트로 구성됩니다.

  • State — 상태 홀더
  • Action — State를 바꾸기 위한 명령
  • Reducer —이전의 state와 action을 받아 새로운 state를 만드는 순수 함수

MVI의 model()에서는 적절한 intent와 최근 state로 reducer() 를 호출하여 그 결과를 응답 값(*새로운 state)으로 전달합니다. 최근 state값과 함께 작업하기 위해 reducer()함수에서는 RxJava의 scan 과 같은 연산자를 활용 할 수 있습니다. reducer() 는 순수한 함수라는 점이 중요합니다. 여기엔 서프라이즈도, 사이드 이펙트도 없고 그저 새로운 state를 계산해 냅니다.

공식문서인 Redux for JavaScript 를 읽어보길 진심으로 추천합니다. 정말 잘 쓰여졌고 이 주제를 잘 이해하는 데 도움이 될 겁니다.

상태의 충돌

왜 state 가 필요한지 물어본다면.. 답은 간단합니다. 상태의 충돌을 피하기 위해서 입니다.

전통적으로 앱은 각각의 레이어 마다 상태를 유지하는 것이 일반적입니다. 비즈니스 로직, ViewModel, 그리고 이론적으로는 데이터 바인딩을 사용할 경우 XML 레이아웃 내부에도 상태 가 존재 할 수 있습니다. 너무 많은 상태를 관리할 경우 모든 상태에 대해 제어할 수 없게 되고 더 이상 앱에서 벌어지고 있는 일을 이해 할 수 없는 상태에 도달하게 됩니다.

또한 적절한 상태 관리가 없다면 ViewModel 이나 Activity가 가진 상태가 충돌하는 경우가 발생 할 수 있습니다. 운이 좋다면 화면에 진행 상황과 성공 결과가 함께 표시되는 귀여운 버그만 발생하지만 운이 좋지 않다면 수정 불가능한 버그 리포트를 받게 될 수도 있습니다.

결과가 이미 표시 되어있는데 프로그래스는 계속 도는 경우 (운이 좋은..)

View

우린 state가 model이 되고 그것으로 부터 정보를 표시하는 view가 되기를 원했습니다. view() 는 새로운 state를 받아 화면에 표시하는 로직이 정의된 함수 입니다.

Configuration change(화면 전환이나 언어 변경과 같은)는 어떻게 처리하나요?

가장 최근의 state를 표시하면 됩니다.

Navigation

Navigation은 MVI아키텍쳐에서 여전히 열려있는 질문입니다.

먼저 네비게이션을 state의 일부분이라고 보는 접근법이 있습니다. 만약 화면을 전환하고 싶다면 관련 intent() 를 트리거 하고 전체 사이클을 돌고 난 다음 만들어진 state에 따라서 화면을 변경 할 수 있습니다.

다른 방법으로 네비게이션을 사이드 이펙트의 결과로 호출하는 방법이 있지만 사이드 이펙트는 API호출, DB작업 또는 원격 로깅과 관련이 있으므로 이런 개념을 함께 섞지 않는것이 좋습니다.

내 추천은 네비게이션을 직접 호출하는 것입니다. state에는 표시하려는 화면이 아니라 화면에 표시하려는 데이터가 있어야 한다고 생각합니다. 화면 자체는 state에서 어떤 데이터를 가져야 하는지를 알고 있어야 합니다. 특히 Android Jetpack의 Navigation 구성 요소를 활용할 수 있게 되면 훨씬더 의미가 있습니다. 적어도 필자에겐 그랬습니다.

끝으로..

지금까지 상태지향 MVI 아키텍쳐에 대한 간략한 소개였습니다. 이 아키택쳐에는 다음과 같은 장점이 있습니다.

  • 상태의 충돌이 없습니다 앱의 상태는 하나 뿐입니다.
  • 단방향 데이터 흐름을 갖습니다 앱의 로직을 보다 예측가능하고 이해하기 쉽게 만듭니다.
  • 불변성 — 각각의 출력값이 불변값이기 때문에 불변성이 갖는 쓰레드 안전성, 공유 가능성 같은 이점을 활용 할 수 있습니다.
  • 디버그의 용이함 — 단방향 데이터 흐름은 앱을 쉽게 디버깅 할 수 있습니다. 컴포넌트에서 다른 컴포넌트로 데이터를 전달 할 때마다 현재 값을 기록할 수 있기때문에 버그 리포트를 받았을때 오류가 발생한 시점의 앱 상태를 확인 할 수 있고 사용자의 의도도 옅볼 수 있습니다.
  • 분리된 로직 — 각각의 컴포넌트는 자체적으로 책임을 가집니다.
  • Testability — 앱의 유닛 테스트를 위해서 우리는 적절한 비즈니스 메소드를 호출하고 그 상태가 예상되는 값인지를 확인하기만 하면 됩니다.

하지만 완벽한것은 없으며 MVI를 사용하기 전에 알아야할 몇 가지 단점이 있습니다.

  • 너무 많은 상용구 — 작은 UI 변경도 intent 로 시작하여 한 사이클을 통과해야 합니다. 아주 간단한 구현도 앱에서 수행하는 모든 작업들은 어김없이 최소한 intent와 state가 필요합니다.
  • 복잡성 — 내부엔 많은 규칙이 있어야하며 함께 작업하는 모든 사람이 그것을 업격하게 따라야합니다. 하지만 새로운 사람은 그런걸 알기 쉽지 않기때문에 팀이 확장 되면서 문제가 발생 할 수 있습니다. 특히 신입사원이 익숙해지기 위해 더 많은 시간이 필요합니다.
  • 객체 생성 —객체 생성의 비용이 많이 듭니다. 너무 많은 객체가 생성되면 힙 메모리가 쉽게 한계에 도달할 수 있으며 gc가 빈번하게 수행 될 수 있습니다. 앱의 구조와 사이즈 사이에서 균형을 유지해야 합니다.
  • SingleLiveEvents — MVI 아키텍처를 만들 기 위해서 (예를 들어 스낵바 메시지를 표시할때) showMessage = true 와 같은 속성이 필요할 것이며 이 플래그를 확인하여 스낵바는 랜더링 되어야 합니다. 하지만 이때 config가 바뀌게 된다면 어떨까요? 그럼 스낵바를 다시 한번 표시해야 합니다. 이건 의도된 올바른 동작이지만 사용자가 볼땐 그렇지 않을 수 있습니다. 메시지를 다시 표시 하지 않도록 다른 상태를 만들어야 합니다. 그러려면 showMessage = true 를 방출한 후 몇 초 후에 showMessage = false 라는 새로운 상태를 만들어야 합니다(예: RxJava의 타이머 연산자 사용). 이상적인 솔루션은 아니지만 일반적으로 수행되는 방식입니다.

MVI 아키텍쳐는 내가 만든것이 아닙니다. 잘 알려진 기업이 개발하고 확인 할 수 있는 많은 라이브러리들이 있습니다.

어떤 현자가 말했습니다.

“1년전 코드가 아직 마음에 든다면, 당신은 올해 충분히 배우지 못 한겁니다.”

이 아키텍처를 사용하기를 강요하는건 아닙니다. 아키텍쳐는 진화하고 있습니다. 새로운 아키텍쳐에 대한 글이나 비디오는 항상 튀어 나옵니다. 전 그저 사용할 수 있는 또다른 아키텍처에 대해서 소개하고 싶었습니다. 다음 프로젝트에서 이걸 사용할지를 결정하기전에 장점과 단점을 잘 고려하세요.

여기 필자가 만든 MVI 앱이 있습니다. (Benoit Quenaudon’s example 에게서 많이 영감 받음)

그리고 여기 관련 세션 영상이 있습니다. (바르셀로나 ADG 제공)

이 글을 리뷰하고 더 나아질 수 Alexander Kovalenko, Lubos MudrakJirka Helmich에게 감사하고 싶습니다.

--

--

Jaeho Choe
Jaeho Choe

No responses yet