반응형

얼마 전에 플레이 스토어에 'Memory King' 이라는 간단한 게임을 올렸습니다.

늘 그렇듯 앱에 Crashlytics를 연동해두어 유저 측에서 비정상 종료가 발생했을 경우 해당 Error Log를 수신하도록 해두었는데, 출시한 지 이틀 만에 아래와 같은 에러를 만났습니다.

 

Fatal Exception: java.lang.IllegalStateException

Can not perform this action after onSaveInstanceState


무슨 말인고 하니.. Fragment가 전환되면서 발생하는 Exception입니다.

저 같은 경우 Activity에 navigateTo() 메소드를 두어 액티비티 내의 각각의 Fragment들이 getActivity()를 통해 navigateTo() 메소드를 호출하여 FragmentTransaction이 동작할 수 있도록 했습니다.

/**
* Navigate to the given fragment.
*
* @param fragment       Fragment to navigate to.
* @param addToBackstack Whether or not the current fragment should be added to the backstack.
*/
fun navigateTo(fragment: Fragment, addToBackstack: Boolean) {
    val transaction = supportFragmentManager
                    .beginTransaction()
                    .replace(R.id.container, fragment)

    if (addToBackstack) {
        transaction.addToBackStack(null)
    }

    transaction.commit()
}

자, 위 코드는 크게 문제가 없어 보입니다. 그러나 Exception의 Throwable을 살펴보면, transaction.commit() 부분에서 문제가 발생합니다.

그러면 지금부터 왜 문제가 발생하는지, 그리고 어떻게 해결할 수 있는지 살펴보겠습니다.

 

Fragment 전환 시 IllegalStateException 원인

문제의 원인은 Activity의 onSaveInstanceState()가 호출된 후에 FragmentTransaction의 commit()을 동작하는 데에 있습니다.

안드로이드 시스템은 메모리를 비우기 위해 어느 시점에서든 프로세스(액티비티, 서비스 등)를 종료할 권한을 가지고 있습니다.

 

이때 onSaveInstanceState() 콜백 메소드는 Activity에게 자신의 상태를 저장할 수 있도록 주어진 기회(?)라고 보시면 됩니다. 이 메소드를 통해 Bundle 형태로 자신의 Fragment, Dialog, View 등에 대한 정보를 시스템 프로세스로 보내 안전하게 보관하고 있다가 추후 시스템이 Activity를 다시 생성하기로 결정했을 때에 복구하는 용으로 사용할 수 있도록 저장했던 Bundle 객체 그대로를 받게 됩니다.

 

그렇다면 onSaveInstanceState() 메소드와 FragmentTransaction이 Exception을 발생시키는 것과 무슨 상관이 있을까요?

그건 바로 Bundle 객체가 onSaveInstanceState()가 호출된 시점의 스냅샷이라는 데에 있습니다.

쉽게 말해 onSaveInstanceState() 이 호출된 이후에 Fragment 전환이 발생한다면, onSaveInstanceState()을 통해 결정되는 복구 시점과 다르기에 해당 FragmentTransaction에 대해서는 복구할 수 없게 돼버립니다. 만약 이런 상황이 발생한다면 사용자 경험(UX)을 해치는 결과를 초래하고, 안드로이드는 이를 방지하고자 IllegalStateException을 던져버립니다.

 

해결책

위에서 살펴본 원인에 따르면 한 가지 유추해볼 수 있는 것은, Activity Lifecycle 메소드 내에서 Transaction을 커밋할 때 항상 조심해야 한다는 것입니다. 특히 onResume() 메소드 내에서는 절대 커밋해서는 안됩니다. 해당 메소드는 액티비티가 복구되기 이전에 호출될 수 있기 때문에 많은 문제를 야기합니다.

 

당장의 IllegalStateException에 대한 해결책은 크게 두 가지가 있습니다.

 

첫 번째는 비동기 콜백 메소드(Asynchronous Callback Method) 안에서 Transaction commit을 하지 않는 것입니다. 되도록이면 동기(Synchronous)로, 거기에다가 액티비티의 상태를 저장하는 onSaveInstanceState()가 호출되기 전이 보장되는 곳에서 commit을 수행합니다.

 

두 번째는 Transaction의 commitAllowingStateLoss() 메소드를 사용하는 것입니다. 이는 Activity State Loss 현상에 대해 무시하겠다는 것으로, 복구 시점에서 상태 손실이 발생하더라도 괜찮을 때 사용하면 됩니다만 권장하는 방법은 아닙니다.

두 번째 방법에 의하면 처음에 살펴봤던 navigateTo() 메소드가 아래와 같이 변경됩니다.

override fun navigateTo(fragment: Fragment, addToBackstack: Boolean) {
        val transaction = supportFragmentManager
            .beginTransaction()
            .replace(R.id.container, fragment)

        if (addToBackstack) {
            transaction.addToBackStack(null)
        }

        // transaction.commit()
        transaction.commitAllowingStateLoss()
    }

 

이상으로 Fragment 전환 시 발생하는 IllegalStateException에 대해 살펴봤습니다.

명심할 것은 가장 좋은 해결책은 액티비티의 상태가 저장되기 전에 Transaction commit을 수행하는 것이라는 겁니다.

반응형
반응형