생명주기에 안전한 코루틴

lifecycle 컴포넌트를 사용한다면, 생명주기를 인식하는 코루틴을 만들 수 있다.

LifecycleOwner로써 취급되는 AppCompatActivity(ComponentActivity) 또는 Fragment를 일반적으로 사용할 때 lifecycle 컴포넌트를 사용하게 되는데 이때 lifecycleScope를 사용할 수 있다.

일반적은 코루틴 스코프와 마찬가지로 launch를 async와 같은 함수 호출을 통해 suspendable 한 작업을 할 수 있다.

lifecycleScope.launch {
    // 코루틴 작업
}

만약 Fragment 또는 Activity와 같은 일반적인 코루틴 스코프를 만들어 작업중이라면, LifecycleOwner가 Destoryed 될 때 실행중인 코루틴을 취소하기 위해 명시적으로 CoroutineContext.cancel()을 호출해줘야 한다.

하지만, lifecycleScope에서 실행하는 코루틴은 생명주기에 맞춰 안전하게 종료되므로 안전하다. 그 이유는 LifecycleCoroutineScopeImpl 코드 내부를 살펴보면 짐작할 수 있다.

internal class LifecycleCoroutineScopeImpl(
    override val lifecycle: Lifecycle,
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
    init {
        // in case we are initialized on a non-main thread, make a best effort check before
        // we return the scope. This is not sync but if developer is launching on a non-main
        // dispatcher, they cannot be 100% sure anyways.
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            coroutineContext.cancel()
        }
    }

    fun register() {
        launch(Dispatchers.Main.immediate) {
            if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
                lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
            } else {
                coroutineContext.cancel()
            }
        }
    }

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
            lifecycle.removeObserver(this)
            coroutineContext.cancel()
        }
    }
}

Activity 또는 Fragment의 생명주기 이벤트를 받아서 파괴되었다고 판단하면, 하위 코루틴 작업들을 취소하는 것을 확인할 수 있다.

Note: ViewModel의 경우 별도의 생명주기를 갖는데, lifecycleScope대신 viewModelScope를 사용할 수 있다.

생명주기에 맞춰 코루틴 실행하기

생명주기를 인식하는 몇가지 코루틴 함수에 대해서 알아본다.

whenXXX 함수

생명주기가 특정 상태에 있지 않다면 코루틴의 실행을 정지하고 싶을 때가 있을 수 있다. whenCreated, whenStarted, whenResumed 3가지 함수가 있으며 이름에서 알 수 있듯이, 각 함수 when이후 접미어에 해당하는 생명주기에 맞춰 실행이되고, 생명주기의 상태가 충족되지 않으면 정지가 되는 똑똑한 함수다. 다음 예제코드는 생명주기 상태가 최소 STARTED 상태일 때 실행된다.

class MainActivity : AppCompatActivity() {

    val TAG = "Charlezz"

    val counterFlow:Flow<Int> = flow{
        var counter = 0
        while(true){
            delay(1000)
            emit(counter++)
        }
    }
    
    override fun onCreated(savedInstanceState: Bundle?) {
        lifecycleScope.launch {
            whenStarted {
                counterFlow.collect {
                    Log.e(TAG, "$it")
                }
            }
        }
    }

}

위 예제 코드를 실행한 뒤 중간에 홈버튼을 눌러 앱을 백그라운드 상태로 만들고 다시 앱으로 진입하여 포어그라운드 상태로 만들 때 어떠한 변화가 로그에 어떤 변화가 있는지 확인할 수 있다.

E/Charlezz: 0
E/Charlezz: 1
E/Charlezz: 2
E/Charlezz: 3
(홈버튼 눌러 빠져나옴, 더 이상 로그가 출력되지 않음)
.
.
.
(수초후에 다시 앱으로 진입)
E/Charlezz: 4
E/Charlezz: 5
E/Charlezz: 6
E/Charlezz: 7
E/Charlezz: 8
.
.
.

로그에서 알 수 있듯이 생명주기의 최소상태를 충족하지 못하면 작업하던 코루틴을 정지상태(suspended)로 만들고, 다시 생명주기가 충족되면 재개(Resumed)된다.

launch 블록내에서 whenXXX 함수를 호출하는 대신 다음과 같이 lifecycleScope.launchWhenXXX 함수 한줄로 간단히 표현할 수도 있다.

class MyFragment: Fragment {
    init {
        lifecycleScope.launchWhenStarted {
            try {
                // 정지 가능한 함수 호출 
            } finally {
                // 이 라인은 아마 생명주기가 DESTROYED가 될 때 실행된다.
            }
        }
    }
}

생명주기에 맞춰 코루틴 재시작하기

whenXXX 함수 호출을 통해 자동으로 생명주기에 맞춰 코루틴을 시작/중단/재개 할 수 있는 방법을 알아보았다. 하지만 생명주기에 맞춰 코루틴을 시작/취소/재시작 하고 싶을 때가 있다. 이러한 경우 Lifecycle과 LifecycleOwner는 repeatOnLifecycle이라는 확장함수를 제공한다. 다음 예제는 생명주기 상태가 STARTED일 때마다 새로운 스트림을 수집하는 것을 보여준다.

class MainActivity : AppCompatActivity() {

    val TAG = "Charlezz"

    val counterFlow:Flow<Int> = flow{
        var counter = 0
        while(true){
            delay(1000)
            emit(counter++)
        }
    }
    
    override fun onCreated(savedInstanceState: Bundle?) {
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED){
                counterFlow.collect {
                    Log.e(TAG, "$it")
                }
            }
        }
    }

}

이번에도 예제 코드를 실행한 뒤 중간에 홈버튼을 눌러 앱을 백그라운드 상태로 만들고 다시 앱으로 진입하여 포어그라운드 상태로 만들 때 어떠한 변화가 로그에 어떤 변화가 있는지 확인할 수 있다.

E/Charlezz: 0
E/Charlezz: 1
E/Charlezz: 2
E/Charlezz: 3
(홈버튼 눌러 빠져나옴, 더 이상 로그가 출력되지 않음)
.
.
.
(수초후에 다시 앱으로 진입)
E/Charlezz: 0
E/Charlezz: 1
E/Charlezz: 2
E/Charlezz: 3
E/Charlezz: 4
.
.
.

생명주기를 충족하지 못할 때 실행하던 코루틴은 취소하고, 생명주기가 충족될 때 다시 처음부터 작업을 하는 것을 확인할 수 있다.

카테고리: Kotlin

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.