MVC와 MVP에서 View와 Model 간 의존성은 없었지만 Controller 또는 Presenter와 View 사이의 의존성은 여전히 존재했다. MVVM에서는 ViewModel이란 개념을 도입하였고 ViewModel은 View를 참조하지 않기 때문에 의존성이 없다. 이것은 ViewModel이 독립적인 존재라는 뜻이기 때문에 ViewModel과 View는 1:n의 관계를 가질 수 있는 것이다.
MVVM
MVVM은 Model, View, ViewModel의 약자이다. 안드로이드에서 각각에 해당하는 것은 무엇이며 각각의 역할은 무엇인지 알아보자.
Model
MVC, MVP에서와 동일한 개념이며 의존성이 없기 때문에 MVVM 예제에서 그대로 사용 가능하다.
View
사용자가 보는 UI 부분이다. MVP처럼 레이아웃 파일(.xml)은 물론이고 Activity와 Fragment도 View로 분류한다. 보통 Command 패턴이나 DataBinding을 사용하여 ViewModel의 의존성을 없앤다. 여기서는 간단한 예제를 사용하기 때문에 Command 패턴 적용 시 오히려 코드가 복잡해져 이해하는데 도움보다 방해가 될 수 있다. 따라서 LiveData와 DataBinding을 사용한 방법을 살펴본다. MVVM에서 왜 이 두 라이브러리가 정말 유용한 것인지 알 수 있을 것이다. Activity를 통해 바인딩하는 방식과 레이아웃 파일에 직접 바인딩하는 방식으로 나누어 살펴볼 것이다.
ViewModel
View에 필요한 데이터를 Model로부터 가져와서 처리하고 해당 데이터를 View에 보여주기 위한 역할을 한다. 역할 자체만 보면 Controller나 Presenter와 유사하게 느껴질 수 있다. 다만 LiveData와 DataBinding을 사용하여 View와의 의존성을 제거할 수 있다. 안드로이드에서 ViewModel이라는 JetPack 라이브러리를 제공하여 더욱 편리하게 ViewModel을 구성할 수 있지만 ViewModel 라이브러리(AAC ViewModel)를 사용해야만 MVVM인 것은 아니다. 반대로 ViewModel 라이브러리를 사용하지 않았다고 해서 MVVM이 아닌 것도 아니다. 일반적인 MVVM에서 ViewModel은 앞서 말한 데이터 처리 및 View 표시를 위한 역할을 수행하면 된다. 다만 ViewModel 라이브러리를 사용하면 UI 관련 데이터의 수명주기를 관리하는데 용이하다. 예를 들어 Activity의 생명주기를 따르는 ViewModel은 onDestroy()가 호출될 때까지 메모리에 남아 있다.
MVVM 패턴의 구성 요소를 보면 Binder도 존재한다. ViewModel 라이브러리를 사용하지 않는다고 하여 MVVM이 아니라고 말할 수 없듯이 DataBinding 라이브러리를 사용하지 않았다고 MVVM이 아니라고 할 수 있을까? DataBinding을 기준으로 보면 DataBinding의 개념을 실현할 수 있는 다른 방법이 있다면 충분하다고 생각한다. 아래에서 다루게 될 View 1.0.0 버전과 같이 'LiveData를 DataBinding의 개념을 실현할 도구'로 사용하는 경우도 조악하지만 MVVM을 실현한 것이라고 본다. 다만 엄격하게 Binder라는 기준으로 보면 UI 관련 보일러플레이트 코드를 용납하기 어렵고 MVVM을 제대로 실현한 것이라고 말하기 어렵다.
MVVM 구조는 위와 같다. View와 Model은 서로 분리되어 있다. View는 ViewModel에 의존하지만 ViewModel은 View에 의존하지 않는다. 점선으로 되어 있는 것은 두 가지를 뜻한다. 첫 번째는 LiveData를 이용하여 데이터를 전달하는 것을 의미한다. View에서 ViewModel을 참조하기 때문에 ViewModel의 LiveData의 Observer를 등록할 수 있다. 따라서 View에서는 LiveData의 값을 수신할 수 있다. 두 번째는 DataBinding 라이브러리를 사용해서 데이터를 View와 동기화하는 것을 의미한다. 어떻게 구현하느냐에 따라 데이터 전달 방식이 조금 다른 것이다. 다만 DataBinding 라이브러리를 사용하지 않더라도 LiveData를 통해서 데이터를 전달하여 View와 동기화하는 작업을 하는 첫 번째 방식도 결국 전통적인 의미에서 DataBinding을 실현 중인 것이다.
결국 안드로이드의 LiveData, DataBinding, ViewModel 라이브러리는 MVVM의 지향점(View와 ViewModel 의존성 최소화)을 이해하고 그것을 실현하는 데 있어 편리함을 제공하는 도구에 불과하다. 다시 말하지만 ViewModel을 사용한다고 MVVM이 아니고 MVVM을 하기 위해서 ViewModel 라이브러리가 필수는 아니다.
예제
MVC, MVP에서 다룬 예제와 동일한 동작을 하는 앱을 MVVM을 적용하여 구현한다. 총 세 가지 버전을 살펴볼 것이다. 단계별로 조금씩 코드를 간결하게 변경할 것이다. branch로 버전을 구별했다. 첫 번째 버전은 ver 1.0.0으로 DataBinding 라이브러리를 사용하지 않고 LiveData만 사용한 버전이다.
MVC, MVP 예제와 완전히 동일하다. 파일을 그대로 복사해서 사용해도 된다. Model은 의존성 없이 독립적이기에 가능하다. 당연히 여기서 살펴볼 버전에 관계없이 모든 버전에서 동일하다.
[ViewModel]
ViewModel에서 해야 할 동작은 다음과 같다.
1. 메뉴(아메리카노) 수량 증감 요청 (to Model)
2. UI에 DataBinding 할 LiveData 값에 수량 정보 할당 (for View)
3. 수량 증감에 따른 합계 가격 증감 요청 (to Model)
4. UI에 DataBinding 할 LiveData 값에 합계 가격 정보 할당 (for View)
모든 버전에서 위와 같이 동일한 동작을 한다. 다만 2, 4의 경우에 DataBinding 할 대상 View가 Activity냐 레이아웃 파일이냐의 차이가 있다. ver 1.0.0, ver 2.0.0은 Activity를 대상으로 하고 ver 3.0.0은 레이아웃 파일을 대상으로 한다. 그러나 ViewModel의 코드는 모두 동일하다. 왜냐면 View에 의존성 없이 독립적이기 때문이다. 결국 ViewModel에 의존하는 View가 어떤 방식으로 데이터를 UI와 동기화시킬 것인지 처리한다.
1 : Jetpack 라이브러리인 ViewModel을 상속
2 ~ 3 : 수량 정보와 합계 가격 정보를 담을 LiveData 선언
8 ~ 10 : 수량 정보를 감소시킨 후 감소된 값을 LiveData에 할당하고 합계 가격 정보 감소를 위한 함수 호출
14 ~ 16 : 수량 정보를 증가시킨 후 증가된 값을 LiveData에 할당하고 합계 가격 정보 증가를 위한 함수 호출
[View] ver 1.0.0
View는 버전 별로 다르기 때문에 먼저 ver 1.0.0을 살펴본다.
11 : ViewModel에 대한 의존성이 발생한다. ViewModel은 생명주기를 인지하여 UI 데이터를 저장 및 관리하기 때문에 ViewModelProvider를 통해서 생성한다.
22 : ViewModel의 수량 정보 LiveData에 대한 Observer 등록
23 : LiveData의 갱신된 값을 americanoCountText라는 TextView를 통해서 표시한다. 이 부분이 바로 DataBinding을 실현한 것이다.
34 : 수량 증가를 위한 버튼의 입력을 받고 해당 입력 이벤트를 ViewModel에 전달한다.
매우 간단한 코드이다. ver 2.0.0에서는 findViewById를 제거하기 위해 DataBinding 라이브러리를 적용할 것이다.
[View] ver 2.0.0
이 버전에서 DataBinding 라이브러리를 사용하는 이유는 단순히 findViewById를 제거하는 목적임을 다시 한번 상기해야 한다. DataBinding의 실현은 이전 버전과 동일하게 Observer를 통해 받은 LiveData 값을 TextView를 통해서 표시하는 방법을 사용할 것이다. DataBinding 라이브러리를 사용하여 DataBinding을 실현한 것이 아니라는 의미이다. 이것을 말하기 위해 버전을 3개로 나눈 것이다. DataBinding 라이브러리를 사용한다고 모두 MVVM이 아니다. 라이브러리를 쓰든 구현을 하든 DataBinding을 통해서 의존성을 없애야 MVVM이라고 할 수 있다.
7 : 레이아웃에 바인딩
16 : 단순히 레이아웃에 바인딩하여 View에 접근하는 용도로 사용 중이다. 진정한 DataBinding을 위해 DataBinding 라이브러리를 사용한 부분은 없다. 결론적으로는 ver 1.0.0과 같은 방법으로 DataBinding을 실현한 것이긴 하다.
[View] ver 3.0.0
이 버전에서는 Observer와 setOnClickListener를 제거하여 Activity 코드를 아주 간결하게 변경한다. DataBinding 라이브러리를 통해서 ViewModel의 LiveData 값을 레이아웃 파일에 곧바로 동기화시키기 때문이다. 버튼에 대한 입력 동작도 레이아웃 파일에서 onClick으로 처리하였다. DataBinding을 실현시키기 위해 DataBinding 라이브러리를 사용한 예제이다.
9 : DataBinding에서 LiveData의 변화를 감지하기 위해서 사용할 LifecycleOwner를 설정해야 한다. 여기서는 Actvity로 지정하면 된다.
10 : 바인딩된 변수에 viewModel(OrderViewModel 객체)을 할당한다.
32 : 수량 감소 버튼에 대한 입력 이벤트를 처리할 함수를 지정한다. 곧바로 ViewModel로 입력 이벤트를 전달한다.
44 : DataBinding 라이브러리를 사용해서 DataBinding이 실현될 수 있도록 ViewModel의 LiveData를 바인딩한다.
55 : 수량 증가 버튼에 대한 입력 이벤트를 처리할 함수를 지정한다. 곧바로 ViewModel로 입력 이벤트를 전달한다.
78 : DataBinding 라이브러리를 사용해서 DataBinding이 실현될 수 있도록 ViewModel의 LiveData를 바인딩한다.
MVVM에서 ViewModel은 View에 대한 의존성이 없기 때문에 유닛 테스트에 용이하고 테스트 시 MVP처럼 가상의 View를 만들 필요가 없다. LiveData에 변경된 Model의 값이 잘 할당되고 있는지 살펴보면 된다.