Android | ViewModel은 어떻게 관리되는가

A

왜 ViewModel 이여야 하는가

Android Developers, ViewModel 개요

안드로이드 애플리케이션을 개발하거나 사용하면서, 화면이 회전되거나 오랫동안 애플리케이션을 사용하지 않는 경우 UI 관련 데이터가 삭제 되는 경험을 한적이 있을 것이다. 우리는 그러한 경우 onSaveInstanceState 함수에서 데이터를 저장했다가, onCreate 함수에서 기 저장된 데이터가 있는 경우 데이터를 복원하는 방식으로 문제를 해결해왔다.

이 문제 해결방식은 실제로 잘 동작하며 오랫동안 굳혀진 방법이다. 단순한 Input 값을 저장하고 복원하는 작업에서는 이러한 방식이 큰 문제가 없을 것이다. 다만, 비트맵과 같이 다소 큰 데이터를 직렬화하고 역직렬화 하기에는 부담스러운 부분이 있다. 대부분의 경우 이 문제를 해결하기 위해 로컬 저장소에 비트맵을 저장해두었다가 불러오는 방식을 고려하게 될 것이다.

뿐만 아니라 필자가 ViewModel이 해결하는 문제 중 가장 크다고 느끼는 점은, ViewModel에서만 실질적인 비즈니스 로직을 처리하게 함으로써 UI 컨트롤러에서 비즈니스 로직을 처리하면서 발생할 수 있는 작업 중복 수행과 성능 누수의 문제를 막을 수 있다는 점이다. 앞서 말했듯 UI 컨트롤러는 많은 경우에 빈번히 데이터를 초기화 하기 때문에 만약 UI 컨트롤러 생성시 특정 비동기 작업을 수행하거나, UI 컨트롤러 구동시 특정 반복 작업을 수행하고 있는 경우 이를 제어하기 위하여 개발자는 많은 시간을 들여야 한다.

ViewModelStore

ViewModelViewModelStore에 보관된다.

package androidx.lifecycle

interface ViewModelStoreOwner {
    val viewModelStore: ViewModelStore
}

ViewModelStoreOwnerViewModelStore를 가지도록 하는 interface 이다. 실제로 Component Activity와 Fragment는 이 ViewModelStoreOwner를 구현하고 있다.

https://developer.android.com/reference/androidx/lifecycle/ViewModelStoreOwner

그렇다면 ViewModelStore는 무엇인가?

package androidx.lifecycle

import androidx.annotation.RestrictTo

open class ViewModelStore {

    private val map = mutableMapOf<String, ViewModel>()

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    fun put(key: String, viewModel: ViewModel) {
        val oldViewModel = map.put(key, viewModel)
        oldViewModel?.onCleared()
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    operator fun get(key: String): ViewModel? {
        return map[key]
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    fun keys(): Set<String> {
        return HashSet(map.keys)
    }

    fun clear() {
        for (vm in map.values) {
            vm.clear()
        }
        map.clear()
    }
}

ViewModelStore는 실제로 Map에 ViewModel을 key : value 형태로 저장한다.

정리하자면 각자의 ComponentActivityFragmentViewModelStoreOwner를 구현하기 때문에 ViewModelStore를 가지게 되고, 그들이 가진 ViewModelStoreViewModel을 저장하고 가져오는 것이다.

실제로 우리가 사용하는 AppCompatActivityFragmentActivityComponentActivity에서 파생되는 클래스로, 최근 사용되는 대부분의 ActivityViewModelStore를 가진다.

ViewModelProvider

ViewModelViewModelStore에 저장하는 것은 알아보았는데, 실제로 우리는 해당 ViewModelStore에 접근하여 ViewModel을 생성, 저장하지는 않는다. 이를 도와주는 것이 ViewModelProvider이다.

다음은 가장 기본적으로 ViewModel을 로드하는 코드이다.

val viewModel = ViewModelProvider(this@MainActivity).get(MainViewModel::class.java)

ViewModelProvider를 생성하면서 MainActivity 자신을 넘기고 있다.

public constructor(
        owner: ViewModelStoreOwner
    ) : this(owner.viewModelStore, defaultFactory(owner), defaultCreationExtras(owner))

실제로 이들은 ViewModelStoreOwner를 매개변수로 하고있다. ViewModel 생성시 사용되는 FactoryCreationExtras를 같이 넘긴다. 액티비티가 따로 ViewModel 생성시 사용하는 Factory를 가지고 있다면 해당 FactoryViewModel을 생성한다.

실제로 해당 Factory는 생성자 매개변수를 동반하여 ViewModel을 생성해야 할 때 사용할 수 있다.

public class MyViewModelFactory implements ViewModelProvider.Factory {
    private Application mApplication;
    private String mParam;


    public MyViewModelFactory(Application application, String param) {
        mApplication = application;
        mParam = param;
    }


    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        return (T) new MyViewModel(mApplication, mParam);
    }
}
MyViewModel myViewModel = ViewModelProvider(this, new MyViewModelFactory(this.getApplication(), "my awesome param")).get(MyViewModel.class);

from : Android ViewModel additional arguments | stackoverflow.com

실제로 ViewModelProvider 의 get 함수를 호출할 때에는 ViewModelStore 에서 DEFAULT_KEY + canonicalName 형태의 key 로 기존에 생성된 ViewModel 이 있으면 해당 ViewModel 을 가져오고, ViewModel 이 없는 경우 전술한 Factory 를 이용하여 ViewModel 을 생성 및 저장한 뒤 ViewModel 을 반환한다.

@MainThread
public open operator fun <T : ViewModel> get(modelClass: Class<T>): T {
    val canonicalName = modelClass.canonicalName
        ?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
    return get("$DEFAULT_KEY:$canonicalName", modelClass)
}


@Suppress("UNCHECKED_CAST")
@MainThread
public open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {
    val viewModel = store[key]
    if (modelClass.isInstance(viewModel)) {
        (factory as? OnRequeryFactory)?.onRequery(viewModel!!)
        return viewModel as T
    } else {
        @Suppress("ControlFlowWithEmptyBody")
        if (viewModel != null) {
            // TODO: log a warning.
        }
    }
    val extras = MutableCreationExtras(defaultCreationExtras)
    extras[VIEW_MODEL_KEY] = key
    // AGP has some desugaring issues associated with compileOnly dependencies so we need to
    // fall back to the other create method to keep from crashing.
    return try {
        factory.create(modelClass, extras)
    } catch (e: AbstractMethodError) {
        factory.create(modelClass)
    }.also { store.put(key, it) }
}

Why?

본 글에서는 ViewModel을 실제로 어떻게 생성되며 저장하는 지에 대해서 다루었다. 우리는 ViewModelViewModelStore에 저장되며, ComponentActivityFragment 등이 ViewModelStoreOwner 인터페이스를 구현하고 있다는 것에 주목해야 한다.

이 과정을 이해함으로써 우리는 왜 Fragment 간에 부모 ActivityViewModel을 가져와서, 상호작용 할 수 있게 되는지 이해할 수 있게 된다. 반대로 어떠한 상황에서 서로 ViewModel을 공유할 수 없는지 예상할 수 있다.

특히 최근 Hilt 와 같은 의존성 주입 라이브러리에서도 ViewModel 이 제공되고, Jetpack Compose 에서 viewModel() 함수(androidx.lifecycle.viewmodel.compose.viewModel)를 이용해 로드 할 수 있다. 뿐만 아니라 ViewModel 생성시 대 부분 직접 ViewModelProvider를 생성하기 보다는 delegated property를 이용해서 생성하는 경우가 많다. ViewModel이 생성될 수 있는 방법이 다양한 만큼, ViewModel을 저장하는 원천을 파악하기 위해서는 ViewModelStore에 대한 기초적인 지식이 필요하다.

잘못된 뷰 모델을 여러번 생성하여 일어나는 성능 저하를 피하기 위해서는 각 함수가 어떤 UI 컨트롤러에 소속된 ViewModelStore로 부터 ViewModel을 가져오는지 파악할 필요가 있다.

오류 발견 시 알려주세요!

글에 오류를 발견하시면 댓글 혹은 이메일로 알려주시면 감사하겠습니다.
여러분의 도움으로 더 완벽한 내용을 제공하고자 합니다

About the author

Gyeongrok Kim

Add comment

Your sidebar area is currently empty. Hurry up and add some widgets.