(번역) 여러 Coroutine Scope의 차이: CoroutineScope, GlobalScope 등

na.ram·2023년 12월 8일
0

Kotlin

목록 보기
2/3
post-thumbnail

매끄럽지 못한 번역, 의역 관련 미리 양해 부탁드립니다 🥲


Coroutine은 비동기 코드 실행을 달성하기 위한 안드로이드 개발 도구 중 하나입니다. 우리가 알고 있듯이 비동기 또는 non-blocking 프로그래밍은 개발에서 꽤 중요한 부분입니다. Coroutine을 실행하기 위해서는 CoroutineScope 라고 불리는 scope에서 실행해야 합니다. 이 CoroutineScope는 우리가 실행 중인 코루틴을 추적하고, 메모리 누수를 피하기 위해 사용하지 않는 코루틴을 취소하는 데 도움을 줍니다. CoroutineScope 외에도 몇 가지 scope가 있습니다.

CoroutineScope

장기 비동기 작업을 실행하고 싶다고 가정해 봅시다. 작업이 잘 진행된다면 예상대로 완료되고 종료될 것입니다. 하지만 작업이 긴 시간 동안, 그리고 사용자가 더 이상 사용할 의도가 없을 때에도 여전히 실행되고 있다면 우리는 그 작업이 사용자의 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

GlobalScope는 애플리케이션이 살아있는 한 작동하는, 최고 수준의 CoroutineScope입니다. 보통 애플리케이션 scope에서 실행중인 작업을 launch할 때 앱이 죽을 때까지 작업을 유지하기 위해 사용합니다. GlobalScope는 애플리케이션의 생명주기 동안 살아있는 싱글톤 객체입니다. GlobalScopeCoroutineScope와 동일하지만 파라미터로 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

RunBlockingCoroutineScope를 제공하지만 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

coroutineScopeTestrunBlockingTest라는 2가지의 메소드가 있습니다. 첫 번째 메서드는 CoroutineScope를 실행하고 두 번째 메서드는 RunBlocking을 실행합니다. coroutineScopeTest의 출력에서 볼 수 있듯이, Coroutine은 비동기적으로 실행됩니다. CoroutineScope와 달리, RunBlocking은 코드를 동기적으로 실행하므로 RunBlocking 이후의 코드들은 RunBlocking 의 실행이 완료될 때까지 기다려야 합니다.


RunBlocking은 일반 blocking 코드를 suspending style로 작성된 함수나 라이브러리에 연결하도록 설계되었습니다. suspend 함수를 사용하려면 Coroutine의 scope에 넣어야 하기 때문입니다. 하지만 통상 Coroutine은 주로 비동기 및 동시 계산을 위해 사용되기 때문에 이런 식으로 사용하는 것은 권장되지 않습니다.

LifecycleScope

Activity 또는 Fragment에서 CoroutineScope를 사용할 때에는 메모리 누수와 자원 낭비를 피하기 위해 Activity 또는 Fragment를 destroy 할 때 실행 중인 Coroutine이 멈추도록 해야 합니다. 따라서 우리는 lifecycle owner가 destory 될 때 CoroutineScope를 확인하고 취소해야 합니다. 이 경우, LifecycleScope를 사용하면 lifecycle owner가 destroy 되기 전에 실행 중인 Coroutine을 수동으로 확인하고 취소할 필요가 없습니다. LifecycleScope는 lifecycle에 연결되어 있으므로 Coroutine 수명은 lifecycler owner의 수명을 따를 것입니다.

LifecycleScope를 사용하면 특별한 launch 조건을 사용할 수도 있습니다 :

  1. launchWhenCreated는 lifecycle이 최소한 create 상태에 있으면 Coroutine을 launch하고 destroy 상태에 있으면 suspend 됩니다.
  2. launchWhenStarted는 lifecycle이 최소한 start 상태에 있으면 Coroutine을 시작하고 stop 상태에 있으면 suspend 됩니다.
  3. 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

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를 사용할 수 있습니다 :

  1. ViewModel에서 Coroutine을 실행하려면 ViewModelScope를 사용하세요.
  2. Lifecycle owner(Activity 또는 Fragment)에서 Coroutine을 실행하려면 LifecycleScope를 사용하세요.
  3. ViewModel 및 lifecycle owner 이외에서 Coroutine을 실행하려면 CoroutineScope를 사용하세요.
  4. 애플리케이션 scope 실행 작업으로 Coroutine을 실행하려면 GlobalScope를 사용하세요.
  5. blocking 코드에서 suspned 함수나 라이브러리를 실행하려면 RunBlocking을 사용하세요.

원문

Several Types of Kotlin Coroutine Scope Difference: CoroutineScope, GlobalScope, etc.

0개의 댓글

관련 채용 정보