Android에서 올바른 CoroutineScope를 사용해야 하는 이유
CoroutineScope는 Coroutine Job이 실행되는 Scope이다. CoroutineScope가 해제되면 CoroutineScope에 속한 Coroutine Job들은 모두 해제된다. 따라서 만약 CoroutineScope이 안드로이드 구성요소(Activity, ViewModel)의 Lifecycle에 따라 올바로 할당 해제되지 않는다면 해제되어야 하는 Job들이 계속해서 동작해서 Memory Leak으로 이어진다.
예를들어 App의 Lifecycle동안 유지되는 GlobalScope를 1초마다 string을 내보내는 flow를 collect하기 위해 사용해보자.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
//UI 코드 작성
}
val stringFlow : Flow<String> = flow{
for(i in 0..1000) {
emit("integer: $i")
delay(1000)
}
}
GlobalScope.launch {
stringFlow.collect {
println(it)
}
}
}
}
위 코드를 작성한 다음 App을 실행하면 Activity에서 뒤로가기를 눌러 Activity가 onDestroy되더라도 여전히 stringFlow에 대한 collect가 수행된다.
아래는 실제 수행화면이다. integer : 5가 호출됐을 때 뒤로가기를 눌렀는데도 여전히 collect가 일어나는 것을 볼 수 있고 다시 activity를 수행했을 때 이전 Coroutine job이 cancel이 안된 상태에서 다시 새로운 Coroutine job이 수행되어 collect가 일어나는 것을 볼 수 있다.
즉, 해당 코루틴 Job에 대한 제어권을 잃은 상태로 코루틴 Job이 계속 실행되어, Memory Leak이 일어난다.
이에 대해서는 Android Studio에서는 만약 GlobalScope를 사용할거면 섬세한 관리가 필요하다고 Warning 또한 표시해준다. 잘못해서라도 사용하는 일이 없도록 하자.
안드로이드를 위한 CoroutineScope : lifecycleScope, viewModelScope
우리는 Coroutine 사용시 메모리 릭을 방지하기 위해 Activity에서는 Activity의 Lifecycle에 맞춰진 CoroutineScope을 사용해야 하며, ViewModel에서도 ViewModel의 Lifecycle에 맞춰진 CoroutineScope를 사용해야 한다.
Activity에서 사용해야 하는 lifecycleScope
Activity에서는 LifecycleOwner 인터페이스의 확장 프로퍼티로 선언된 lifecycleScope를 사용하면 된다.
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
이 Scope를 사용하면 Activity가 onDestroy될 때 lifecycleScope또한 해제되어 내부 Coroutine Job 들이 취소된다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
//UI 코드 작성
}
val stringFlow : Flow<String> = flow{
for(i in 0..1000) {
emit("integer: $i")
delay(1000)
}
}
lifecycleScope.launch {
stringFlow.collect {
println(it)
}
}
}
}
아래는 위의 코드를 실행했을 때 결과이다. Activity에서 뒤로 가기를 누르면 onDestroy가 호출되므로 lifecycleScope가 취소되어 stringFlow를 collect 하는 Job또한 중지됨을 확인할 수 있다.
ViewModel에서 사용해야 하는 viewModelScope
ViewModel에서 또한 Acitivty에서와 마찬가지로 ViewModel의 확장 프로퍼티로 선언된 viewModelScope를 쓰면된다.
public val ViewModel.viewModelScope: CoroutineScope
ViewModel은 Fragment혹은 Activity의 Lifecycle에 binding되므로 viewModelScope는 binding된 lifecycle에 맞춰 viewModelScope내의 Job에 대한 취소를 하도록 한다. 만약 ViewModel이 어떻게 생성되는지 모른다면 아래 글을 보고 오도록 하자.
[ViewModel] 2. ViewModel은 어떻게 저장되는가?
예시 코드는 다음과 같다. ViewModel 클래스 내부에서는 Coroutine Job을 생성할 때 viewModelScope를 사용한다.
class MainViewModel() : ViewModel() {
val stringFlow : Flow<String> = flow{
for(i in 0..1000) {
emit("integer: $i")
delay(1000)
}
}
fun collectStringFlow(){
viewModelScope.launch {
stringFlow.collect {
println(it)
}
}
}
}
lifecycleScope 사용의 한계점
하지만 lifecycleScope에도 여전히 한계점이 있다. 바로 onDestroy 시 collect가 중단된다는 것이다. 이 말은 앱이 백그라운드로 내려가고 종료되지 않았을 때 데이터가 계속 수집된다는 것을 뜻한다.
이것이 왜 문제가 되는지 해결 방안은 무엇인지 다음 글에서 알아보도록 하자.
[Android CoroutineScope] 2. Activity lifecycleScope에서의 Coroutine Job 사용 시의 한계점과 해결 방안