[Android] AAC를 활용한 MVVM 패턴

Leopold Baik (백중원)
Jan 5 · 13 min read

잘 설계된 아키텍처가 우리 개발자들에게 주는 이점은 말로 표현할 수 없을 만큼 다양하다. 코드를 일관되게 작성하게 도와주고 변화에 유연하게 대처할 수 있게 해주며 테스트가 용이하게 해주는 등 좋은 점이 많기 때문에 많은 개발자들이 좋은 아키텍처를 구성하기 위해 노력해왔다. 오늘 소개할 내용은 그런 노력들의 일환으로 Microsoft에서 제안한 MVVM 패턴에 대해서 이야기해보려고 한다.


MVVM은 Model-View-ViewModel의 약자이다. Model은 UI에 표시될 데이터를 의미하고 View는 UI를 의미하며 ViewModel은 이벤트 처리나, 비즈니스 로직 등을 담당한다. MVVM의 각 컴포넌트 간에 어떤 식으로 인터랙션이 이루어지는지 간단하게 도식화된 그림으로 살펴보자.

각각의 역할을 살펴보면 우선 ViewModel과 Model은 그림에서 보다시피 능동적인 역할을 수행한다. 그에 반해 View는 수동적인 역할을 수행하는데 사실 꼭 그런 것만은 아닌 것이 요구 사항에 따라 Two-way binding처럼 View가 능동적인 역할을 하는 경우도 있다. 하지만 기본적으로 View는 수동적인 포지션을 취한다. 기존 아키텍처 패턴들의 네이밍을 살펴보면 MVC, MVP, MVVM으로 변화되면서 MV는 고정이고 나머지 글자들이 바뀐 것으로 유추하여 MVVM에서 ViewModel이 가장 중요한 것이라고 오해할 수 있다. 하지만 단언컨대 MVVM에서 가장 중요한 핵심은 바로 Data Binding 기술이다. Data Binding 기술을 이용하여 View가 오로지 수동적인 포지션을 취할 수 있고 ViewModel이 View의 존재를 알지 못하게 하여 플랫폼 의존성에서 벗어날 수 있게 해준다.

필자는 아직 MVVM으로 실제 서비스 개발에 적용해보진 못하였다. 현재 회사에서 서비스 중인 앱은 MVP로 되어 있으며 사실 요구 사항에 맞는 구조기도 했다. 그래서 개인 프로젝트로 진행 예정인 앱 개발에 MVVM을 도입하고 다양한 요구 사항에 부딪히면서 어떤 구조가 가장 MVVM스럽고 좋은 구조일지 지속적으로 고민할 생각이다. 그에 앞서 간단하게 안드로이드에서 제공하는 AAC(Android Architecture Component)를 이용하여 MVVM 구조의 간단한 예제를 만들어봤다. MVVM을 구축하는데 AAC가 필요한 것은 아니다. AAC가 없어도 충분히 MVVM 구조를 만들 수 있다. 하지만 우린 Android 개발자이기 때문에 귀찮은 Lifecycle 관리를 해줘야 한다. 물론 RxJava를 이용해서 처리를 해줘도 되지만 AAC에서 제공하는 것들이 귀찮은 것들을 대신해주기 때문에 최대한 활용해서 예제를 만들어봤다. 코드만 먼저 대충 보고 싶으신 분들은 아래 Repository를 참조하면 된다.

원래는 Github Search API를 호출하는 것만 하려고 했으나 Room도 예제에 포함시키고 싶어서 Github의 검색 결과를 북마크에 추가하는 기능까지 억지로 끼워 넣었다. AAC에서 제공하는 LiveData, ViewModel, Room, Paging 등을 적용하였고 DI 프레임워크로 기존 Dagger2를 사용하던 방식에서 Koin으로 바꿔보았다. Koin은 Kotlin으로 작성된 DI 프레임워크로 Dagger2가 사용하기 까다롭지만 컴파일 타임에 DI 그래프상의 오류를 확인할 수 있었다면 Koin은 사용하기는 보다 쉽고 Android의 ViewModel을 지원하는 등 편리한 점이 있지만 컴파일 타임에 DI 그래프상의 오류를 확인할 수 없다는 단점이 있다. 잘못된 DI 그래프는 테스트 코드로 사전에 방지할 수 있으니 프로젝트 성격 및 환경에 맞게 사용하는 것이 좋을 듯싶다.

간단한 예제를 통해 어떤 식으로 구성하였는지 살펴볼 것이기 때문에 특정 기술 또는 특정 라이브러리에 대한 구체적인 설명은 덧붙이지 않으려고 한다.


의존성 설정

필자의 경우 의존성 관리를 편하게 하려고 versions.gradle이라는 별도의 파일을 구성하였다. 자세한 내용을 적기엔 공간이 부족하니 여기선 간단히 app.gradle의 라이브러리 의존성 설정만 표시하였다.


DI 설정

Dagger2는 DI 설정하는 게 좀 까다로웠다. 그에 반해 Koin은 정말 사용하기 쉽다. 코드를 통해 살펴보자. 아래는 Network 관련 Module을 설정하는 코드다.

single로 선언된 블록 안에 초기화 코드들이 있는 것을 볼 수 있다. single은 이름에서 유추할 수 있듯이 Single instance만 제공하고 만약 매번 새로운 객체를 생성하고 싶다면 factory로 선언하면 된다. <Gson>, <Retrofit> 이런 식으로 타입을 선언하기도 하였는데 타입 추론을 통해 필요한 타입을 찾아 주입시켜주기 때문에 생략해도 무방하다. 하지만 만약 동일한 타입을 리턴하는 게 2개 이상이라면 파라미터로 name을 설정하여 구분해서 주입하도록 해야 한다. 이렇게 작성된 Module은 아래와 같이 Application 시작과 함께 startKoin을 통해 인자로 넘겨주기만 하면 된다.

이것으로 객체를 주입받기 위한 설정이 끝이다. 번거로웠던 Dagger2와 비교하면 상당히 간편하다는 것을 느낄 수 있을 것이다.


Data Binding 설정

Android의 Activity에서 Data Binding을 사용하기 위한 사전 준비 단계는 다음과 같다.

  1. Activity 클래스 및 해당 Activity의 Layout File 생성
  2. Layout File의 root를 <layout> 태그로 변경
  3. Activity의 onCreate 함수 내에서 DataBindingUtil.setContentView 함수 호출
  4. 자동 생성된 Binding 클래스에 데이터 바인딩
  5. LifecycleOwner 등록을 통한 데이터 옵저빙 및 라이프 사이클 관리 위임

위의 과정을 간단하게 코드로 보는 것이 이해가 빠를 것이다.

위 코드에서 ActivitySearchBinding은 자동 생성된 클래스로 xml 파일의 이름을 기반으로 파스칼 표기법 형태로 생성된다. xml의 root를 <layout>으로 설정해주는 것만으로 ActivitySearchBinding이라는 클래스가 자동 생성된다.

자 이제 xml에서 참조할 데이터를 선언하고 위에서 생성한 binding 클래스에 해당 데이터를 바인딩 시켜주는 코드를 작성해보자.

위 코드에서 xml에 <data>태그 안에 vm이라는 이름의 SearchViewModel 변수를 선언한 것을 볼 수 있다. 다시 Activity 코드로 돌아와서 확인해보면 binding 클래스에 vm 변수가 생성된 것을 확인할 수 있다. vm 변수에 getViewModel()을 통해서 데이터를 바인딩 시켰다. 일단 여기선 getViewModel()에 대해선 신경 쓰지 않도록 한다.


ViewModel 만들기

그냥 순수 클래스 형태로 ViewModel을 만들 수도 있지만 귀찮은 작업은 최대한 AAC에게 위임할 것이기 때문에 ViewModel을 상속받아서 SearchViewModel이라는 클래스를 하나 만들었다.

DisposableViewModel은 Android의 ViewModel을 상속받은 클래스다. 다른 부분은 신경 쓰지 말고 _refreshing_items를 살펴보자. _refreshing은 로딩 UI를 표시할지 여부를 나타내는 Boolean 값을 갖는 LiveData이고 _items는 Github의 Repository 검색 결과 목록을 RecyclerView에 바인딩 시킬 List<Repository>값을 갖는 LiveData이다. LiveData의 내부 접근을 막기 위해 private으로 선언 후 별도의 변수에 getter로 할당하였다.

간단하게 ViewModel을 만들었으니 어떻게 아까 생성한 ActivitySearchBinding클래스에 연결해주는지 확인해보자. 우선 ViewModel을 주입받기 위한 Module을 작성하자.

Koin의 장점 중에 하나가 저렇게 viewModel 함수를 통해 Android ViewModel을 상속받는 클래스를 DI로 주입할 수 있게 지원한다는 점이다. 참고로 viewModel 함수는 내부적으로 factory 함수 호출로 생성되기 때문에 요청 시마다 새로운 객체를 생성한다.

위 코드는 ViewModel을 주입받는 두 가지 방식을 나타낸 것이다. by viewMode()을 통해서 지연 초기화를 할 수도 있고 getViewModel() 함수를 통해 즉시 객체를 요청할 수도 있다. 이제 xml에 ViewModel을 바인딩 시켰지만 LiveData의 상태 변화를 xml에서 감지하려면 LifecycleOwner를 등록해줘야 한다. LifecycleOwner를 등록하지 않으면 Android Lifecycle에 맞게 관리되지도 않고 LiveData의 상태 변화를 xml에서 감지하지 못한다. 물론 LiveData 대신에 Observable…로 시작하는 타입으로 선언하거나 BaseObservable를 상속받아 Bindable 어노테이션을 등록하는 방법도 있을 것이다. 하지만 그것들은 추가적인 코드를 요구한다. 무조건 LiveData를 사용하라는 것은 아니고 각각의 방식이 주는 이점 및 특징이 있으니 요구 사항에 맞게 적절히 구현해서 사용하면 될 것이다. 이 포스팅의 논지는 최대한 AAC 컴포넌트를 활용하는 것에 초점을 맞추고 있기 때문에 AAC가 무조건 좋다는 함정에 빠지지 않는 것이 좋다.


BindingAdapter 만들기

Github Search API를 통해 Repository 검색 결과 목록을 RecyclerView를 통해 리스트 형태로 표시하고 싶다. RecyclerView에 데이터를 바인딩 시키고 데이터의 상태 변화를 RecyclerView가 스스로 갱신하게 하려면 어떻게 해야 할까? 그 해답은 BindingAdapter에 있다.

위에서 우린 <data>태그 안에 vm이라는 SearchViewModel 변수를 선언하고 Activity에서 바인딩 시켜주었다. 이제 xml에서는 SearchViewModel의 변수를 참조할 수 있고 함수를 호출할 수도 있다. 그런데 못 보던 속성이 보인다. 바로 refreshing, viewModel, repositories란 속성인데 @{}안에 vm의 변수를 할당한 것을 볼 수 있다. 대충 유추해보면 vm 변수의 값을 각각의 속성에 대입하는 것으로 생각해볼 수 있다. 이제 refreshing, viewModel, repositories 각각이 어디에 정의되어 있는지 살펴볼 차례다.

@BindingAdapter라는 어노테이션을 이용하여 함수를 정의한 것을 볼 수 있다. refreshing 속성은 범용적으로 쓰일 수 있기 때문에 SwipeRefrshLayout에 extension을 이용하여 정의하였다. 위의 코드들로 우리가 유추해볼 수 있는 내용은 SearchViewModel에 정의된 변수의 값에 변화가 발생했을 때 @BindingAdapter어노테이션으로 매핑된 함수가 호출되고 그 값이 전달된다는 것이다. Activity나 어떤 View 요소에서 별도의 요청이 없어도 데이터가 전달되고 상태 변화를 전달받는다.


동적 데이터 바인딩

RecyclerView는 여러 가지 layout 요소를 가질 수 있다. 특히나 요즘처럼 복잡한 UI를 구현함에 있어서 하나의 layout 요소만을 가지는 RecyclerView는 없을 것이다. 각각의 뷰 타입에 맞게 ViewHolder를 정의하게 되는데 각각의 layout에도 데이터 바인딩을 적용할 수 있다.

RecyclerView의 아이템 layout 파일에 Repository 변수를 선언한다. 그런 다음 자동 생성된 LayoutRepositoryItemBinding클래스의 bind함수를 ViewHolder의 초기화 코드에서 호출해준다. 그리고 onBindViewHolder 함수 안에서 LayoutRepositoryItemBinding 클래스의 item 변수에 데이터를 바인딩 시켜주면 끝이다. 정말 간단하지 않은가? 물론 극단적으로 간단한 예제라 더 그렇게 보일 수도 있지만 그래도 예전에 비해 확실히 간결하고 코드의 양이 극적으로 줄어든 것을 느낄 수 있을 것이다.


Appendix

원래 Room 및 Paging에 대한 내용도 본문에 담으려고 했으나 쓰다 보니 너무 길어져서 해당 내용은 그냥 Github 예제 소스로 대체하려고 한다. 그래도 간단히 맛을 보자면 Room database와 Paging을 이용한 한 줄짜리 코드를 보자.

위와 같이 선언해주고 데이터 바인딩을 적용하면 DB의 데이터를 불러오고 변경이 발생했을 때 자동 업데이트와 별도의 페이징 관련 코드 작성이 필요 없다. 물론 Adapter를 PagedListAdapter를 상속받아 구현해야 하긴 하지만 앱 개발자가 페이징 관련하여 신경 써야 할 부분을 극적으로 줄여준다. DB뿐만 아니라 Network도 동일하게 적용할 수 있다.

Android에서는 위와 같은 구조의 다이어그램으로 MVVM을 구현할 것을 추천하고 있다. 하나의 방법이 모든 시나리오를 충족시켜줄 순 없지만 참고 용으로 보면 좋을 것 같고 ViewModel과 Model 간에 레이어를 어떻게 구성할지가 중요한 것 같다.


마치며

예제도 그렇고 포스팅 내용도 그렇고 Android에서 제공하는 AAC 컴포넌트를 최대한 활용하는 방향으로 작성되었다. 그래서 미처 다루지 못한 내용들도 있지만 MVVM 관련하여 많은 분들이 좋은 포스팅을 작성하였기 때문에 생략해도 될 것 같다. 기존 MVP에 비해 MVVM은 좋은 점이 꽤 있는 것 같다. MVP에서 View-Presenter가 일반적으로 1:1관계였다면 MVVM에서 View-ViewModel1:n의 관계로 구성할 수 있다. 그래서 ViewModel을 여러 화면에서 재활용할 수 있는 구조를 만들 수 있다. 필자가 생각하는 MVVM을 잘 설계하기 위한 핵심은 Data Binding을 얼마나 잘 활용하는지와 View-ViewModel의 인터랙션 구조, ViewModel-Model 간에 추상화된 레이어 계층을 잘 설계하는 것인 것 같다.

Leopold Baik (백중원)

Written by

사람들에게 사랑받는 서비스를 만드는 게 목표이자 독서와 개발이 취미인 평범한 개발자. 소소한 내용들을 가끔씩 공유하고 있습니다.