본문 바로가기
안드로이드/기타

viewLifecycleOwner.lifecycleScope vs lifecycleScope

by 나이아카 2022. 10. 20.

 별 생각 없이 코드를 작성하다가(정확히는 멋모르고?) 저에게는 충격적으로 와닿은 것이 있었습니다. 생각해보면 면접때도 관련된 내용이 나왔었는데, 연관되지 않는다고 생각해서 그냥 지나쳤던 기억이 있네요. 그래서 이번에는 정리해볼까 합니다. 


 이번 주제는 viewLifecycleOwner.lifecycleScope로 선언한 코루틴 블럭과 그냥 프래그먼트의 lifecycleScope의 차이점에 대해서 알아보려고 합니다.

lifecycleScope

 두 개의 차이점을 알아보기 전에 먼저 lifecycleScope가 뭔지 알아보겠습니다.

 먼저 lifecycleScope는 CoroutineScope의 일종인데, 이름처럼 view의 lifecycle에 맞춰 실행되는 범위가 정해지는 코루틴을 의미합니다. 이러한 CoroutineScope에는 GlobalScope, coroutineScope, lifecycleScope등이 존재하고 있습니다. 이렇게 나눈 이유는 코루틴이 실행되는 범위를 제한해 좀 더 효율적으로 사용하기 위함입니다. 이 중 lifecycle은 view의 생존여부에 따라 실행-취소가 나뉘기 때문에 주로 UI에 뿌려주기 위한 데이터의 호출이나 관리 및 수정에 주로 이용됩니다.

 

/**
 * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
 *
 * This scope will be cancelled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
 */
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

 위의 코드는 프래그먼트의 lifecycleScope와 viewLifecycleOwner의 lifecycleScope를 클릭하면 나오는 scope입니다. 이는 두 개의 변수가 동일한 기능을 제공한다는 사실을 알려주고 있습니다.

 

fragment.lifecycleScope vs viewLifecycleOwner.lifecycleScope

 위에서 본 것처럼 두 코드의 쓰임새는 동일하지만, 동작 방식에는 차이가 존재합니다.

lifecycleScope.launch {
    val index = 0
    while (true) {
        delay(1000)
        Log.d("lifecycleScope:","$index")
    }
}

viewLifecycleOwner.lifecycleScope.launch {
    val index = 0
    while (true) {
        delay(1000)
        Log.d("viewLifecycleOwner.lifecycleScope:","$index")
    }
}

 위의 코드를 프래그먼트 A의 onViewCreated에서 선언을 했습니다. 둘 다 onViewCreated가 실행되고 난 이후에는 아무런 문제 없이 계속해서 1초당 로그를 1번씩 찍게 됩니다. 이후 프래그먼트에서 동작을 시행할 때, B 프래그먼트로 이동을 하게 됩니다. 이때 lifecycleScope의 index는 계속해서 증가하게 되지만, viewLifecycleOwner.lifecycleScope의 index는 사라지게 됩니다. 그래서 프래그먼트 B가 동작하는 동안 lifecycleScope는 계속 동작하게 되기 때문에 lifecycleScope 블럭 안에 코드를 심어두어도 프래그먼트의 이동에 따라 내부 코드의 정지가 일어나지 않습니다.

 더욱 큰 문제는, 만약 프래그먼트 B의 행동이 끝나 A로 돌아오는 경우입니다. 이 때 프래그먼트 A의 onViewCreated가 재실행되면서 lifecycleScope와 viewLifecycleOwner.lifecycleScope가 다시 실행되게 됩니다. 그러면 현재 프래그먼트의 이동이 A -> B -> A가 되고, lifecycleScope 블럭은 2개,  viewLifecycleOwner.lifecycleScope의 블럭은 1개가 존재하게 됩니다. 이 상황에서 A에 연결된 또다른 프래그먼트 C로 이동하거나 다시 B로 이동했다가 A로 돌아오게 되면 lifecycleScope 블럭은 첫 A에서 생성된 것과 B로 이동후 생성된 블럭, 그리고 마지막으로 C 또는 B로 다녀와서 생긴 블럭 1개까지 총 3개가 되고,  viewLifecycleOwner.lifecycleScope의 블럭은 그대로 1개가 존재하게 됩니다.

 다행히 이 코드를 프래그먼트 B의 onViewCreated에 생성을 해두고 A -> B -> A로 프래그먼트 이동이 이루어지는 경우에는 문제 없이 전부 종료가 완료 되는 것을 확인할 수 있었습니다. 이는 결국 onDestroy가 동작해야 lifecycle의 scope가 종료된다는 사실을 알 수 있습니다. 이는 프래그먼트에서 바로 사용할 수 있는 lifecycleScope는 LifecycleOwner를, viewLifecycleOwner.lifecycleScope는 viewLifecycleOwner에 영향을 받는다는 점을 확인할 수 있습니다.

 LifecycleOwner와 viewLifecycleOwner가 무슨 차이가 존재하는지 알아보겠습니다.

LifecycleOnwer

 연결된 프래그먼트의 생명주기를 가지고 있는 클래스로 프래그먼트의 생명주기(onAttach() ~ onDestroy())를 따릅니다.

 

viewLifecycleOwner

 연결된 프래그먼트 뷰의 생명주기를 가지고 있는 클래스로 프래그먼트의 뷰의 생성부터 제거까지(onCreateView() ~ onDestroyView())를 따릅니다.

 

 이는 프래그먼트가 액티비티와는 다르게 프래그먼트 클래스와 프래그먼트가 지닌 뷰의 생성시기가 다르기 때문에 생기는 문제입니다. 그래서 실제 프래그먼트 객체가 메모리 상에서 살아있는 동안에 실행되는 코드와 프래그먼트를 화면에 뿌려주는 동안 실행되는 코드를 달리할 수 있고, 이를 적절하게 이용해야만 누수 없이 사용할 수 있는 것입니다.

 제가 위와 같이 예시를 든 과정에서 문제가 되는 부분은 FragmentTransaction에서 replace를 하면서 addToBackStack()이 이루어지는 부분과 Home 버튼을 눌러 앱을 종료하지 않고 빠져나가는 상황입니다. 이때는 아래에 깔린 프래그먼트의 onDestroy가 동작하지 않기 때문에(lifecycleScope는 onDestroy에 제거됩니다.) lifecycleScope 내부의 코드는 계속 동작하게 됩니다. 이는 불필요한 동작이기 때문에 굳이 동작할 필요가 없습니다. 


 실 프로젝트에서는 이렇게 거의 무한으로 반복되는 코루틴 블럭을 작성할 일이 거의 없기 때문에 막 큰 일 날 정도로 리소스 낭비가 이루어지지 않을 것입니다.(저는 주로 통신 관련 코드에서 자주 사용하기 때문에 더더욱...) 하지만 기본적으로 코루틴을 사용한다면 쉽게 막을 수 있는 리소스는 잡고 가야할 것 같습니다.(중요한 개념이라고 생각합니다.)

 생각해보면 의외로 당연하고도 간단한 개념인데, 중요 정보들을 몰랐고, 알아가는 과정에서 이를 블럭처럼 쌓아나가지 못해 결국 한참이나 돌아서 이런 글을 쓰게 된 것 같습니다. (어서 개발자는 그만둬야겠습니다.)

댓글