Image
Coroutines/CoroutineScope

[CoroutineScope] 2. Activity에서 lifecycleScope을 사용하는 것을 주의해야 하는 이유

lifecycleScope 사용의 한계점

lifecycleScope만을 사용해 Coroutine Job을 사용하는 것은 한계점이 있다. 바로 onDestroy 시 Job이 cancel된다는 것이다. onDestroy 시 job이 cancel 된다는 것은 백그라운드로 내려가는 onStop이 일어났을 때 여전히 Job이 수행됨을 뜻한다.

 

그림1. Activity의 Lifecycle

 

즉, Activity를 finish 시키는게 아니라. 홈버튼을 눌러 onStop만 되었더라면 Activity에서는 여전히 데이터를 수집하게 된다. 이러한 불필요한 동작은 백그라운드로 내려간 앱의 메모리 사용량을 증가시켜 시스템에 의한 Crash를 만들어낼 수도 있고, 사용자가 원치 않는 데이터 사용이 일어나도록 만들 수도 있다.

 

 

lifecycleScope 문제 예시

아래와 같이 flow를 이용해 데이터를 collect하는 Coroutine Job이 있다고 해보자. 아래 코드에서는 lifecycleScope에서 flow에 대한 collect가 호출된다.

class MainActivity : ComponentActivity() {
    private val stringFlow: Flow<String> = flow {
        for (i in 0..1000) {
            emit("integer: $i")
            delay(1000)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            stringFlow.collect {
                println(it)
            }
        }
    }
}

 

 

위의 코드를 실행시켰을 때 다음과 같이 동작이 일어난다.

  • MainActivity에서 뒤로 가기를 눌렀을 때는 finish()가 호출되어 onDestroy가 호출되어 아래와 같이 데이터 수집이 중단된다.

그림2. onDestroy 시 동작

  • 하지만 만약 홈버튼을 눌러 앱이 그냥 백그라운드로 갔다면 어떻게 될까? 바로 onStop만 호출되어 데이터가 계속해서 백그라운드에서 수집된다.

그림3. home 버튼을 눌렀을 때 데이터 수집이 중단되지 않음

우리는 위에서 Activity가 백그라운드로 갔을 때도 여전히 데이터스트림에 대한 collect가 일어난다는 것을 확인할 수 있었다. 만약 해당 flow가 UI화면 구성을 위한 데이터 스트림이라면 사람들이 UI화면을 보지 않음에도 collect가 일어난다는 것을 뜻하며, 이는 우리가 생각한 동작이 아니다. 따라서 이 문제를 해결할 방안이 필요하다.

 

 

onStart에서 Job을 생성 시작하고 onStop에서 Job을 cancel하기

우리는 이를 처리하기 위해 onStart에서 Job을 생성하고 onStop에서 Job을 cancel하는 방향으로 해결할 수 있다. 아래 코드는 예시 코드이다. 예시를 위해 간단히 적어두었다.

class MainActivity : ComponentActivity() {
    private val stringFlow: Flow<String> = flow {
        for (i in 0..1000) {
            emit("integer: $i")
            delay(1000)
        }
    }

    private var job : Job? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ..
    }

    override fun onStart() {
        super.onStart()
        job = lifecycleScope.launch {
            stringFlow.collect {
                println(it)
            }
        }
    }

    override fun onStop() {
        super.onStop()
        job?.cancel()
    }
}

코드를 해석하면 Coroutine Job을 onStart에서 생성하고 onStop에서 cancel하게 되면 아래와 같이 home 버튼을 눌렀을 때도 데이터스트림(flow)에 대한 collect가 중지된다.

 

위의 코드를 수행하면 아래와 같이 home버튼을 눌렀을 때도 데이터 수집이 중단됨을 확인할 수 있다.

그림4. home 버튼 시 데이터 수집해제하면 동장

 

위 방식의 문제점과 해결책

위와 같이 매번 Coroutine Job을 생성하고 해제해주는 일은 보일러 플레이트 코드를 생성시킨다. 특히 사람의 실수로 인해 하나의 Coroutine Job이라도 cancel하는 것을 깜박했을 경우 백그라운드에서 해당 작업이 계속 일어나 메모리 사용량이 높은 상태로 유지되어 시스템에 의해 App이 강제 종료될 수도 있다.

*보일러 플레이트 코드: 반복적으로 비슷한 형태를 띄는 코드

 

이러한 상황 방지를 위해 안드로이드에서는 onStart에서의 job의 생성과 onStop에서의 job의 cancel을 위한 API를 제공한다. 바로 lifecycleScope에서 쓸 수 있는 repeatOnLifecycle 함수이다. 위 글을 통해 왜 repeatOnLifecycle API를 사용해야 하는지 이해했으면 다음 장으로 넘어가 repeatOnLifecycle에 대해 살펴보도록 하자.

반응형

 

이 글의 저작권은 '조세영의 Kotlin World' 에 있습니다. 글, 이미지 무단 재배포 및 변경을 금지합니다.

 

 

Kotlin, Android, Spring 사용자 오픈 카톡

오셔서 궁금한 점을 질문해보세요!
비밀번호 : kotlin22

open.kakao.com