본문 바로가기

Android

[AOS] ViewModel

ViewModel

ViewModel은 Android Jetpack Architecture 구성요소의 하나로, 개발하면서는 무조건 들어볼 수 밖에 없는 MVVM 디자인 패턴의 ViewModel에서 탄생했다.

많이들 MVVM에서의 ViewModel(VM)과 이 글에서 설명하고자 하는 ViewModel을 같다? 다르다? 정리한 글도 많이 찾아볼 수 있는데, 엄연히는 다른 개념이고 일반적으로는 AAC(Android Architecture) ViewModel이라고 한다.

구글 개발자 사이트에서 보면, ViewModel을 다음처럼 정의하고 있다.

  • 비즈니스 로직 또는 화면 수준 상태 홀더이다.

한국어 번역이라 이해가 잘 안될 수도 있지만, 간단히 말하면 ViewModel은 내부에 비즈니스 로직(repository를 통한 local or remote 접근을 위한 로직) 또는 화면 수준 상태 홀더(View 화면에 표시하기 위해 사용되는 데이터)들을 정의해 놓는 class라고 보면 될 것이다.

 

MVVM 디자인 패턴의 MVVM에서 탄생했다고 하는데, 간단하게 MVVM을 살펴보자.


MVVM

개발자라면 MVC, MVP, MVVM 등의 디자인 패턴을 들어봤을 것이다. MVVM은 Model-View-ViewModel의 약어로 해당 구성요소로 앱을 디자인하는 것을 말한다.

왼쪽이 MVVM 디자인 패턴의 ViewModel/오른쪽이 AAC ViewModel

각 요소 Model, View, ViewModel이 무엇인지보다 각각 분리되어 있다는 개념이 중요하다. View는 일반적으로 화면으로 이해하면 되고, Model은 저장데이터, ViewModel은 그 사이에 위치해 있어서 View, Model을 연결해 주고 있다. 디자인패턴에 대한 내용은 구글링 조금만 해봐도 흘러 넘치니 다른 곳에서 더 익히면 되고, 우리에게 중요한 것은 AAC ViewModel인 오른쪽 이미지다. Activity/Fragment(View)와 분리되어 있고, Repository 하위 내용을 포함한 내용을 ViewModel이 연결해 주고 있다. 여기서도 역시 View와 ViewModel이 분리되어 있다는 것이 중요하다.


도대체 왜 중요하고 왜 ViewModel이 필요한건데?

디자인패턴이 없다면, 그냥 Activity/Fragment class 내부에 UI + UI 제어하는 내용 + UI 표현할 데이터 + 이외 로직 등등으로 구현해야 할 것이다. 거기다 모바일 기기에서 화면 회전이나, 다른 앱 실행을 했다가 다시 돌아왔을 때까지의 모든 처리를 해줘야 한다면? 끔직하다.

ViewModel은 View에 분리되어 독립적이고, View에 필요한 데이터만을 가지고 있다. 이 구조를 통해 코드로 깔끔해지고, 유지보수도 좋고, 테스트도 더 편리해야하게 만들어 준다.


ViewModel Lifecycle

이런 경우를 생각해보자. 간단한 숫자(Int)를 저장하고 있는 변수가 있고, 이 번호를 증가시킬 수 있는 버튼으로 구성된 앱이 있다. 유저가 버튼을 눌러 숫자를 증가시키는 로직만 생각하면 좋을텐데, 문제는 이런 경우다.

  • 모바일 기기가 회전된 경우
  • 다른 앱을 실행 후, 다시 돌아왔을 때 등

ViewModel이 없었을 때는 이런 경우를 처리 못했다는 내용을 말하고자 하는게 아니라, ViewModel이 등장함으로써 이러한 처리들이 더욱 간편해졌다는 것이다. 기존에는 아마 값을 유지해야하는 데이터를 Bundle에 저장(store)하는 과정과 복원(restore)하는 과정까지 신경써줘야 할 것이다. 위에서 예시로 들은 경우는 Activity/Fragment의 Lifecycle에 따라 그외의 처리과정이 더 들어갈 것이고.

ViewModel lifecycle에서 중요한 점은 데이터가 유지되는 기간이다. ViewModel Scope라 표현된 그림을 보면 Activity lifecycle이 onDestroy까지 scope로 표현되어 있다. 이말인 즉슨, ViewModel에 View에서 사용할 데이터를 저장해 놓으면, Activity/Fragment가 어떤 Lifecycle의 단계에 있어도 값이 유지된다는 것이다! (화면을 회전하건 다른앱 실행 후 돌아오건, 앱이 종료되기 전까지는 유효한 데이터가 유지)

실제로 Activity가 onDestroy되면서 종료가 되면, ViewModel은 onCleared()를 호출하고 종료된다.


ViewModelStoreOwner

자, 그럼 ViewModel이 뭔지는 알았고, 어떻게 만들어서 사용하지를 알아봐야 하는데 중요한 것이 ViewModelStoreOwner이다. 이름만 봐선 ViewModel과 관련된 소유자인것 같은데...일단 ViewModel을 사용하기 위해선 ViewModelProvider를 사용한다. 

    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        this(owner.getViewModelStore(), factory);
    }

ViewModel에서 사용하는 ViewModelProvider 생성자는 위와 같은데, 내부 생성자를 다시 호출하고 있다.

인자로 전달되는 Factory는 그대로 전달하고, ViewModelStoreOwner를 ViewModelStore로 전달하고 있다. Factory는 이후에 살펴보고, 앞서 중요하다고 말한 ViewModelStoreOwner를 보면, ViewModelStoreOnwer.getViewModelStore를 통해 ViewModelStore를 가져오는게 중요하다.

ViewModel 사용하려면 ViewModelProvider를 사용한다더니, 일단 생성자는 알겠는데 어떻게 ViewModel을 가져오는거야, 라고 생각한다면 거이 다 왔다. ViewModelProvider를 만들고, get 메서드를 통해 ViewModel을 가져오면 된다.

get 메서드 내부를 보면 ViewModelStore.get(key) 부분이 있는데, ViewModelStore class 안에는 HashMap<String, ViewModel> 값을 가지고 있다. 즉 key값에 따른 ViewModel을 가지고 있다는 말이 된다. key에 따른 ViewModel이 없으면, Factory (ViewModelProvider.Factory, ViewModelProvider의 생성자 인자)를 통해 ViewModel을 생성 후에, ViewModelStore에 저장하고 ViewModel 반환. key에 따른 ViewModel이 있으면 ViewModelStore에서 기존의 ViewModel을 반환.

 

위에서 글로써 말한 내용을 정리하면 다음과 같다.

  1. ViewModel 생성 요청
  2. ViewModelProvider 생성자를 통해 ViewModelStoreOwner와 ViewModelProvider.Factory 인자를 전달
    (ViewModelProvider(ViewModelStoreOwner, ViewModelProvider.Factory)
  3. ViewModelStoreOwner를 참조하여 ViewModelStore를 가져옴
    (ViewModelProvider(ViewModelStore, ViewModelProvider.Factory)
  4. 3에서 가져온 ViewModelStore가 ViewModel을 지니고 있으면, 해당 ViewModel 반환
  5. 3에서 가져온 ViewModelStore가 ViewModel을 지니고 있지 않으면, 2에서 전달한 인자인 Factory를 통해 ViewModel을 생성 한 뒤에 ViewModelStore에 저장하고 해당 ViewModel 반환.

여기서 중요한 것은 ViewModelStore가 ViewModel을 지니고 있다면, ViewModel을 새로 생성하는 것이 아니고 기존의 ViewModel을 반환한다는 것이다. 

이게 무슨 의미냐면, 다음과 같은 구조로 구현이 가능하다는 의미다.

A Activity는 일반적인 구조다. 대부분의 앱 구현 시에는 Activity를 생성한 뒤에, Navagation을 통해 Fragment를 Activity에 교체해서 화면에 표시할 것이다. 이런 경우 Activity에 종속된 Fragment는 동일한 ViewModel을 바라보며 데이터를 공유하고 접근할 수가 있다.

B Activity의 경우는 A Activity처럼 여러 Fragment를 가지고 있지만, B Fragment는 Activity에 종속된 ViewModel이 아니라, B Fragment에만 종속된 ViewModel을 만들 수 있다(ViewOwner를 다르게 생성할 때 전달하면).

자, 이론은 여기까지고 실제 코드를 좀 살펴보자.


샘플

예제는 간단히 Activity에 종속되는 ViewModel을 생성하고, 2개의 Fragment에서 Activity에 종속된 ViewModel을 참고하는 구조다(바로 위에서 말한 A Activity 내용).

 

ViewModel

class MainViewModel : ViewModel(){
    val count = MutableLiveData<Int>().apply { value = 0 }
}

내용은 간단히 ViewModel을 상속받고, 내부에 각 Fragment에서 참고하는 count 변수 선언했다.

 

Fragment

class AFragment : Fragment() {
    private val viewModel by activityViewModels<MainViewModel>()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.count.value
    }
}
class BFragment : Fragment() {
    private lateinit var viewModel : MainViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        activity?.run {
            viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
                .get(MainViewModel::class.java)
        }
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.count.value
    }
}

AFragment는 위임(by)을 통해 ViewModel 생성해주면서 접근하며, BFragment의 경우는 따로 ViewModelProvider를 이용해서 생성한다. 어느 것으로 해도 문제 없고, 중요한 것은 A, BFragment 모두 동일한 ViewModel을 접근한다는 것이다. (A, B Fragment를 가지고 있는 Activity의 ViewModel)

 

ViewModel 주의할 점 

샘플로 간단히 살펴본 ViewModel의 경우는 내부에 비지니스 로직을 구현하지 않았는데, 내부에서 Context를 사용하면 안된다. 메모리 누수가 발생한다고 한다. ViewModel에서 Context 사용하기 위해서는 AndroidViewModel class를 확장해서 사용하라고 한다.

class MainViewModel(application: Application) : AndroidViewModel(application) {
    private val context = getApplication<Application>().applicationContext
}

또한 ViewModelProvider의 Factory는 생성자가 없는 ViewModel의 경우에만 위처럼 간단히 NewInstanceFactory를 사용할 수 있다. 만약, ViewModel에 Repository 관련 생성자가 필요한 경우는 직접 ViewModelProvider.Factory를 구현해야 한다.

viewModel = ViewModelProvider(this, object : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return MainViewModel(
            XXXRepository()
        ) as T
    }
}).get(MainViewModel::class.java)

 

 

참고 사이트

  • https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko