[Android] 상태관리 #2 SavedState

16 분 소요

상태관리 #1에 이어서 상태관리 2번과정인 savedState Api 방법을 정리해보겠다.

  • SavedState api : process 종료에도 유지 but, Bundle을 사용하여 적고 단순한 형태의 데이터에 적합

savedState api는 SavedStateRegistry 를 이용하여 상태를 관리하는 방법으로 앱이 실행되는 프로세스가 아닌 외부의 메모리에 Bundle 타입으로 저장하기 때문에 프로세스 종료에도 유지되지만, Bundle자체가 직렬화를 통해 저장되기 때문에 적고 단순한 형태의 데이터에 적합하다.

ActivityComponent의 상태 관리

상태를 복원하는 과정은 Activity의 onCreate(savedInstanceState: Bundle) 에서 app 프로세스 외부 메모리에 저장된 Bundle 객체를 받아와서 SavedStateRegistry 내에 저장해놓는 흐름으로 수행된다.

ComponentActivity 클래스를 살펴보면,

SavedStateRegistryOwner & SavedStateRegistry

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        ContextAware,
        LifecycleOwner,
        ViewModelStoreOwner,
        HasDefaultViewModelProviderFactory,
        SavedStateRegistryOwner,
        ''' 생략 '''

이전챕터에서 ViewModelStoreOwner 와 관련된 ViewModel 인스턴스의 생성부터 소멸까지를 살펴봣었다. 이제 나머지 파트인 SavedStateRegistryOwner 에 대해서 알아볼 차례이다.

SavedStateRegistryOwner 는 SavedStateRegistry를 관리하는 책임을 갖는 인터페이스고 이를 구현한 ComponentActivity가 그역할을 수행한다.

interface SavedStateRegistryOwner : LifecycleOwner {
    val savedStateRegistry: SavedStateRegistry
}

public interface LifecycleOwner {
    @NonNull
    Lifecycle getLifecycle();
}

ViewModelStoreOwner와 달리 LifecycleOwner를 구현하고 있는데 이후에 설명하겠지만 lifecycle을 구현함으로써 SavedStateRegistryOwner의 lifecycle 변화에 따라 SavedState를 복원(onCreate)하고 저장(onDestroy)해야 하기 때문이다.

SavedStateRegistry가 lifecycle 에 따라 처리되야 하므로 LifecycleOwner를 ComponentActivity가 구현하여 내부에 lifecycle을 만들어 두어야 하며, 이두가지를 연결시켜주어야 한다.

그작업을 ComponentActivity 생성자에서 SavedStateRegistryController#performAttach() 로 수행하므로 이곳이 SavedStateRegistry 의 시작점이 된다.

초기화 및 연결

public ComponentActivity() {
	'''
	생략
	'''
	mSavedStateRegistryController.performAttach();
	enableSavedStateHandles(this);
	
	'''
	생략
	'''
}

CompoentActivity의 생성자가 호출되는 INITALIZED 시점에 위의 두 메소드를 통해 복원과정을 위한 초기화 및 연결작업을 수행하게 된다.

먼저, SavedStateRegistryController#performAttach 를 살펴보자면,

final SavedStateRegistryController mSavedStateRegistryController = SavedStateRegistryController.create(this);

mSavedStateRegistryController는 ComponentActivity 내에서 위의 형태로 지역변수로써 초기화 되고 있는데 SavedStateRegistry 객체는SavedStateRegistryController에 의해 관리 및 조작된다.

class SavedStateRegistryController private constructor(private val owner: SavedStateRegistryOwner) {

    val savedStateRegistry: SavedStateRegistry = SavedStateRegistry()

    private var attached = false

    @MainThread
    fun performAttach() {
        val lifecycle = owner.lifecycle
        check(lifecycle.currentState == Lifecycle.State.INITIALIZED) {
            ("Restarter must be created only during owner's initialization stage")
        }
        lifecycle.addObserver(Recreator(owner))
        savedStateRegistry.performAttach(lifecycle)
        attached = true
    }
		
    @MainThread
    fun performSave(outBundle: Bundle) {
        savedStateRegistry.performSave(outBundle)
    }

    companion object {
        @JvmStatic
        fun create(owner: SavedStateRegistryOwner): SavedStateRegistryController {
            return SavedStateRegistryController(owner)
        }
    }
}

ViewModelStore와는 달리 저장과 복원이라는 비즈니스 로직을 수행해야 하기 때문에 Controller 객체를 두어 SavedStateRegistry을 조작(restore & save) 하는 로직에 대한 캡슐화를 한 것으로 보여진다.

componentActivity 에서 SavedStateRegistryController#performAttach() 를 수행했을 때 Recreator 인스턴스를 만들어 SavedStateRegistryOwner의 lifecycle 변화에 따라 콜백처리하고, savedStateRegisry#performAttach()를 수행하고 있고, 해당 메소드는 단순히 SavedStateRegistry를 attached 상태로 만들어두고 있다.

또한 해당 챕터 뒷부분에서 살펴볼 Recreator 인스턴스는 요약하자면 configuration change 또는 그에 상응하는 상황에 의해 ComponentActivity가 재생성되었을 때 가지고있는 ViewModelStore의 SavedStateViewModel들을 attached 상태로 만들고, 상태 저장 대상인 SavedStateProvider를 등록하는 과정을 수행한다.

이 과정이 왜 필요하냐면, ViewModelStore는 configuration change에도 소멸하지 않고, 그에따라 ViewModel들도 소멸하지 않지만 SavedStateRegistry의 lifecycle을 구독하면서 attached가 false가 되게 된다. 또한 ComponentActivity에 종속적인 SavedStateRegistry는 재생성되면서 내부의 restoredState와 savedStateProviders 를 잃게 되므로 onCreate 시점에 복원과정을 거치게 된다.

즉, SavedStateViewModel은 어차피 저장했던 상태를 그대로 들고 있어 복원이 필요없게 되지만 SavedStateRegistry는 자신의 상태를 사용하는 SavedStateViewModel의 저장방식을 잃어버리게 되므로 해당 과정을 통해서 SavedStateProvider를 등록하는 과정을 필요로 하게 되는것이다. 따라서 위 과정을 AutoRecreated 인터페이스와 Recreator 에 위임하여 수행하게 된다.

이부분은 legacy 코드들을 호환하기 위한 방법으로 최근에는 추가된 CreationExtras를 이용하여 SavedStateHandleSupport 패키지의 코드들을 사용하는 방법으로 SavedStateViewModel을 생성하고 있다.

다음으로 SavedStateHandleSupport 패키지에 있는 enableSavedStateHandles를 살펴보면,

SavedStateHandleSupport

@MainThread
fun <T> T.enableSavedStateHandles()
    where T : SavedStateRegistryOwner, T : ViewModelStoreOwner {
    val currentState = lifecycle.currentState
    require(
        currentState == Lifecycle.State.INITIALIZED || currentState == Lifecycle.State.CREATED
    )

    if (savedStateRegistry.getSavedStateProvider(SAVED_STATE_KEY) == null) {
        val provider = SavedStateHandlesProvider(savedStateRegistry, this)
        savedStateRegistry.registerSavedStateProvider(SAVED_STATE_KEY, provider)
        lifecycle.addObserver(SavedStateHandleAttacher(provider))
    }
}

internal class SavedStateHandleAttacher(
    private val provider: SavedStateHandlesProvider
) : LifecycleEventObserver {

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        check(event == Lifecycle.Event.ON_CREATE) {
            "Next event must be ON_CREATE, it was $event"
        }
        source.lifecycle.removeObserver(this)
        provider.performRestore()
    }
}

internal class SavedStateHandlesProvider(
    private val savedStateRegistry: SavedStateRegistry,
    viewModelStoreOwner: ViewModelStoreOwner
) : SavedStateRegistry.SavedStateProvider {
    private var restored = false
    private var restoredState: Bundle? = null

    private val viewModel by lazy {
        viewModelStoreOwner.savedStateHandlesVM
    }

    override fun saveState(): Bundle {
        return Bundle().apply {
            if (restoredState != null) {
                putAll(restoredState)
            }
            viewModel.handles.forEach { (key, handle) ->
                val savedState = handle.savedStateProvider().saveState()
                if (savedState != Bundle.EMPTY) {
                    putBundle(key, savedState)
                }
            }
        }.also {
            restored = false
        }
    }

    fun performRestore() {
        if (!restored) {
            restoredState = savedStateRegistry.consumeRestoredStateForKey(SAVED_STATE_KEY)
            restored = true
            viewModel
        }
    }

    fun consumeRestoredStateForKey(key: String): Bundle? {
        performRestore()
        return restoredState?.getBundle(key).also {
            restoredState?.remove(key)
            if (restoredState?.isEmpty == true) {
                restoredState = null
            }
        }
    }
}

SAVED_STATE_KEY 를 통해 SavedStateRegistry에서 savedStateProvider를 가져오는데, 당연히 componentActivity가 가장 처음 생성된 이후라면 이값이 null 이되고, SavedStateProvider를 생성하여 등록해준뒤, LifecycleEventObserver 구현체인 SavedStateHandleAttacher 를 등록하여 onCreate 일때 SavedStateProvider 내의 performRestore()를 수행하게 된다.

위 과정은 이후 ViewModel 인스턴스 생성 시점에 사용하기 위한 초기화의 역할을 수행하게 되며, 이후 아래에서 연계되는 코드들과 설명하겠다.

생성자에서 attach하는 과정이 수행되고 나서는 activity의 lifecycle에 따라 onCreate()에서 복원작업이 수행된다.

상태 복원

componentActivity의 onCreate()에서 savedStateRegistryController의 performRestore()를 호출하게 되고,

@MainThread
fun performRestore(savedState: Bundle?) {
    if (!attached) {
        performAttach()
    }
    val lifecycle = owner.lifecycle
    check(!lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
        ("performRestore cannot be called when owner is ${lifecycle.currentState}")
    }
    savedStateRegistry.performRestore(savedState)
}

복원작업은 CREATED 상태 에서 수행되어야 하기 때문에 attached 상태인지 체크한뒤, SavedStateRegistryOwner#lifecycle이 CREATED 상태를 넘어섯는지 확인한다. CREATED가 맞다면 SavedStateRegistry#performRestore() 를 수행한다.

@MainThread
internal fun performRestore(savedState: Bundle?) {
    check(attached) {
        ("You must call performAttach() before calling " +
            "performRestore(Bundle).")
    }
    check(!isRestored) { "SavedStateRegistry was already restored." }
    restoredState = savedState?.getBundle(SAVED_COMPONENTS_KEY)

    isRestored = true
}

attached 상태이면서 저장된 상태가 없다면, savedState Bundle로부터 SAVED_COMPONENTS_KEY로 Bundle타입으로 복원한뒤 내부에 resotredState 프로퍼티에 캐싱해둔다.

상태 저장

상태 저장은 onDestroy() 직전에 onSaveInstanceState() 콜백에서 수행되며 전반적인 절차는 복원 과정보다 간단하며(빠르게 저장하고 종료되기 때문) SavedStateRegistry 내에 캐싱된 restoredState를 외부메모리로 내보내는 흐름으로 수행된다.

@CallSuper
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
    Lifecycle lifecycle = getLifecycle();
    if (lifecycle instanceof LifecycleRegistry) {
        ((LifecycleRegistry) lifecycle).setCurrentState(Lifecycle.State.CREATED);
    }
    super.onSaveInstanceState(outState);
    mSavedStateRegistryController.performSave(outState);
}

ComponentActivity의 onSaveInstanceState내에서 SavedStateRegistryController의 performSave()에 외부에 저장할 Bundle을 파라미터로 보내고

fun performSave(outBundle: Bundle) {
    val components = Bundle()
    if (restoredState != null) {
        components.putAll(restoredState)
    }
    val it: Iterator<Map.Entry<String, SavedStateProvider>> =
        this.components.iteratorWithAdditions()
    while (it.hasNext()) {
        val (key, value) = it.next()
        components.putBundle(key, value.saveState())
    }
    if (!components.isEmpty) {
        outBundle.putBundle(SAVED_COMPONENTS_KEY, components)
    }
}

fun interface SavedStateProvider {
					fun saveState(): Bundle
}

SavedStateRegistry 내에 캐싱된 restoredState를 새로운 Bundle 객체에 담은뒤 SavedStateProvider 타입의 객체의 saveState()를 호출하여 Bundle 객체를 저장한뒤, 결과적으로 파라미터로 받은 Bundle에 SAVED_COMPONENTS_KEY로 저장하고 있다.

SavedStateProvider는 SavedStateRegistry 클래스에 선언된 상태를 저장하는 행위에 대한 인터페이스로 요약하자면 SavedStateRegistry 의 복원되어 캐싱된 상태를 소비 및 기여하는 다른 컴포넌트들이 사용하고 나서 SavedStateRegistry 의 저장과정에 엎어쓰기 하는 로직을 추상화한 것이다.

SavedStateRegistry 내부에서 상태를 복원하고 캐싱해두면, 이 상태들을 어디선가는 사용하게되고(SavedStateViewModel 등) 그렇게 사용되고 변화되어진 상태를 다시 저장해야 하는데 사용처 컴포넌트들을 SavedStateRegistry는 모르기 때문에 그것들을 등록해둔다고 생각하면 된다.

또한 restoredState를 가져간 뒤에 변화된 상태들이 최신의 상태값이 되므로 restoredState를 먼저 담은뒤 SavedStateProvider의 해당 Key로 엎어쓰기 하는 순서로 수행된다.

SavedStateProvider는 이후 뒷부분에더 더다루기로 하고, 여기까지만 봤을 때 구조적인 측면에서 정리해보면 좋을것 같다.

  1. SavedStateRegistry는 Model 로써 데이터를 캐싱해두고, 데이터를 조작하는 비즈니스 로직 수행을 담당하고 있다.
  2. SavedStateRegistryController 는 SavedStateRegistry를 가지며, Model을 제어하여 비즈니스 로직을 캡슐화 하고 있다.
  3. 캡슐화된 로직을 바탕으로 Model의 변경이 View에 영향을 주지않고 Controller 까지만 변경된다.

핵심은 Model 과 View는 서로 모르고 영향을 주지 않는다는 것이고 여기서 View를 굳이 따지자면 ComponentActivity가 될것이다. 이를 통해 MVC 디자인패턴이 구현하고자 했던 목적은 빈번한 UI와 비즈니스로직의 변경이 서로에 영향을 주어 유지보수가 어려워진다는 것을 해결하고자 한것이다.

하지만 Model의 변경은 Controller의 수정을 야기하기 때문에 OCP(개방 폐쇄 원칙)을 위배하는 것은 명확한 사실이다. 또한 View에 공개할 비즈니스로직이 많아질수록 Controller가 비대해 질 수밖에 없다. 어디까지나 관심사를 분리하여 변경의 영향을 최소화 하기 위했던 목적이니 만큼(없앤다는게 아니라..) 그래서 MVP와 MVVM이 등장한것으로 보인다.

여기까지가 ComponentActivity에서 일어나는 SavedStateRegistry 를 이용한 상태 관리 과정이었다. 이제 우리는 상태관리#1에서 보았던 2번과정을 SavedStateHandle을 이용한 SavedStateViewModel을 살펴볼 차례이다.

SavedState 의 상태 관리

상태관리#1 에서 ComponentActivity는 HasDefaultViewModelProviderFactory 인터페이스를 구현하여 ViewModelProvider.Factory의 기본값(default)으로써 SavedStateViewModelFactory 인스턴스를 반환하는 것을 보았다.

SavedStateViewModelFactory

@NonNull
@Override
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
    if (mDefaultFactory == null) {
        mDefaultFactory = new SavedStateViewModelFactory(
                getApplication(),
                this,
                getIntent() != null ? getIntent().getExtras() : null);
    }
    return mDefaultFactory;
}

이 인스턴스를 ViewModelProvider클래스의 생성자로 주입하고, ViewModelProvider#get() 메소드를 호출함으로써 내부에서 Factory#create() 를 호출하여 ViewModel 인스턴스를 생성한뒤 ViewModelStore 내에 캐싱해두고 가져올수 있었다.

그렇다면 구현체인 SavedStateViewModelFactory#create()를 살펴볼 필요가 있다.

override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
    val key = extras[ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY]
        ?: throw IllegalStateException(
            "VIEW_MODEL_KEY must always be provided by ViewModelProvider"
        )

    return if (extras[SAVED_STATE_REGISTRY_OWNER_KEY] != null &&
        extras[VIEW_MODEL_STORE_OWNER_KEY] != null) {
        val application = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]
        val isAndroidViewModel = AndroidViewModel::class.java.isAssignableFrom(modelClass)
        val constructor: Constructor<T>? = if (isAndroidViewModel && application != null) {
            findMatchingConstructor(modelClass, ANDROID_VIEWMODEL_SIGNATURE)
        } else {
            findMatchingConstructor(modelClass, VIEWMODEL_SIGNATURE)
        }

        if (constructor == null) {
            return factory.create(modelClass, extras)
        }
        val viewModel = if (isAndroidViewModel && application != null) {
            newInstance(modelClass, constructor, application, extras.createSavedStateHandle())
        } else {
            newInstance(modelClass, constructor, extras.createSavedStateHandle())
        }
        viewModel
   } else {
       val viewModel = if (lifecycle != null) {
           create(key, modelClass)
        } else {
            throw IllegalStateException("SAVED_STATE_REGISTRY_OWNER_KEY and" +
                "VIEW_MODEL_STORE_OWNER_KEY must be provided in the creation extras to" +
                "successfully create a ViewModel.")
        }
        viewModel
    }
}

override 된 Factory 인터페이스의 create 메소드 구현체인데,

요약해보자면 만약 SAVED_STATE_REGISTRY_OWNER_KEY 나 VIEW_MODEL_STORE_OWNER_KEY 에 맞는 extras에 value가 있다면, AndroidViewModel 을 상속한 클래스 인지 아닌지에 따라 맞는 생성자와 함께 CreationExtras#createSavedStateHandle() 확장리시버함수로 SavedStateHandle 객체를 생성하여 ViewModel 인스턴스를 reflection 으로 생성한다.

해당 과정이 최근의 code이고 아래부분은 legacy 를 호환하기 위한 코드들인데,

value가 없다면 생성자 파라미터로 받았던 savedStateRegistryOwner의 lifecycle을 체크하여,

fun <T : ViewModel> create(key: String, modelClass: Class<T>): T {
    if (lifecycle == null) {
        throw UnsupportedOperationException(
            "SavedStateViewModelFactory constructed with empty constructor supports only " +
                "calls to create(modelClass: Class<T>, extras: CreationExtras)."
        )
    }
    val isAndroidViewModel = AndroidViewModel::class.java.isAssignableFrom(modelClass)
    val constructor: Constructor<T>? = if (isAndroidViewModel && application != null) {
        findMatchingConstructor(modelClass, ANDROID_VIEWMODEL_SIGNATURE)
    } else {
        findMatchingConstructor(modelClass, VIEWMODEL_SIGNATURE)
    }

    if (constructor == null) {
        return if (application != null) factory.create(modelClass)
            else instance.create(modelClass)
    }
    val controller = LegacySavedStateHandleController.create(
        savedStateRegistry, lifecycle, key, defaultArgs
    )
    val viewModel: T = if (isAndroidViewModel && application != null) {
        newInstance(modelClass, constructor, application!!, controller.handle)
    } else {
        newInstance(modelClass, constructor, controller.handle)
    }
    viewModel.setTagIfAbsent(
        AbstractSavedStateViewModelFactory.TAG_SAVED_STATE_HANDLE_CONTROLLER, controller
    )
    return viewModel
}

lifecycle이 null이 아니라면 lifecycle의 출처인 ComponentActivity의 ViewModelStore와 SavedStateRegistry를 이용하여 LegacySavedStateHandleController#create()로 SavedStateHandleController 인스턴스를 생성한뒤 controller가 가지고있는 SavedStateHandle 인스턴스를 가지고 ViewModelProvider.Factory#create 의 구현체와 같은 과정을 통해 생성자를 찾아서 ViewModel 인스턴스를 reflection 으로 생성한다.

그리고 눈여겨봐야 할점은 viewModel 에 setTagIfAbsent() 메소드를 통해 SavedStateHandleController 인스턴스를 등록하고 있는 점인데, 이건 뒷부분에서 설명하겠지만 상태관리#1에서 잠시 설명했던 configuration change 이후 ViewModelStore에 존재했던 ViewModel인스턴스라면 onRequry()를 호출시킨뒤 SavedStateViewModelFactory#onRequery() 에서 LegacySavedStateHandleController#attachHandleIfNeeded() 메소드를 호출하여 SavedStateHandle의 savedStateProvider를 등록함으로써 N(>=2)번째 복원후 저장 로직을 등록하게 된다. 해당과정은 Legacy 이름이 붙어 있으므로 legacy 코드들을 호환하기 위한 코드로 보이며, 현재는 위에서 보았던 CreationExtras로 SavedStateHandleSupport 패키지의 방법을 사용하고 있다.

결국 이두가지 create() 메소드의 공통점은 복원된 상태를 이용하여 SavedStateHandle 인스턴스를 생성하면서 SavedStateRegistry에 저장 대상을 추가하고, ViewModel 의 생성자로 넘김으로써 인스턴스를 생성한다.

다음으로 SavedStateHandle 을 생성하는 LegacySavedStateHandleController#create 로직을 이어가서 SavedStateHandleController를 살펴보자.

SavedStateHandleController

internal object LegacySavedStateHandleController {
    const val TAG_SAVED_STATE_HANDLE_CONTROLLER = "androidx.lifecycle.savedstate.vm.tag"

    @JvmStatic
    fun create(
        registry: SavedStateRegistry,
        lifecycle: Lifecycle,
        key: String?,
        defaultArgs: Bundle?
    ): SavedStateHandleController {
        val restoredState = registry.consumeRestoredStateForKey(key!!)
        val handle = createHandle(restoredState, defaultArgs)
        val controller = SavedStateHandleController(key, handle)
        controller.attachToLifecycle(registry, lifecycle)
        tryToAddRecreator(registry, lifecycle)
        return controller
    }

    @JvmStatic
    fun attachHandleIfNeeded(
        viewModel: ViewModel,
        registry: SavedStateRegistry,
        lifecycle: Lifecycle
    ) {
        val controller = viewModel.getTag<SavedStateHandleController>(
            TAG_SAVED_STATE_HANDLE_CONTROLLER
        )
        if (controller != null && !controller.isAttached) {
            controller.attachToLifecycle(registry, lifecycle)
            tryToAddRecreator(registry, lifecycle)
        }
    }

    private fun tryToAddRecreator(registry: SavedStateRegistry, lifecycle: Lifecycle) {
        val currentState = lifecycle.currentState
        if (currentState === Lifecycle.State.INITIALIZED ||
            currentState.isAtLeast(Lifecycle.State.STARTED)) {
            registry.runOnNextRecreation(OnRecreation::class.java)
        } else {
            lifecycle.addObserver(object : LifecycleEventObserver {
                override fun onStateChanged(
                    source: LifecycleOwner,
                    event: Lifecycle.Event
                ) {
                    if (event === Lifecycle.Event.ON_START) {
                        lifecycle.removeObserver(this)
                        registry.runOnNextRecreation(OnRecreation::class.java)
                    }
                }
            })
        }
    }

    internal class OnRecreation : SavedStateRegistry.AutoRecreated {
        override fun onRecreated(owner: SavedStateRegistryOwner) {
            check(owner is ViewModelStoreOwner) {
                ("Internal error: OnRecreation should be registered only on components " +
                    "that implement ViewModelStoreOwner")
            }
            val viewModelStore = (owner as ViewModelStoreOwner).viewModelStore
            val savedStateRegistry = owner.savedStateRegistry
            for (key in viewModelStore.keys()) {
                val viewModel = viewModelStore[key]
                attachHandleIfNeeded(viewModel!!, savedStateRegistry, owner.lifecycle)
            }
            if (viewModelStore.keys().isNotEmpty()) {
                savedStateRegistry.runOnNextRecreation(OnRecreation::class.java)
            }
        }
    }
}

internal class SavedStateHandleController(
    private val key: String,
    val handle: SavedStateHandle
) : LifecycleEventObserver {

    var isAttached = false
        private set

    fun attachToLifecycle(registry: SavedStateRegistry, lifecycle: Lifecycle) {
        check(!isAttached) { "Already attached to lifecycleOwner" }
        isAttached = true
        lifecycle.addObserver(this)
        registry.registerSavedStateProvider(key, handle.savedStateProvider())
    }

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (event === Lifecycle.Event.ON_DESTROY) {
            isAttached = false
            source.lifecycle.removeObserver(this)
        }
    }
}

LegacySavedStateHandleController#create 에서 저장된 상태를 consumeRestoredStateForKey 로 가져온뒤 SavedStateHandle#create 로 인스턴스를 생성한 뒤, SavedStateHandleController#attachToLifecycle 로 SavedStateRegistry에 저장 대상을 추가하고

LegacySavedStateHandleController#tryToAddRecreator 를 호출하여 OnRecreation 클래스이름을 SavedStateRegistry#recreatorProvider 에 추가하게 된다. 추후 configuration change 이후 상태 복원과정에서 reflection을 통해 OnRecreation의 onRecreated를 호출함으로써 위에서 설명한대로 SavedStateViewModel 의 configuration change 이후 상태 저장 대상을 등록할수 있게 된다.

구조적인 측면에서 attached 값을 SavedStateRegistryController, SavedStateRegistry, SavedStateHandleController 등에서 따로 가지고 있는데,

SavedStateRegistryController 나 SavedStateRegistry는 ComponentActivity의 view lifecycle이 INITALIZED 일 때라서 ComponentActivity 생성 시점에 인스턴스가 각각 생성되어 lifecycle을 따라간다는 의미가 되고, SavedStateHandleController는 상태 복원 과 ViewModel 인스턴스 생성 이후 이므로 CREATED 일 때가 되서 SavedStateRegistry의 복원된 상태를 사용하기 시작함을 의미한다.

각각이 attach 되어진다는 의미가 다르고, 시점도 다르기 때문에 단순히 lifecycle의 State나 Event값으로 체크하지 않고 하나의 프로퍼티값으로 비교하고 있음을 볼수있다. 또한 가독성에서의 장점도 있다.

LegacySavedStateHandleController#tryToAddRecreator 내에서 SavedStateRegistry#runOnNextRecreation 호출부분을 자세히 살펴보면,

@MainThread
fun runOnNextRecreation(clazz: Class<out AutoRecreated>) {
    check(isAllowingSavingState) { "Can not perform this action after onSaveInstanceState" }
    recreatorProvider = recreatorProvider ?: Recreator.SavedStateProvider(this)
    try {
        clazz.getDeclaredConstructor()
    } catch (e: NoSuchMethodException) {
        throw IllegalArgumentException(
            "Class ${clazz.simpleName} must have " +
                "default constructor in order to be automatically recreated", e
        )
    }
    recreatorProvider?.add(clazz.name)
}

SavedStateRegistry 내부의 코드인데, 여기서 내부 프로퍼티인 recreatorProvider는 SavedStateProvider의 구현체로써 상태 저장 대상이며 여기서 초기화 후 인스턴스 생성이 가능한지 생성자를 호출하여 검증하고 난뒤, Recreator#add() 로 전달받은 OnRecreation 클래스 이름을 추가하고 있다.

internal class Recreator(
    private val owner: SavedStateRegistryOwner
) : LifecycleEventObserver {

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (event != Lifecycle.Event.ON_CREATE) {
            throw AssertionError("Next event must be ON_CREATE")
        }
        source.lifecycle.removeObserver(this)
        val bundle: Bundle = owner.savedStateRegistry
            .consumeRestoredStateForKey(COMPONENT_KEY) ?: return
        val classes: MutableList<String> = bundle.getStringArrayList(CLASSES_KEY)
            ?: throw IllegalStateException(
                "Bundle with restored state for the component " +
                    "\"$COMPONENT_KEY\" must contain list of strings by the key " +
                    "\"$CLASSES_KEY\""
            )
        for (className: String in classes) {
            reflectiveNew(className)
        }
    }

    private fun reflectiveNew(className: String) {
        val clazz: Class<out AutoRecreated> =
            try {
                Class.forName(className, false, Recreator::class.java.classLoader)
                    .asSubclass(AutoRecreated::class.java)
            } catch (e: ClassNotFoundException) {
                throw RuntimeException("Class $className wasn't found", e)
            }
        val constructor =
            try {
                clazz.getDeclaredConstructor()
            } catch (e: NoSuchMethodException) {
                throw IllegalStateException(
                    "Class ${clazz.simpleName} must have " +
                        "default constructor in order to be automatically recreated", e
                )
            }
        constructor.isAccessible = true
        val newInstance: AutoRecreated =
            try {
                constructor.newInstance()
            } catch (e: Exception) {
                throw RuntimeException("Failed to instantiate $className", e)
            }
        newInstance.onRecreated(owner)
    }

    internal class SavedStateProvider(registry: SavedStateRegistry) :
        SavedStateRegistry.SavedStateProvider {

        private val classes: MutableSet<String> = mutableSetOf()

        init {
            registry.registerSavedStateProvider(COMPONENT_KEY, this)
        }

        override fun saveState(): Bundle {
            return Bundle().apply {
                putStringArrayList(CLASSES_KEY, ArrayList(classes))
            }
        }

        fun add(className: String) {
            classes.add(className)
        }
    }

    companion object {
        const val CLASSES_KEY = "classes_to_restore"
        const val COMPONENT_KEY = "androidx.savedstate.Restarter"
    }
}

Recreator 클래스는 LifecycleEventObserver를 구현하고 있어서 SavedStateRegistryOwner의 lifecycle이 ON_CREATE이면 savedStateRegistry의 상태중에 내부에 상태 저장 방법인 SavedStateProvider 클래스로 저장했던 클래스이름들을 가져와서 reflection을 수행 한뒤 onRecreated 를 호출한다.

이것의 구현체는 SavedStateHandleController#OnRecreation 이고, 내부에서는 다시 SavedStateHandleController#attachToLifecycle 를 호출하여 SavedStateHandle의 상태 저장 대상을 추가하고, 다음 configuration change 에 대비하기 위해 SavedStateRegistry#runOnNextRecreation()을 호출함으로써 다시 Recreator를 SavedStateRegistry에 등록한다.

즉, 이부분에서 앞서 언급했던 configuration change 이후 ViewModel은 상태를 그대로 가지고있지만 SavedStateRegistry에서는 상태 저장 방법을 잃어버리기 때문에 ON_CREATE에서 복원된 OnRecreation 클래스 이름을 가져와서 reflection을 한뒤 onRecreated 호출하게 되고 이로써 다시 ViewModel의 상태 저장 방법을 SavedStateRegistry#savedStateProviders 에 캐싱해두게 된다.

마지막으로 SavedStateHandle을 살펴볼 차례이다.

SavedStateHandle

일반적으로 defaultViewModelFactory를 사용하여 viewModel 인스턴스를 생성하는 by viewModels() 프로퍼티 델리게이트로 만들어진다면, 우리는 ViewModel 클래스의 생성자로 SavedStateHandle을 파라미터로 받을 수 있다.

class SampleViewModel(savedStateHandle: SavedStateHandle): ViewModel() { ''' }

ComponentActivity#onCreate 에서 상태를 복원하여 SavedStateRegistry에 저장된 storedState를 ViewModel 인스턴스 생성시에 SavedStateHandle 객체를 생성하면서 가져와서 상태복원과 저장에 기여할 수 있다.

class SavedStateHandle {
    private val regular = mutableMapOf<String, Any?>()
    private val savedStateProviders = mutableMapOf<String, SavedStateRegistry.SavedStateProvider>()
    private val liveDatas = mutableMapOf<String, SavingStateLiveData<*>>()
    private val flows = mutableMapOf<String, MutableStateFlow<Any?>>()
    private val savedStateProvider =
        SavedStateRegistry.SavedStateProvider {
            val map = savedStateProviders.toMap()
            for ((key, value) in map) {
                val savedState = value.saveState()
                set(key, savedState)
            }

            val keySet: Set<String> = regular.keys
            val keys: ArrayList<String> = ArrayList(keySet.size)
            val value: ArrayList<Any?> = ArrayList(keys.size)
            for (key in keySet) {
                keys.add(key)
                value.add(regular[key])
            }
            bundleOf(KEYS to keys, VALUES to value)
        }
				
		constructor(initialState: Map<String, Any?>) {
        regular.putAll(initialState)
    }
				
    companion object {
        private const val VALUES = "values"
        private const val KEYS = "keys"
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        @JvmStatic
        @Suppress("DEPRECATION")
        fun createHandle(restoredState: Bundle?, defaultState: Bundle?): SavedStateHandle {
            if (restoredState == null) {
                return if (defaultState == null) {
                    SavedStateHandle()
                } else {
                    val state: MutableMap<String, Any?> = HashMap()
                    for (key in defaultState.keySet()) {
                        state[key] = defaultState[key]
                    }
                    SavedStateHandle(state)
                }
            }

            val keys: ArrayList<*>? = restoredState.getParcelableArrayList<Parcelable>(KEYS)
            val values: ArrayList<*>? = restoredState.getParcelableArrayList<Parcelable>(VALUES)
            check(!(keys == null || values == null || keys.size != values.size)) {
                "Invalid bundle passed as restored state"
            }
            val state = mutableMapOf<String, Any?>()
            for (i in keys.indices) {
                state[keys[i] as String] = values[i]
            }
            return SavedStateHandle(state)
        }

SavedStateHandle 클래스의 구현체 모습이다. regular 변수가 Map 자료구조로 SavedStateRegistry#storedState 를 캐싱해두고, liveDatas 와 flows 로 storedState를 각각 MutableLiveData를 상속하여 커스텀한 SavingStateLiveData, MutableStateFlow로 저장해두고 있다.

그리고 상태관리#1 ViewModel 생성부분 마지막에 언급했던 SavedStateRegistry.SavedStateProvider 구현체를 가지고 있어 내부에 캐싱해둔 regular의 keys 와 values 를 bundle에 담아 반환하여 SavedStateRegistry 에서 저장작업에 사용되도록 하고 있으며, savedStateProviders 라는 프로퍼티로 SavedStateProvider 타입 구현체를 Map 자료구조로 저장하여 SavedStateHandle 객체의 regular 값을 소비 및 기여하는 컴포넌트가 상태값을 사용하고 저장할수 있도록 해두고 있는 모습이다.

SavedStateHandle 클래스를 보면서 단순히 SavedStateRegistry의 상태들을 값으로써 노출할 뿐만아니라 편의성 측면에서 LiveData 타입으로 반환한다는 점을 알수있었고, 최근에는 비동기 프레임워크로 안드로이드 의존성이 있는 android.lifecycle 의 livedata 를 사용하지 않고 Kotlinx.coroutines 의 flow를 많이 사용하고 있어 StateFlow로도 캐싱 및 사용할수있게 만들어 두기도 한것 같다. 이런것들이 AAC-ViewModel의 정체성과 같은 느낌으로 보인다. 좀더 상태 관리와 사용에 있어서 편리하게 만들기 위한 클래스의 설계가 보여졌다.

하단부에는 SavedStateHandle의 인스턴스를 생성로직인 SavedStateHandle#create() 메소드인데, 파라미터로 restoredstate와 defaultState를 받고 있다.

restoredState는 복원된 데이터고 defaultState는 SavedStateViewModelFactory#create에서 파라미터로 전달된 CreationExtras 로 넘겨진 ComponentActivity의 intent.extras 또는 Fragment의 전환에 전달되는 Bundle Argument 값이다.

잘보면, defaultState를 먼저 Map 자료구조에 집어넣고 그후 restoredState를 집어넣는다. 즉, restoredState에 없는 상태라면 쓰고 있는 상태값이면 엎어쓰기한다는 것을 의미한다.

또한 이값을 최종적으로 SavedStateHandle 생성자로 집어넣어 regular값으로 초기화한다.

SavedStateHandle은 SavedStateHandleController에 의해 생성되며 캡슐화 되어진다.

이제 ComponentActivity 에서 상태를 복원한뒤 SavedStateViewModel 에서 복원된 상태를 어떻게 가져오고 사용하는지, 그리고 저장까지의 흐름을 살펴보자.

SavedState 의 상태 복원 과정

앞서 보았던 과정속에서 CreationExtras를 이용한 방법과 그렇지않은 legacy한 방법과 CompoentActivity 최초생성 과 configuration change 이후의 흐름으로 살펴보았다. 따라서 복원 과정도 각각의 흐름으로 살펴볼 필요가 있다.

가장먼저 CreationExtras를 사용한 방법으로 보자면,

최초

  1. ComponentActivity#enableSavedStateHandles()
  2. SavedStateHandlesProvider 인스턴스 생성후 SavedStateRegistry에 상태 저장 대상 추가
  3. SavedStateHandleAttacher를 lifecycle에 구독시켜 onCreate 시점에 상태 복원 수행
  4. ViewModelProvider#get()
  5. SavedStateViewModelFactory#create()
  6. 파라미터로 들어온 CreationExtras#get() 으로 SavedstateRegistryOwner 와 ViewModelStoreOwner가 존재함
  7. CreationExtras#createSavedStateHandle()
  8. SavedStateHandleSupport#createSavedStateHandle() 델리게이트
  9. savedStateRegistryOwner#savedStateHandlesProvider 확장 프로퍼티로 SavedStateProvider(상태 저장 대상) 생성 및 SavedStateRegistry#consumeRestoredStateForKey로 상태 복원
  10. 복원된 상태를 가지고 SavedStateHandle#create() 로 SavedStateHandle 인스턴스 생성

configuration change 이후

  1. ComponentActivity#enableSavedStateHandles()
  2. SavedStateHandlesProvider 인스턴스 생성후 SavedStateRegistry에 상태 저장 대상 추가
  3. SavedStateHandleAttacher를 lifecycle에 구독시켜 onCreate 시점에 저장된 상태값 가져와 캐싱
  4. ViewModelProvider#get()
  5. ViewModel 인스턴스 가져옴

다음으로, CreationExtras를 사용하지 않는 방법으로 보자면,

최초

  1. ViewModelProvider#get()
  2. SavedStateViewModelFactory#create()
  3. 파라미터로 들어온 CreationExtras#get() 으로 SavedstateRegistryOwner 와 ViewModelStoreOwner가 없음
  4. LegacySavedStateHandleController#create 로 SavedStateHandleController 인스턴스 생성후
  5. SavedStateRegistry#consumeRestoredStateForKey 로 저장된 상태값 가져와 SavedStateHandle 인스턴스 생성 및 초기화
  6. SavedStateHandleController#attachToLifecycle() 로 LifecycleEventObserver 등록(attached = false 트리거)
  7. SavedStateRegistry#registerSavedStateProvider 로 상태 저장 대상 추가
  8. SavedStateHandleController#tryToAddRecreator()
  9. SavedStateRegistry#runOnNextRecreation()로 LegacySavedStateHandleController#OnRecreation 클래스이름 전달
  10. SavedStateRegistry#recreatorProvider에 클래스 이름을 add() 하여 등록함으로써 Recreator#SavedStateProvider를 SavedStateRegistry에 등록하여 상태 저장 대상 추가
  11. SavedStateHandle 을 주입하여 생성자 파라미터로 가진 ViewModel 인스턴스 생성

configuration change 이후

  1. ViewModelProvider#get()
  2. SavedViewModelFactory#onRequery() 호출
  3. lifecycle이 null이 아니므로 LegacySavedStateHandleController#attachHandleIfNeeded() 호출
  4. ViewModelStore의 ViewModel에서 SavedStateHandleController를 꺼냄
  5. SavedStateHandleController#attachToLifecycle() 로 LifecycleEventObserver 등록(attached = false 트리거)
  6. SavedStateRegistry#registerSavedStateProvider 로 상태 저장 대상 추가
  7. SavedStateHandleController#tryToAddRecreator()
  8. SavedStateRegistry#runOnNextRecreation()로 LegacySavedStateHandleController#OnRecreation 클래스이름 전달
  9. SavedStateRegistry#recreatorProvider에 클래스 이름을 add() 하여 등록함으로써 Recreator#SavedStateProvider를 SavedStateRegistry에 등록하여 상태 저장 대상 추가
  10. ViewModel 인스턴스 가져옴

CreationExtras를 사용한 이유는 상태관리#1 에서 보앗듯이, 생성 시점에 필요한 파라미터들을 모두 알수 없어서 였다. 하지만, 이것을 통해 개선한 형태는 결국 OnRecreation 클래스로 상태 저장 대상을 추가하지 않게되고 그로인해 발생하는 reflection 과정을 없애버렸다.

자바의 reflection 과정은 클래스를 찾아가는 과정이 수행되어 그자체로 성능 악화의 오버헤드가 발생하게 되어 이과정을 없앰으로써 좀더 빠른 상태 복원 과정을 수행할 뿐만아니라 configuration change이후 과정에서도 볼수있듯이 상태 복원을 위한 참조 깊이가 적어지게 되어 더빠른 상태복원 과정이 수행될수 있음을 짐작할수 있다.

상태 저장과정은 위에서 보았던 ComponentActivity.performSave() 가 결국 SavedStateRegistry#performSave() 를 호출하게 되고 내부에 가지고 있는 savedStateProviders 와 restoredState로 저장하기 때문에 저장과정 자체는 비교적 복원과정보다 굉장히 단순하여 빠름을 짐작할 수 있다.

끝으로

ViewModel과 SavedState api 들의 내부구조를 살펴보게된 원인은 사실 수행하고 있던 “헬스케어앱” 프로젝트에서 좀더 나은 객체지향적인 클래스를 설계하고 디자인패턴을 학습해보기 위한 것이었다.

디자인패턴들을 찾아보던중 생성패턴인 팩토리패턴과 구조와 행위에 대한 패턴인 스트레티지패턴, 데코레이터패턴 등등을 찾아보던중에 팩토리패턴으로 클래스들을 설계하면서 애로사항들이 존재했고 좀더 SOLID 원칙을 적용가능한 방향으로 나의 요구사항에 맞는 패턴을 적용해볼순 없을까? 에서 모범답안인 구글의 내부코드를 살펴보자로 생각이 전환되었다.

앞서 진행했던 View #1 글에서 setContentView를 살펴보면서 ComponentActivity가 수많은 인터페이스를 구현했던중에 보였던 ViewModelProvider.Factory 인터페이스가 기억에 남았었고 팩토리패턴을 적용해보는데 도움이 될것이라 생각하고 내부구조를 하나씩 들여다 보았었다.

결과적으로 팩토리패턴 뿐만아니라 MVC, 인터페이스를 통한 DIP, 잘분리된 클래스들을 보면서 설계측면에서 꽤나 많은걸 느꼇고, 순수하게 내부 코드를 들여다본다는 것은 그원리를 정확히 파악하고 나의 요구사항에 적용할 때 도움을 받기 위함도 있지만 신입개발자로써 가독성이 좋게 잘작성된 코드를 보면서 영감을 느끼고, 회사에서 일을하려면 남의 코드를 보면서 빠르게 이해하고 요구 및 목적을 파악할수 있는 능력을 기르는것에 대한 공부도 되었다고 생각한다.

태그:

카테고리:

업데이트:

댓글남기기