출처 : https://upday.github.io/blog/model-view-viewmodel/

6개월동안 upday 앱을 개발하는데 4가지 디자인 패턴을 적용한 후에, 우리느 아주 중요한 교훈 하나를 얻었습니다. 디자인 변화에 대해서 빠르게 반응할 수 있는 아키텍쳐 패턴이 필요하다는 것이었습니다! 그 고민들 끝내 선택한 패턴이 바로 Model-View-ViewModel 패턴입니다. 저와 함께 MVVM을 알아보고 우리가 어떻게 upday에 적용하였으며 무엇이 좋았는지 알아보도록 합니다.

Model-View-ViewModel 패턴

MVVM 패턴의 주요 플래이어들을 살펴봅시다:

  • View — ViewModeld에게 유저의 동작을 알려줍니다.
  • ViewModel- View에 관련된 데이터 스트림을 드러냅니다.
  • DatModel — 데이터 소스를 추상화합니다.

언뜻 보기에는, MVVM이 Model-View-Presenter 패턴과 매우 비슷하게 보일 수 있습니다. 둘 다 view의 상태와 동작을 추상화 하는데 큰 기여를 하기 때문입니다. Presentation 모델은 View를 특정한 유저 인터페이스 플랫폼으로부터 독립시킵니다. 반면에 MVVM 패턴은 유저 인터페이스의 이벤트 기반 프로그래밍을 단순화 하기 위해 만들어 졌습니다.

만약 MVP 패턴이 의미하는 바가 Presenter가 View에 보여줄 것들을 직접적으로 말해주는 것이라 한다면, MVVM에서는, ViewModel이 이벤트들의 스트림을 뷰가 그 스트림에 바인드할 수 있도록 노출 시킵니다. 이처럼, ViewModel은 Presenter와는 다르게 View의 참조자를 가지고 있을 필요하 없습니다. 이것은 MVP가 필요로 했던 모든 인터페이스가 없어져도 된다는 것을 의미하죠.

뷰들은 또한 ViewModel에게 다른 액션들이 발생했음을 알려줍니다. 그래서, MVVM패턴은 View와 ViewModel의 양방향 데이터 바인딩을 지원합니다. 그리고 View와 ViewModel은 many-to-one 관계가 있다고 할 수 있습니다. View는 ViewModel의 참조자를 가지게 되지만, ViewModel은 View에대한 정보가 전혀 없게 됩니다. 데이터 소비자(consumer)는 데이터 공급자(producer)에 대해서 알아야 하지만 데이터 공급자(이 경우엔 ViewModel)는 데이터 소비자가 누구인지 알지도, 신경쓰지도 않습니다.

Model-View-ViewModel 클래스 구조

upday에 적용된 Model-View-ViewModel

upday 블로그에 올라가 있는 안드로이드 포스팅들을 훑어보면 어떤 라이브러리를 즐겨 사용하는지 알 수 있습니다. 바로 RxJava 인데요. RxJava가 upday의 코드의 중추이기 때문에 더이상 걱정이 없어졌습니다. MVVM 패턴에 필요한 이벤트 기반 프로그래밍 부분은 바로 이 RxJava의 Observable 을 이용해 완성이 되었습니다. 자, 우리가 어떻게 RxJava를 이용하여 MVVM 패턴을 안드로이드 앱에 적용하였는지 알아보도록 할까요?

DataModel

DataModel은 데이터를 이벤트 스트림을 통해서 소비 가능하게(consumable) 노출시킵니다. RxJava의 Observable 을 이용해서 말이죠. 그것은 네트워크 계층이나 데이터베이스 또는 shared preferences 등의 다양한 소스로 부터 데이터를 구성합니다. 그리고 쉽게 소비가능한 데이터를 누구든지 필요한 것들에 노출시킵니다. DataModel은 모든 비지니스 로직을 가지고 있게 됩니다.

단일 책임 법칙(single responsibility principle)에 대해서 우리가 강조하는 것은 나중에 DataModel을 만들도록 이끌 것입니다. 예를들어, 출력값을 API 서비스와 데이터베이스 계층으로부터 받아와 구성하는 ArticleDataModel이 있다고 합시다. 이 DataModel은 age filter를 적용하여 최근의 뉴스들이 데이터베이스로부터 받아지도록 하기 위해서 비지니스 로직을 다루게 됩니다.

ViewModel

ViewModel은 앱의 View를 위한 model입니다. View가 추상화 된 것이죠. ViewModel은 DataModel로부터 필요한 데이터를 받고, UI 로직을 적용한 뒤 View가 소비하는 데이터를 노출시킵니다. DataModel과 비슷하게, ViewModel은 Observable을 통해서 데이터를 노출시킵니다.

우리는 이를 적용하면서 ViewModel에 대한 두가지를 배웠습니다:

  • ViewModel은 단순히 어떤 이벤트가 아닌 View의 상태를 노출시켜야 합니다. 예를들어, 만약 User 객체의 이름과 이메일을 나타내야 한다면, 두개의 스트림을 만드는 것 보다는, DisplayableUser 을 만들어 이 두가지를 하나로 감싸는 것이 더 좋습니다. 이 스트림이 매번 이름과 이메일이 바뀔때마다 정보를 내보낼 것입니다. 이런 방법으로, 우리는 View가 항상 User 의 상태를 표시하도록 하였습니다.
  • 우리는 유저의 모든 동작들이 ViewModel을 통하도록 만들었고, 모든 뷰의 로직들이 ViewModel에 있도록 하였습니다.

두가지 토픽을위해 MVVM + RxJava 를 사용하는 중에 자주하는 실수 라는 블로그 포스팅을 작성하였으니 확인해 보세요.

View

View는 앱에서 유저 인터페이스의 실질적인 부분입니다. Activity , Fragment 안드로이드 View 도 이 View가 될 수 있습니다. 이 View의 onResume() onPause() 에서 이벤트 소스를 바인딩, 언바인딩 하게됩니다.

private final CompositeSubscription mSubscription = new CompositeSubscription();

@Override
public void onResume() {
super.onResume();
mSubscription.add(mViewModel.getSomeData()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::updateView,
this::handleError));
}

@Override
public void onPause() {
mSubscription.clear();
super.onPause();
}

만약 MVVM의 View가 커스텀 안드로이드 View 라면, 그 바인딩을 생성자에서 해야 할 것입니다. 메모리 누수로 이어지는, subscription이 계속 남아있게 되는 경우를 피하기 위해서, 언바인딩이 onDetachedFromWindow 에서 이루어집니다.

private final CompositeSubscription mSubscription = new CompositeSubscription();

public MyView(Context context, MyViewModel viewModel) {
...
mSubscription.add(mViewModel.getSomeData()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::updateView,
this::handleError));
}

@Override
public void onDetachedFromWindow() {
mSubscription.clear();
super.onDetachedFromWindow();
}
}

Model-View-ViewModel 클래스들의 테스트 가능성

Model-View-ViewModel을 사랑하게된 가장 주된 이유는 테스트하기 쉽다는 점이었습니다.

DataModel

제어 반전 패턴 (inversion of control pattern)의 사용이 우리의 코드에 많은 부분을 차지하게 되었고, Android 클래스들을 덜 사용함에 따라 DataModel의 테스트를 구현하는 것이 용이해졌습니다.

ViewModel

우리는 View와 유닛 테스트, 이 둘을 ViewModel로 부터 받은 데이터를 소비하는 것들로 여기며 구현하였습니다. ViewModel은 UI나 Android 클래스들로부터 완전히 분리되었고, 결국 유닛 테스트 하기 쉽게 되었습니다.

ViewModel이 단순히 DataModel로부터 받은 데이터를 노출시키는 역할을 하는 예시를 살펴봅시다:

public class ViewModel {
private final IDataModel mDataModel;

public ViewModel(IDataModel dataModel) {
mDataModel = dataModel;
}

public Observable<Data> getSomeData() {
return mDataModel.getSomeData();
}
}

이 경우에 ViewModel을 위한 테스트를 구현하는 것은 아주 쉽습니다. Mockito의 도움을 받아, DataModel을 모킹하고(mocking) 반환하는 데이터를 조정합니다. 그다음, getSomeData() 를 통해 얻은 Observable 을 구독할때(subscribe), 예상되는 데이터가 내보내졌는지를 확인합니다.

public class ViewModelTest {

@Mock
private IDataModel mDataModel;

private ViewModel mViewModel;

@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);

mViewModel = new ViewModel(mDataModel);
}

@Test
public void testGetSomeData_emitsCorrectData() {
SomeData data = new SomeData();
Mockito.when(mDataModel.getSomeData()).thenReturn(Observable.just(data));
TestSubscriber<SomeData> testSubscriber = new TestSubscriber<>();

mViewModel.getSomeData().subscribe(testSubscriber);

testSubscriber.assertValue(data);
}
}

만약 ViewModel이 안드로이드 클래스에 접근할 필요가 있으면, Provider 를 호출하는 wrapper를 생성합니다. 예를들어, 안드로이드 resource들에 접근하기 위해서 IResourceProvider 를 생성하고, 이 객체가 String getString(@StringRes final int id) 와 같은 메소드들을 노출하게 됩니다. IResourceProvider 의 구현은 Context 의 참조자를 가지고 있게 되지만, ViewModel은 단지 IResourceProvider를 가지고 있게 되는 것이죠.

위에 언급하였듯이, 그리고 자주하는 실수에 대한 블로그 포스트에 남겼듯이, 우리는 model 객체를 데이터의 상태를 담기 위해서 만듭니다. 이렇게 하는 것은 훨씬더 높은 수준의 테스트 가능성을 가지게 해주며, ViewModel로 부터 내보내진 데이터를 컨트롤 할 수있도록 해줍니다.

View

UI 로직이 최소화 되었기 때문에, View는 Espresso로 테스트하기 쉽게 되었습니다. 우리는 DaggerMock과 MockWebServer과 같은 라이브러리들을 가지고 UI 테스트의 안정성을 개선하였습니다.

MVVM이 정말 적절한 솔루션인가?

지금까지 저희는 거의 1년 동안을 RxJava를 가지고 MVVM 패턴을 적용하여 개발해왔습니다. 그러면서 우리는 View가 단지 ViewModel의 소비자이기 때문에, UI 요소들을 아주 최소한의 변경을 통해서 다른것으로 대체할 수 있었습니다.

우리는 또한 이 과정을 통해서 작업 거리들을 분리하는 것이 얼마나 중요한지를 알게 되었으며 코드는 분리 되어야 하고, View와 ViewModel이 특정한 작은 역할을 가져야 한다는 것을 알게 되었습니다. ViewModel은 View에 주입됩니다. 이것의 부분의 경우에 우리가 View들을 단지 XML UI에 추가해 주기만 하면 된다는 것을 의미합니다. 그렇기 때문에 UI 요청들이 다시 변할때에, 우리는 기존의 View를 다른 View로 쉽게 대체할 수 있습니다.

결론

MVVM은 MVP가 작업들을 분리하는 장점은 고스란히 물려 받는 동시에 데이터 바인딩의 장점또한 받아들입니다. 그 결과 모델은 가능한 많은 작업들을 하게 되고, view의 로직은 최소화되게 됩니다.

우리 앱의 유아기에 많은 디자인 변화를 겪으면서, 우리는 MVVM를 upday 앱의 청소년기에 적용하게 되었습니다. 이 기간은 우리가 실수로부터 많이 배우는 기간이 되었습니다. 이제, 디자인을 다시 하는 일을 충분히 감당할 수 있게된 우리 앱을 자랑할 수 있게 되었네요. 우리는 결국 upday를 성인시기라고 부를 수 있는 시기에 가까워 졌습니다.

MVVM 구현의 간단한 예시는 여기서 확인해 볼 수 있습니다.

“Hello World!” 앱으로 MVP와 MVVM을 비교해 보는 예시는 여기에서 볼 수 있습니다.

--

--