MVVM - Model, View, ViewModel

Updated:

MVVM - Model, View, ViewModel

MVVM 의 개념의 기초는 PM(Presentation Model - 마틴파울러의 제안)의 기반으로 만들어 졌다. PM 은 View에 렌더링에 필요한 데이터를 가지고 있는 개체이고, View 는 PM 의 데이터를 기반으로 렌더링한다. PM 의 중요한 포인트는 PM 이 들고 있는 데이터와 View는 항상 동기화가 되어야한다는 것. 여기서 후에 PM 을 ViewModel 이라는 이름으로 명명하면서 MVVM이 탄생했다.

전체 작업을 UI 로직 작업 과 비즈니스 로직 작업으로 나눌 수 있다. 이렇게 나누어진 작업을 디자인에 특화된 UI 개발자는 UI쪽을 담당하고, 비즈니스 로직에 특화된 개발자는 나머지 부분을 담당할 수 있도록 하기 위해서 나뉘어있다.

MVVM은 Model, View, ViewModel의 약자이고 구조는 다음과 같다.

  • View
    • 화면에 표현되는 레이아웃에 대해 관여.
    • 기본적으로 비지니스 로직을 배제 하지만 UI 관련된 일부 로직을 수행할 수 있다.
      • 사람들 마다 의견이 다를 수 있고, ‘안드로이드 내부에서는 데이터를 뿌리는 기능만 수행해야 한다’ 라는 의견을 가진 이도 있다.
    • ViewModel 을 관찰하고 있다가 상태 변화가 전달되면 화면을 갱신해야한다.
  • ViewModel

    • View에 연결 할 데이터와 명령으로 구성. 변경 알림을 통해 View에 상태 변화를 전달한다. 그에 따른 결과를 화면에 반영 하는 여부는 View가 택하도록 하며 명령은 UI 를 통해서 동작하도록 한다.

    • 비동기 작업 통해 UI의 응답성을 유지. 사용자의 성능 인식 기능을 개선하기 위해 UI 스레드를 차단 해제 된 상태로 유지해야한다. View Model 에서 I/O 작업에 비동기 메서드를 사용하고 이벤트를 발생시켜 View의 속성변경을 알린다.

    • View Model 과 Model 사이에는 일대 다 관계가 있으며 ViewModel은 View의 컨트롤이 직접 data binding 을 할 수 있도록 View 에 Model 클래스를 직접 노출 하도록 선택할 수 있다. 이 경우에는 Model 은 data binding을 지원하고 notification even 를 변경 할 수 있도록 디자인 되야한다.

    • AAC(Android Architecture Component) 의 ViewModel 은 LifeCycle 을 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계된다. ViewModel 클래스를 사용하면 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있다.

    • Andorid 에서 MVVM 을 사용하려고 한다면, 반드시 AAC 의 ViewModel 을 사용하지 않아도 구현은 가능하다.

    • View의 상태와 행동이 추상화 된것.

    • View의 input과 output이 명시되어 있는 인터페이스

    • 순수함수 처럼 같은 값의 input에는 항상 같은 값의 output이 반환되어야함

    • output은 View의 상태와 Route로 나뉨

  • Model
    • App 의 데이터를 캡슐화 하는 비시각적 클래스. 일반적으로 데이터를 액세스 하거나 캐싱이 필요한 서비스 또는 레포지 토리와 함께 사용된다.
    • ViewModel 에서 data를 가져갈 수 있게 준비하고, 그에대한 event 를 보낸다.

View 은 ViewModel 을 알지만 ViewModel 은 View를 알 수 없고, ViewModel은 Model을 알지만 Model은 ViewModel을 몰라야한다.


MVVM 은 MVP 와 반대로 View 가 능동적이다. View 가 스스로 ViewModel 객체의 어떤 데이터가 필요한지 직접적으로 관찰한다. 관찰 하기 위해서는 ViewModel 의 데이터는 관찰 가능한 형태여야한다.

LiveData, ObservableField 객체(Google) 를 사용하거나, Observable 객체(RxJava)도 사용 가능하다.

변화가 생기면 즉시 View는 알림을 받고, 알맞은 View 렌더링 로직을 수행하게 된다.

Google에서 제안한 예제에서는 2가지 형태로 호출하는 방법이 있다.

  • ViewModel 내부의 메서드를 직접호출
    • MVP 와 동일한 방법
    • 별도의 인터페이스나 클래스 객체를 만들어서 비즈니스 로직을 위임하는 것이 로직을 분리하는 측면에서 바람직
  • ViewModel 내부에 있는 CommandObservable 객체를 통해 호출
    • View에서 발생한 Action을 전달만 할 뿐 비즈니스 로직 처리는 해당 CommandObservable을 관찰하는 쪽에서 담당하기 때문에 비즈니스 로직을 좀 더 강제로 분리 할 수 있다.
    • 하지만 CommandObservable을 관찰하는 객체가 ViewModel 자신이라면 그것은 불필요한 관찰이 될 수 있다. 그렇기에 CommandObservable을 사용 한다면 ViewModel 이 아닌 다른 비즈니스 로직 처리 객체가 관찰을 하도록 설계하는 것이 바람직하다.

MVC 와 MVVM 의 차이는 그림 한장이면 충분할 것 같아서 가져왔다.

  • 출처 : https://blog.yena.io/studynote/2019/03/16/Android-MVVM-AAC-1.html

MVVM 으로 개발시에는 Databinding 라이브러리 사용을 고려하는 것이 좋다고 한다. View의 dependency 를 Activity, Fragment 의존에서 xml 의존으로 내릴 수 있으며 다음의 그림으로 표현이 가능하다.

View는 실제 XML 만 존재하며 View에 코드적으로 접근할 일이 있다면 BindingAdapter 와 InverseBindingAdapter 를 통해 접근하고, 상태값을 통해 View를 갱신한다. ViewModel 은 View에 표현할 데이터를 들고 있으면서 MVP 의 Presenter와 같이 비즈니스 로직과 View 사이의 중재자 역할을 한다.

ViewModel 내부에는 observable 필드들을 정의하고, View의 Action을 받을 interface 또는 CommandObservable 객체를 미리 정의하면 된다. ViewModel 에서 미리 정의된 값을 xml 에서 observing 하고 필요한 View의 Action 을 호출하도록 만들면 분리된 영역 내에서 서로 부딪히지 않고 작업이 가능하다. 각 모듈간의 의존성이 낮기에 각자의 파트에서 개발이 가능하는데 불편함이 줄어든다.

  • UI 개발자는 xml, BindingAdapter, InverseBindingAdapter만 개발

  • 비즈니스 로직 담당자는 나머지 부분을 개발

MVP 에서는 영역분리가 가능은 하지만 충돌 영역이 생긴다. View는 Presenter 를 , Presenter는 View 를 호출해야 하기 때문에 작업 영역이 겹치고 해당 부분에서 충돌이 발생 할 수 있다.

databinding 라이브러리를 사용하게 되면, xml 이 디자인 요소 배치를 담당할 뿐 아니라, ViewModel 을 통해 data 연결과 이벤트 전달의 역할을 한다.

특정 View에 어떤 데이터가 매핑되는지 xml상에서 바로 입력이 가능하고, observe 함수를 사용하지 않아도 빌드시 알아서 observe 함수를 자동 생성한다. MVP 나 기존의 방법으로 매핑 하려면 각각의 View 에 id 를 부여하고 해당 View 를 findViewById 등으로 참조를 가져온 후에, 데이터 세팅해야하는데, databinding 라이브러리는 아이디 없이도 알아서 매핑시킨다.

MVVM 의 장점

  • ViewModel 이 model 의 어댑터 역할을 하여 model 의 주요한 코드 변경을 방지 할 수 있다.
  • 개발자는 View를 사용하지 않고, ViewModel 및 Model 에 대한 Unit 테스트를 만들 수 있고, ViewModel 에 대한 Unit 테스트는 View 를 사용하는 것과 정확하게 동일한 기능 실행을 할 수 있다. -> UI, 비지니스 로직, DB 들이 기능별로 모듈화가 되어있기 때문
  • 완전하게 새롭게 구성되는 UI를 다시 디자인 하여도 코드를 건드리지 않고 만들 수 있다. 새롭게 만들어진 View 에서도 기존의 ViewModel 과 동일한 기능이 수행이 가능하다.
  • UI 개발자와 비즈니스 로직 개발자가 각각의 분리되어 동시에 작업이 가능하다.비즈니스 로직 개발자가 ViewModel 과 Model 의 요소에서 작업을 하고 있을 때 UI 개발자는 View에만 집중하여 작업할 수 있다.
  • ViewModel 을 통해 data 를 참조하기 때문에 Activity, Fragment 의 생명주기의 영향을 받지 않는다. 또한 View가 활성화 되어있을 경우에만 작동하기 때문에 불필요한 메모리 사용을 줄일 수 있다.
  • ViewModel과 View가 일 대 다 연결이 가능하기 때문에 ViewModel 의 메소드를 A-Activity, B-Activity 등 여러 View에서 호출해 재 사용하기 편하다. 사실 해당 구조는 사용이 가능하지만 경우의 수가 극히 적다.
  • binding되는 시점에 모든 input에 대한 output을 산출하는 로직이 정해지기 때문에 개발자가 상태를 관리해야하는 위험을 줄여준다.
  • Databinding을 통해 수많은 보일러 플레이트 코드를 줄일 수 있다.

단점

  • xml 내부에서 중괄호 안에 선언하는 구문을 별도의 문법을 사용하는 불편함이 생긴다. 특정 observing 필드를 여러 View에서 참조하는 경우 이 필드를 참조하는 View 들을 찾기 위해서는 텍스트 기반 검색을 수동으로 해야한다.
  • 또한 Xml escaping 에서 불편함을 느낌 수 있다. xml 에서는 <, > 문자가 허용 되지 않기 때문에 &lt; &gt; 형식으로 써야한다.
  • 기존에 비해 추가로 만들어 주어야 하는 클래스도 많을 수 있으며, 연결해주어야하고 해당 과정이 복잡해지면 cost 가 많이 필요로 할 수 있다.

XML 에 정의한 View 에서 발생한 이벤트도 일반적으로 ViewModel을 매개로 이루어지며 방법은 크게 3가지가 있다.

  • ViewModel 직접 호출 방식
app:onRefreshListener="@{viewmodel::onRefresh}"
app:refreshing="@{viewmodel.dataLoading}"

android:onClick="@{() -> viewmodel.addNewTask()}"=

ViewModel은 옵저빙 데이터를 보관하는 용도가 주요 역할이기 때문에 이렇게 메서드를 ViewModel 에 넣는 방식은 지양하는게 좋다. ViewModel에 비즈니스 로직이 직접적으로 들어가는 경우는 각 화면이 해당 비즈니스 로직에 특화되고, 해당 비즈니스 로직이 다른 곳에서 쓰이지 않아야 설계적으로 의미가 있다.

  • Observing Command Field를 통한 방식

    • Add Command Observing Field
    val snackbarMessage = SingleLiveEvent<Int>()
    val newTaskEvent = SingleLIveEvent<Void>()
    
    • Observe Field
    newTaskEvent.observe(this@Activity, Observer<Void>{
    	this@Activity.addNewTask()
    })
    
    • Invoke Field
    fun addNewTask(){
    	newTaskEvent.call()
    }
    

​ Observing Command 를 사용하는 경우, 위와 같이 3가지 단계를 늘 작성해주어야한다.

  • 관찰 필드 선언

  • 로직 수행부에서 관찰

  • 필드를 invoke 시켜 Observer 들에게 알려준다.

    기존 MVP 에서는 함수를 작성하고, 그 함수를 실행만 하면 됐지만, 해당 방식은 함수와 함수 실행 사이에 3가지 일이 추가된다. 관찰 포인트를 두는 것이 깔끔하지만 대부분은 1곳에서만 필요로 하기 때문에 코드가 늘어나는 부분에 있어 가독성이 떨어지고 유지보수가 힘들 수 있다.

  • 별도의 Listener를 통한 방식

    listener = object : TaskDetailUserActionsListener{
    	override. fun onCompleteChanged(v : View){
    		viewmodel?.setCompleted((v as CheckBox).isChecked)
    	}
    }
    

    위와 같이 별도의 Listener를 두게 되면 ViewModel 에서 비지니스 로직을 떼어 낼 수 있어 좋은 점이 있다. Listener의 실제 구현은 Binding이 일어나는 곳에서 할 수 있게 되는데, 그 곳이 만약 Activity 나 Fragment라면 해당 클래스에 비지니스를 작성하기 보다는, 별도의 분리된 클래스로 위임해주는 것이 좋다.


참고

https://developer.android.com/topic/libraries/architecture/viewmodel

https://docs.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/mvvm

https://medium.com/@bansooknam/android-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EB%B9%84%EA%B5%90-mvp-mvvm-svc-2-7c44ea167d56

https://medium.com/@jsuch2362/android-%EC%97%90%EC%84%9C-mvvm-%EC%9C%BC%EB%A1%9C-%EA%B8%B4-%EC%97%AC%EC%A0%95%EC%9D%84-82494151f312

https://medium.com/hongbeomi-dev/aac%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-mvvm-pattern%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%9C-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-1d6d73689bd0

https://blog.yena.io/studynote/2019/03/16/Android-MVVM-AAC-1.html

https://velog.io/@jojo_devstory/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%ED%8C%A8%ED%84%B4-MVVM%EC%9D%B4-%EB%AD%98%EA%B9%8C

https://poqw.github.io/about_mvvm/