안녕하세요. 강남언니에서 Android 개발을 맡고 있는 David 입니다.
이번 글에서는 Android Architecture Components를 활용하여 MVVM을 적용하는 방법에 대하여 다뤄보도록 하겠습니다.

먼저 AAC(Android Architecture Components)에 대하여 간략하게 설명하도록 하겠습니다.

  • 테스트와 유지관리가 쉬운 앱을 디자인하도록 돕는 라이브러리 모음
  • ViewModel은 앱 회전시 제거되지 않는 UI 관련 데이터를 저장
  • LiveData를 사용하여 기본 데이터베이스가 변경되면 뷰에 알리는 데이터 개체를 빌드
  • Room은 SQLite 개체 매핑 라이브러리 RxJava, Flowable, LiveData Observable을 반환

Why use Android Architecture Components

개발자들은 AAC를 왜 사용해야할까요?

AAC를 사용하는 이유는 관심사 분리 하기 위해서 입니다.

Activity/Fragment에 모든 코드를 작성하는 경우가 많았고 이러한 문제를 해결하기 위하여 다양한 Architecture을 사용하여 해결해왔습니다.

AAC를 활용하면 View 객체 및 앱 구성요소와 독립되어 있어 LifeCycle의 영향을 받지 않습니다. 또 네트워크 연결이 끊어졌을 경우 앱을 지속적으로 작동시킬 수 있어 일관성을 유지할 수 있습니다.


Recommended App Architecture

AAC 다이어그램을 살펴보도록 하겠습니다.

Activity/Fragment는 ViewModel을 알고있고 ViewModel에는 LiveData를 가지고 있으며 Repository를 알고 있습니다. 각 구성요소는 하나의 구성요소에만 종속됨을 볼 수 있습니다.

AAC 다이어그램을 참고하여 MVVM을 적용하면 다음과 같이 표현할 수 있습니다.


Example

보다 더 쉽게 이해하기 위하여 간단한 예제를 보면서 설명하도록 하겠습니다.

  • View : activity_search.xml
  • ViewModel : SearchViewModel
  • Model : SearchRepository

input에는 GitHub 사용자 이름을 입력할 수 있고 searchBtn을 누르면 해당 이름을 가진 id가 recyclerView에 출력됩니다.


ViewModel

먼저 ViewModel 부터 살펴보도록 하겠습니다.

ViewModel은 Activity보다 긴 LifeCycle을 가집니다.

ViewModel은 UI에 관련된 데이터를 유지, 관리하기 위한 클래스입니다.

이때 주의해야할 점이 있는데 절대로 context의 레퍼런스를 가지는 객체 (ex : Activity, Fragment, View) 를 ViewModel에 집어넣으면 안됩니다. 집어넣었을 경우 memory leak 현상이 나타납니다.

ViewModel은 Activity/Fragment 보다 더 긴 LifeCycle을 가지고 있기 때문에

configuration이 바뀌었을 경우 Activity/Fragment는 destroy 되고 recreate 됩니다.

이때 생성된 새로운 instance는 ViewModel의 동일한 instance 가지게 됩니다.

따라서 ViewModel에 Activity/Fragment에 대해 참조를 유지하는것은 memory leak 현상이 일어나고 최악의 경우에는 crash가 날 수 있습니다.

SearchViewModel

class SearchViewModel(private val repository: SearchRepository) : BaseViewModel() {
    private var name: String = ""
    val users: MutableLiveData<ArrayList<User>> = MutableLiveData(arrayListOf())
    
    fun search() {
        repository.fetchUsers(name)
            .subscribe({ users.value = it.items },
                { Log.e("search api error", it.toString()) })
            .addTo(compositeDisposable)
    }
    
    fun onTextChanged(text: CharSequence) {
        this.name = text.toString()
    }
}

search 메서드를 보면 Repository를 통하여 사용자 이름을 가져와서 LiveData 타입인 users에 저장하는것을 볼 수 있습니다.

LiveData

RxJava 라이브러리를 이미 사용중이라면 LiveData 대신에 RxJava를 사용하셔도 됩니다.

LiveData도 RxJava와 유사하게 데이터가 변경 됬을 경우 observer에게 데이터 업데이트를 알려줍니다.

하지만 RxJava를 이용했을 경우 앱 수명주기에 맞춰 dispose를 해야하는 불편함이 있습니다.

그와 반대로 LiveData를 사용할 경우 다양한 이점이 있습니다.

  • UI와 데이터 상태의 일치 보장
  • memory leak 현상 없음
  • 항상 최신의 데이터를 받을 수 있음
  • LifeCycle를 수동으로 처리할 필요 없음
  • 리소스 공유

주로 LiveData는 ViewModel 클래스 내에서 사용됩니다.

LiveData에 저장된 데이터를 업데이트 하고 싶을 경우 setValue(T)postValue(T) 메서드를 이용하면 LiveData를 관찰하는 observer에게 업데이트된 데이터를 넘겨줍니다.

 val users: MutableLiveData<ArrayList<User>> = MutableLiveData(arrayListOf())

 fun search() {
        repository.fetchUsers(name)
            .subscribe({ users.value = it.items },
                { Log.e("search api error", it.toString()) })
            .addTo(compositeDisposable)
    }

결국 search 메서드가 호출되면 users의 값이 업데이트되고

users를 관찰하는 observer는 업데이트된 users를 받을 수 있습니다.


View

예전에는 Activity/Fragment에 UI 관련된 코드가 있었지만

DataBinding을 사용하면 UI에 관련된 코드를 xml에 작성할 수 있습니다.

activity_search.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <!-- ViewModel 정의 -->
    <data>
        <variable
            name="searchVm"
            type="com.dhha22.architecture.search.SearchViewModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
				...
        >
        ...
        <!-- search 메서드 호출 -->
        <ImageButton
            android:id="@+id/searchBtn"
            ...
            android:onClick="@{() ->searchVm.search()}"/>
      
        <!-- LiveData users -->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            ...
            app:itemList="@{searchVm.users}"/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

layout 루트 태그로 시작하고 data요소 안에는 아까 선언했던 SearchViewModel이 있습니다.

searchBtn을 눌렀을 경우 SearchViewModel에 선언된 search 메서드가 호출되고

recyclerView에서는 app:itemList를 통해 LiveData 타입인 users를 받고 있습니다.

SearchBindingAdapter

@BindingAdapter("app:itemList")
fun RecyclerView.setListItem(users: ArrayList<User>) {
    (adapter as? SearchAdapter)?.apply {
        items = users
        notifyDataSetChanged()
    } ?: run { adapter = SearchAdapter(users) }
}

BindingAdapter를 보면 Adapter가 없을 경우에는 Adapter를 생성하고

Adapter가 있을 경우 SearchViewModel의 users를 받아서 데이터를 업데이트 하는 모습을 볼 수 있습니다.

LiveData를 활용하여 UI에 데이터 변경 알림을 받을 수 있어 복잡한 로직을 줄일 수 있습니다.


Model

ViewModel에 SearchService를 직접 호출하여 데이터를 가져오고 해당 데이터를 ViewModel에 있는 LiveData 객체에 할당할 수 있지만

이렇게 개발했을 경우 기능이 추가됬을 때 유지관리가 어려워질 수 있기 때문에 Repository를 만들고

Repository에 데이터를 가져오는 작업을 처리하면 좀 더 깔끔한 코드를 짤 수 있습니다.

SearchRepository

class SearchRepository(private val searchService: SearchService) {
    fun fetchUsers(name: String): Single<UsersResponse> {
        val param = HashMap<String, String>().apply {
            this["q"] = "$name in:name"
        }
        return searchService.getUsers(param)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
    }
}

현재는 서버에서 데이터를 가져오는 방법밖에 없지만

추후 캐싱에 대한 기능을 추가하고 싶으면 Repository 클래스 안에서 기능을 구현하고

Repository는 다양한 데이터 소스 중재자 역할을 할 수 있습니다.

Repository 객체를 만들면 SearchService의 객체도 만들어야하는데 DI를 이용하면 이러한 문제점을 쉽게 해결할 수 있습니다.

상황에 따라 Dagger2 또는 Koin을 이용하시면 됩니다.


Review

지금까지 개발하면서 MVP + Clean Architecture + DDD 방식을 사용해왔는데

MVP의 가장 큰 문제점인 Presenter 안에 중복된 코드가 나타나는 현상이 나타났습니다.

이러한 문제점을 해결하기 위하여 AOP(Aspect Oriented Programming) 방식도 해보고

다양한 방법으로 개발을 진행하였으나 보다 더 깔끔한 코드를 짜는데 한계를 느꼈습니다.

그 후 AAC에 대하여 학습을 진행하고 AAC + MVVM + DDD 방식을 사용해보니 전보다 복잡성이 낮아지고 구조도 간결해졌습니다.

추후 드로이드 나이츠 2020에서 DDD를 적용하여 복잡한 도메인을 단순하게 만드는 방법에 대하여 발표할 예정입니다. DDD를 왜 사용해야하는가 부터 실제 프로덕션에 적용하였을때 겪었던 부분에 대하여 공유할 예정입니다. 많은 관심 부탁드립니다.

David
Android Developer
강남언니에서 안드로이드 개발을 맡고 있습니다. 새로운 기술을 꾸준히 연구하고 있고 코드 품질을 높이기 위하여 노력하고 있습니다.