[Android] MVVM 아키텍쳐와 단방향 데이터 플로우 패턴


개요

아키텍쳐에 아무런 이해없이 모바일 앱을 개발하던 시절이 있었습니다. ActivityFragment 같이 뷰를 담당하는 곳에 온갖 비즈니스 로직을 작성하고, 데이터와 통신하는 코드조차 뷰나 어댑터에서 처리했습니다. 그리고 어느새 프로젝트가 커지게 되면 한 번 작성이 끝난 뷰 코드는 볼 엄두조차 나지 않았습니다. 새로운 기능을 추가할 때마다 코드 전체를 고치는 일이 빈번했고, 테스트는 꿈도 못꿨습니다. 이게 맞나 싶은 생각이 들어 정보를 찾아본 결과 안드로이드 뿐만 아니라 모바일 개발에서 권장하는 아키텍쳐가 존재했습니다. 아키텍쳐를 적용한 이후부터는 전체 프로젝트 구조부터 한 눈에 파악이 가능했고, 코드의 유지보수와 테스트가 매우 간편해졌습니다. 이번 포스트에서 안드로이드 진영에서 적용했었던 여러 아키텍쳐들을 살펴보고, 그 중 MVVM 아키텍쳐의 등장배경과 필요성에 대해 알아보겠습니다. 또한 구글에서 제공하는 Android Architecture Components로 MVVM 패턴을 적용하는 법을 살펴봅시다.


MVC, MVP, 그리고 MVVM

안드로이드 진영에는 MVVM 아키텍쳐 등장 이전에 많이들 적용했었던 패턴들이 있습니다. 지금까지 사용됐던 아키텍쳐들은 크게 MVC, MVP, MVVM 3가지로 나뉩니다. 각각에 대해 알아보고, 어떤 문제점이 있어서 새로운 아키텍쳐로 넘어가게 되었는지 알아봅시다.


MVC (Model + View + Controller)

흔히 에서 많이 사용되었던 MVC 패턴입니다. 사용자의 Action이 Controller로 들어오게 되면 Model을 업데이트하고, 이를 보여줄 View를 선택하여 화면에 보여주게 됩니다. 이때 Controller는 여러개의 View를 선택할 수 있고, 선택만 할 뿐 직접 업데이트 하지 않습니다. View의 업데이트는 Model을 직접 참조하거나 Model에서 View에게 Notify 하는 방식으로 이루어집니다. 가장 단순한 패턴이기 때문에 구현이 쉽다는 장점이 있지만, 작동 방식에서 알 수 있듯이 ViewModel이 강하게 결합되어 있어 의존성이 높다는 것입니다. 각 요소가 강하게 결합되어 있으면 한 요소의 변화가 여러 요소에 영향을 미칠 수 있기 때문에 코드의 유지 보수와 단위 테스트가 불리합니다. 따라서 이를 해결하기 위해 MVP 패턴이 등장하게 됩니다.


MVP (Model+ View + Presenter)

MVP 패턴의 가장 큰 특징은 ViewModel 사이에 Presenter라는 다리를 두었다는 것입니다. 사용자의 Action이 View로 들어오게 되면 Presenter를 통해서 Model에 데이터를 요청하고, Model은 다시 Presenter를 통해 응답한 데이터를 전달합니다. 그런데 이때 ViewPresenter는 1:1 관계를 가집니다. 따라서 얼핏보면 ViewModel 간의 결합을 느슨하게 하여 의존성을 낮춘 것처럼 보이지만, 이번엔 PresenterModel에 높은 의존성을 갖게되고, 사실상 ViewPresenter는 한 몸이기 때문에 코드의 유지 보수 용이성은 여전히 낮습니다. 따라서 등장하게 된 것이 MVVM 패턴입니다.


MVVM (Model + View + ViewModel)

현재 안드로이드를 포함한 다양한 모바일 플랫폼 진영에서 권장하는 MVVM 패턴입니다. 사용자의 Action이 View를 통해 들어오면 Command Pattern으로 ViewModel에 이를 전달합니다. 이후 ViewModelModel에 데이터를 요청하고 받은 데이터를 가공하여 저장합니다. 이때 ViewViewModelData Binding하여 화면에 나타냅니다. 이때 커맨드 패턴(Command Pattern) 이란 요청을 객체의 형태로 캡슐화하여 사용자의 요청을 재사용 할 수 있도록 하는 패턴으로 Data Binding과 함께 MVVM 패턴의 핵심을 이룹니다. 이를 통해 ViewModelView에 대한 의존성이 없어지며, 하나의 ViewModel은 여러개의 View와 연결될 수 있습니다. 따라서 MVVM 패턴은 ViewViewModel의 결합성을 낮추고, 결과적으로 ViewModel에 높은 의존성을 갖던 문제를 해결합니다. 따라서 코드의 유지 보수성을 향상시키고 각각의 요소를 테스트하고 모듈화하여 개발할 수 있게 되었습니다.

그렇다면 MVVM 아키텍쳐에는 아무런 단점이 없을까요? MVVM 아키텍쳐는 위와 같은 장점을 제공하는 대신 설계와 구현이 어렵다는 단점이 있습니다. 따라서 무조건 MVVM 패턴을 적용하는 것이 아니라 프로젝트 규모에 따라 적절한 아키텍쳐를 선정해야한다는 의견이 많습니다. 그런데 이러한 MVVM 패턴 적용을 쉽게 도와주는 솔루션이 있습니다. 바로 구글에서 제공하는 Android Architecture Components입니다.


AAC (Android Architecture Components)

3

AACMVVM 아키텍쳐의 요소들을 매핑한 사진입니다. 이와 같이 AAC 자체가 MVVM이 아니라, AAC를 이용한 적절한 설계를 통해 MVVM 패턴을 구현할 수 있습니다.


View

말그대로 화면을 표현하는 UI를 나타내며, 안드로이드에서는 ActivityFragment를 의미합니다. ViewModel이 갖고있는 데이터를 관찰(observe)하고, Data Binding을 통해 이를 동기화하여 화면을 업데이트합니다.


ViewModel

MVVM에서와 마찬가지로 AAC에서도 핵심을 이루는 요소입니다. Model에 데이터를 요청하여 받아오고, 이를 View에서 관찰 가능한 데이터로 저장하고 있습니다. 이때 AAC는 관찰 가능한 데이터로써 LiveData를 사용하고 있습니다. LiveData는 관찰 가능하다는 것과 동시에 안드로이드 수명주기를 인식한다는 특징을 갖고 있습니다. 이외에도 코루틴의 Flow가 사용되기도 하는데, 이에 대한 내용은 다른 포스트에서 따로 다루도록 하겠습니다.

4

AAC에서 ViewModel 요소의 특징은 바로 위 사진과 같이 View를 담당하는 ActivityFragment보다 긴 Lifecycle을 갖고있다는 것입니다. 따라서 Activity가 회전하는 등 수명주기에 변화를 겪어도 ViewModel에 있는 데이터는 변경되지 않으므로 데이터를 보존할 수 있습니다. ViewModelView에 의존하지 않는다는 MVVM 패턴의 특징을 다시 한 번 확인할 수 있습니다.


Model

AAC에서 MVVMModel에 속하는 레이어에서 가장 주목할 만한 것은 바로 Repository 입니다. ViewModel에서 DataSource에 직접적인 접근을 하지 않고 Repository를 거침으로써 다음과 같은 이점을 기대할 수 있습니다.

Repository Pattern

  • 순수 DB 쿼리 및 API 통신과 데이터 로직을 따로 분리시킬 수 있다.
  • ViewModel 입장에서 Data Source 유형에 관계없이 일관된 인터페이스로 데이터를 요청할 수 있다.
  • 단위 테스트를 통한 검증이 가능해진다.

다만 추상레이어가 하나 추가되므로, 관리해야 할 코드와 파일이 늘어납니다. 하지만 AAC에서는 위에서 언급한 여러 이점들 때문에 Repository 패턴의 이용을 권장하고 있습니다.

그 외에 로컬 DB를 다룰 때 사용하는 SQLite ORM 라이브리리인 RoomAAC에 포함되어 있습니다. 이때 RoomViewModel에서 사용되는 LiveData와 같이 관찰 가능한 객체도 반환할 수 있습니다.


Unidirectional Data Flow

모든 Android 앱에는 다음과 같은 핵심 UI 업데이트 루프가 있습니다.

1

  • Event – 사용자 또는 프로그램의 다른 부분에 의해 이벤트가 생성됩니다.
  • Update State – 이벤트 핸들러는 UI에서 사용하는 상태를 변경합니다.
  • Display State – 새 상태를 표시하도록 UI가 업데이트됩니다.

이제 사용자의 입력에 따라 특정 Text를 출력하는 다음 예제를 보고, 어떤 문제가 있는지 살펴봅시다.

2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ExampleActivity : AppCompatActivity() {

   private lateinit var binding: ActivityExampleBinding
   var name = ""

   override fun onCreate(savedInstanceState: Bundle?) {
       /* ... */
       binding.textInput.doAfterTextChanged {text ->
           name = text.toString()
           updateHello()
       }
   }

   private fun updateHello() {
       binding.helloText.text = "Hello, $name"
   }
}

이러한 코드는 단방향 데이터 플로우 원칙을 지키지 않은 방식을 사용함으로써 구조화되지 않은 코드입니다. 따라서 당장은 문제가 없을 수 있지만, UI가 점점 방대해질수록 아래와 같은 문제들이 발생할 수 있습니다.

  • UI의 상태와 View가 얽혀있으므로 테스트가 어렵다.
  • 화면에 더 많은 event가 있을 경우, event 마다의 state 수정 코드를 작성해야하기 때문에 이를 하나라도 빼먹을 시 사용자가 잘못된 화면을 보게될 수 있습니다.
  • State에 따른 UI 업데이트 또한 일일이 작성하기 때문에 위와 동일하게 의도와는 다른 화면이 나타나게 될 가능성이 높아집니다.
  • UI 코드와 로직 코드가 같이 있기 때문에 코드 복잡성이 높아지고 가독성이 떨어집니다.

그렇다면 어떻게 단방향 데이터 플로우를 적용하여 해결할 수 있을까요? 바로 AAC의 ViewModelLiveData를 이용하는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ExampleViewModel: ViewModel() {

   // UI 상태를 ViewModel로부터 내려줌
   private val _name = MutableLiveData("")
   val name: LiveData<String> = _name

   // Event가 UI로부터 올라옴
   fun onNameChanged(newName: String) {
       _name.value = newName
   }
}

class ExampleActivity : AppCompatActivity() {
   private val helloViewModel by viewModels<ExampleViewModel>()

   override fun onCreate(savedInstanceState: Bundle?) {
       /* ... */

       binding.textInput.doAfterTextChanged {
           helloViewModel.onNameChanged(it.toString()) 
       }

       helloViewModel.name.observe(this) { name ->
           binding.helloText.text = "Hello, $name"
       }
   }
}

위 코드와 같이 UI 상태를 observable 객체인 LiveData를 사용하여 ViewModel로 옮겨 보았습니다. 앞서 소개한 UI 업데이트 루프와 같이 살펴보면 아래와 같습니다.

  • Event – 텍스트 입력에 따라 onNameChanged 호출
  • Update StateonNameChanged에서 _name 값 업데이트
  • Display Statename의 옵저버가 호출되어 UI 에게 상태 변화를 알림

이러한 구조는 마치, UI 에서 eventViewModel로 올려주고, 그에 따라 ViewModel에서 UIstate를 내려주는 것처럼 보입니다.

3

이렇게 이벤트와 상태를 각각 한 방향으로만 흐르게 하는 패턴을 바로 단방향 데이터 플로우(unidirectional data flow)라고 합니다. 이러한 패턴을 적용함으로써 다음과 같은 이점을 얻을 수 있습니다.

  • StateUI를 구분함으로써 ViewViewModel 각각을 테스트하기 더욱 쉬워집니다.
  • State를 ViewModel 한 곳에서만 업데이트하기 때문에, UI가 방대해짐으로써 state가 부분적으로만 업데이트되는 버그를 줄일 수 있습니다.
  • View에서는 관찰 가능한 객체인 LiveDataobserve하고 있기 때문에 상태 변화에 따라 UI가 즉시 업데이트 되어 UI 일관성이 보장됩니다.

따라서, 단방향 데이터 플로우 패턴을 사용할 시 코드양이 다소 늘어날 수는 있지만, 좀 더 다양하고 복잡한 stateevent를 다루기 훨씬 편리합니다. 이는 AAC 사용의 가장 큰 이점이자 의의라고 할 수 있겠습니다.


마치며

처음 MVVM 아키텍쳐로 앱을 개발할 때에는 AAC가 있음에도 불구하고 많은 어려움을 겪었습니다. 하지만 성공적으로 적용한 이후에는 MVVM 패턴의 이점을 몸소 깨달을 수 있었고, 시간이 지난 뒤 코드를 열어봐도 쉽게 새로운 기능을 추가할 수 있을만큼 가독성과 유지 보수성이 좋아졌습니다. 이와 더불어 AAC를 통해 훨씬 구조화된 코드 작성이 가능해졌습니다. 작은 프로젝트를 시작으로 꼭 MVVM 패턴과 단방향 데이터 플로우 패턴을 익혀보시기 바랍니다.

0%