매끄럽지 못한 번역, 의역 관련 미리 양해 부탁드립니다 🥲
Coroutine은 비동기 코드 실행을 달성하기 위한 안드로이드 개발 도구 중 하나입니다. 우리가 알고 있듯이 비동기 또는 non-blocking 프로그래밍은 개발에서 꽤 중요한 부분입니다. Coroutine을 실행하기 위해서는 CoroutineScope
라고 불리는 scope에서 실행해야 합니다. 이 CoroutineScope
는 우리가 실행 중인 코루틴을 추적하고, 메모리 누수를 피하기 위해 사용하지 않는 코루틴을 취소하는 데 도움을 줍니다. CoroutineScope
외에도 몇 가지 scope가 있습니다.
장기 비동기 작업을 실행하고 싶다고 가정해 봅시다. 작업이 잘 진행된다면 예상대로 완료되고 종료될 것입니다. 하지만 작업이 긴 시간 동안, 그리고 사용자가 더 이상 사용할 의도가 없을 때에도 여전히 실행되고 있다면 우리는 그 작업이 사용자의 CPU와 메모리 자원을 낭비하는 것을 원하지 않기 때문에 어느 시점에서 그것을 중단해야 합니다. Coroutine에서 우리는 CoroutineScope
를 사용하여 작업을 추적하고 수명을 제한함으로써 이 문제를 피할 수 있습니다. CoroutineScope
를 사용하여 실행 중인 Coroutine을 추적함으로써 작업을 더 이상 실행할 필요가 없을 때 취소할 수 있습니다.
class ExampleActivity: AppCompatActivity() {
...
private lateinit var mCoroutineScope: CoroutineScope
...
private fun coroutineTest() {
mCoroutineScope = CoroutineScope(Dispatchers.Main)
mCoroutineScope.launch {
println("loading..")
delay(3000)
println("job is done")
}
}
override fun onDestroy() {
super.onDestroy()
if(::mCoroutineScope.isInitialized && mCoroutineScope.isActive) {
mCoroutineScope.cancel()
}
}
클래스 내에 mCoroutineScope
라는 변수를 정의하고 아무 CoroutineContext
를 사용해 실행할 Coroutine의 scope를 정의합니다. 이 경우 Dispatchers.Main
을 사용합니다. 그런 다음 Coroutine scope 내의 작업을 취소하고 싶을 때에는 간단히 mCoroutine.cancel()
을 호출하면 됩니다. 위의 코드 스니펫에서 Activity가 destroy 된다면 mCoroutineScope
내의 코루틴 실행을 취소할 것입니다.
만약 scope 내에서 일부 Coroutine만 취소하고 다른 Coroutine을 유지하려면 어떻게 해야 할까요? launch
를 사용하여 Job
을 정의하고 원할 때마다 취소할 수 있습니다. Job
은 사실 Coroutine 그 자체입니다. Coroutine은 Job
으로 대표됩니다. 우리가 launch
를 호출할 때마다 Job
인스턴스가 반환됩니다.
...
private lateinit var mJob1: Job
private lateinit var mJob2: Job
...
private fun coroutineTest() {
mCoroutineScope = CoroutineScope(Dispatchers.Main)
mJob1 = mCoroutineScope.launch {
println("loading..")
delay(3000)
println("job 1 is done")
}
mJob2 = mCoroutineScope.launch {
println("loading..")
delay(3000)
println("job 2 is done")
}
}
private fun cancelJob1() {
if (::mJob1.isInitialized && mJob1.isActive) {
println("job 1 is canceled")
mJob1.cancel()
}
}
위에 정의된 대로 coroutineTest()
메소드를 호출한다고 가정해 봅시다. 메소드는 mCoroutineScope
내에서 job1과 job2를 시작합니다. 이 case에서 mCoroutineScope
내부의 모든 job을 취소하려면 mCoroutineScope.cancel()
을, job1만 취소하려면 mJob1.cancel()
을 호출할 수 있습니다.
GlobalScope
는 애플리케이션이 살아있는 한 작동하는, 최고 수준의 CoroutineScope
입니다. 보통 애플리케이션 scope에서 실행중인 작업을 launch할 때 앱이 죽을 때까지 작업을 유지하기 위해 사용합니다. GlobalScope
는 애플리케이션의 생명주기 동안 살아있는 싱글톤 객체입니다. GlobalScope
는 CoroutineScope
와 동일하지만 파라미터로 CoroutineContext
를 가지지 않습니다. GlobalScope
는 우리가 launch의 파라미터에 CoroutineContext
를 정의해주지 않아도 기본적으로 Dispatchers.Default
라는 CoroutineContext
를 가지고 있습니다.
GlobalScope.launch(Dispatchers.Main) {
println("loading..")
delay(3000)
println("job is done")
}
위의 스니펫 코드에서 사용 예시를 볼 수 있습니다. CoroutineScope
와 달리, GlobalScope
에는 CoroutineContext
생성자가 없습니다. GlobalScope
에서 실행 중인 작업을 취소하고 싶다면, CoroutineScope
와 동일하게 cancel
메소드를 사용하거나 개별적으로 작업을 취소할 수 있습니다.
RunBlocking
은 CoroutineScope
를 제공하지만 block 기능이 있는 scope 빌더입니다. 따라서 일반 CoroutineScope
대신에 RunBlocking
을 사용하면 현재 스레드를 block 하고 runBlocking
내부의 코드가 실행될 때까지 기다릴 것입니다. 아래의 코드 스니펫을 살펴봅시다.
private fun coroutineScopeTest() {
CoroutineScope(Dispatchers.Main).launch {
delay(1000)
println("1")
}
println("2")
}
private fun runBlockingTest() {
runBlocking {
delay(1000)
println("1")
}
println("2")
}
// Output for coroutineScopeTest()
2
1
// Output for runBlockingTest()
1
2
coroutineScopeTest
와 runBlockingTest
라는 2가지의 메소드가 있습니다. 첫 번째 메서드는 CoroutineScope
를 실행하고 두 번째 메서드는 RunBlocking
을 실행합니다. coroutineScopeTest
의 출력에서 볼 수 있듯이, Coroutine은 비동기적으로 실행됩니다. CoroutineScope
와 달리, RunBlocking
은 코드를 동기적으로 실행하므로 RunBlocking
이후의 코드들은 RunBlocking
의 실행이 완료될 때까지 기다려야 합니다.
RunBlocking
은 일반 blocking 코드를 suspending style로 작성된 함수나 라이브러리에 연결하도록 설계되었습니다. suspend 함수를 사용하려면 Coroutine의 scope에 넣어야 하기 때문입니다. 하지만 통상 Coroutine은 주로 비동기 및 동시 계산을 위해 사용되기 때문에 이런 식으로 사용하는 것은 권장되지 않습니다.
Activity 또는 Fragment에서 CoroutineScope
를 사용할 때에는 메모리 누수와 자원 낭비를 피하기 위해 Activity 또는 Fragment를 destroy 할 때 실행 중인 Coroutine이 멈추도록 해야 합니다. 따라서 우리는 lifecycle owner가 destory 될 때 CoroutineScope
를 확인하고 취소해야 합니다. 이 경우, LifecycleScope
를 사용하면 lifecycle owner가 destroy 되기 전에 실행 중인 Coroutine을 수동으로 확인하고 취소할 필요가 없습니다. LifecycleScope
는 lifecycle에 연결되어 있으므로 Coroutine 수명은 lifecycler owner의 수명을 따를 것입니다.
LifecycleScope
를 사용하면 특별한 launch 조건을 사용할 수도 있습니다 :
launchWhenCreated
는 lifecycle이 최소한 create 상태에 있으면 Coroutine을 launch하고 destroy 상태에 있으면 suspend 됩니다.launchWhenStarted
는 lifecycle이 최소한 start 상태에 있으면 Coroutine을 시작하고 stop 상태에 있으면 suspend 됩니다.launchWhenResumed
는 lifecycle이 최소한 resume 상태에 있으면 Coroutine을 시작하고 pause 상태에 있으면 suspend 됩니다.lifecycleScope.launchWhenResumed {
println("loading..")
delay(3000)
println("job is done")
}
예를 들어, lifecycle owner(Activity 또는 Fragment)가 최소한 onResumed 라면 위의 코드 스니펫이 실행됩니다. lifecycle owner가 여전히 onCreated 거나 onStarted 라면 Coroutine은 실행되지 않습니다. 기본적으로 LifecycleScope
에는 Dispatchers.Main
이 기본 CoroutineContext
로 있습니다.
ViewModelScope
는 ViewModel에서 발생하는 것을 제외하고는 LifecycleScope
와 유사합니다. ViewModelScope
를 사용하면 Coroutine을 수동으로 취소할 필요가 없으며, ViewModel이 cleared 될 때 자동으로 취소되도록 할 수 있습니다.
viewModelScope.launch {
println("loading..")
delay(3000)
println("job is done")
}
ViewModelScope
의 사용 예는 위의 코드 스니펫에서 볼 수 있습니다. LifecycleScope
와 마찬가지로, 기본적으로 ViewModelScope
에는 Dispatchers.Main
이 기본 CoroutineContext
로 있습니다.
결론적으로, 우리는 다음과 같이 Coroutine에 적절한 scope를 사용할 수 있습니다 :
ViewModelScope
를 사용하세요.LifecycleScope
를 사용하세요.CoroutineScope
를 사용하세요.GlobalScope
를 사용하세요.RunBlocking
을 사용하세요.Several Types of Kotlin Coroutine Scope Difference: CoroutineScope, GlobalScope, etc.