Post

coroutine 알아보기 (6) - CoroutineScope & CoroutineContext

CoroutineScope & CoroutineContext

CoroutineScope

CoroutineScope는 코루틴의 수명 주기를 관리하는 역할을 합니다.

코루틴을 실행할 때 특정 CoroutineScope 내에서 실행하면, 해당 스코프가 종료될 때 하위 코루틴들도 함께 취소됩니다.

우리가 사용했던 launch 혹은 async 와 같은 코루틴 빌더는 CoroutineScope 의 확장함수입니다. 즉, launch 와 async 를 사용하려면 CoroutineScope이 필요했던 것입니다.

  • launch signature
    1
    2
    3
    4
    5
    
      public fun CoroutineScope.launch(
          context: CoroutineContext = EmptyCoroutineContext,
          start: CoroutineStart = CoroutineStart.DEFAULT,
          block: suspend CoroutineScope.() -> Unit
      ): Job
    
  • async signature
    1
    2
    3
    4
    5
    
      public fun <T> CoroutineScope.async(
          context: CoroutineContext = EmptyCoroutineContext,
          start: CoroutineStart = CoroutineStart.DEFAULT,
          block: suspend CoroutineScope.() -> T
      ): Deferred<T>
    

지금까지 사실은 runBlocking 이 코루틴과 루틴의 세계를 이어주며, CoroutineScope를 제공해주었고, runBlocking 안에서 launch 와 async 를 사용했었던 것입니다.

만약 우리가 직접 CoroutineScope 을 만든다면 runBlocking 이 굳이 필요하지 않습니다. main 함수를 일반 함수로 만들어 코루틴이 끝날 때까지 main 스레드를 대기시킬 수도 있고, main함수 자체를 suspend 함수로 만들어 join() 시킬 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
    val job = CoroutineScope(Dispatchers.Default).launch {
        delay(1_000L)
        printWithThread("Job 1")
    }
    Thread.sleep(1_500L) // job1이 끝나기를 기다린다.
}

suspend fun main() {
    val job = CoroutineScope(Dispatchers.Default).launch {
        delay(1_000L)
        printWithThread("Job 1")
    }
    job.join()
}

CoroutineScope의 특징

  • 코루틴 실행 범위 제공: launch, async 등의 코루틴 빌더가 동작하는 범위를 정의
  • 구조적 동시성 보장: 부모 스코프가 종료되면 자식 코루틴도 자동으로 취소됨
  • 수동 취소 가능: 필요에 따라 명시적으로 cancel()을 호출하여 코루틴을 중단할 수 있음

CoroutineContext

CoroutineContext는 코루틴이 실행되는 환경을 정의하는 요소들의 집합입니다.

실제로 CoroutineScope interface를 보면 CoroutineContext를 가지고 있는 것을 볼 수 있습니다.

1
2
3
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

각 코루틴은 특정 CoroutineContext 내에서 실행되며, CoroutineDispatcher, CoroutineExceptionHandler, 이름, 코루틴의 Job 자체 등이 들어가 있습니다.

CoroutineContext의 특징

CoroutineContext 는 Map과 Set을 섞어 둔 자료구조와 같습니다. CoroutineContext 에 저장되는 데이터는 key-value로 이루어져있으며, Set과 비슷하게 동일한 Key를 가진 데이터는 하나만 존재할 수 있습니다.

이 key-value 하나하나를 Element 라 부르고, + 기호를 이용해 각 Element 를 합치거나 context에 Element 를 추가할 수 있습니다. 만약 context에서 Element를 제거하고 싶다면, minusKey 함수를 이용해 제거할 수 있습니다.

1
2
3
4
5
6
// Element 합성
CoroutineName("my coroutine") + SupervisorJob()
// context에 element 추가
coroutineContext + CoroutineName("my coroutine")
// context에서 element 제거
coroutineContext.minusKey(CoroutineName.Key)

CoroutineScope & CoroutineContext

CoroutineScope & CoroutineContext with Structured Concurrency

위를 바탕으로 정리해보자면, CoroutineScope 은 코루틴이 탄생할 수 있는 영역이고, CoroutineScope 안에는 CoroutineContext 라는 코루틴과 관련된 데이터가 들어 있습니다.

우리가 부모 코루틴과 자식 코루틴이라고 불렀던 것도 한 영역 안에서 코루틴이 생기는 것을 의미하는 것입니다.

image.png

위의 그림과 같이 최초 한 영역에 부모 코루틴이 있다고 가정해봅니다.

이때 CoroutineContext 에는 이름, Dispatchers.Default, 부모 코루틴이 들어 있습니다. 이 상황 에서 부모 코루틴에서 자식 코루틴을 만들어봅니다.

그럼 자식 코루틴은 부모 코루틴과 같은 영역에서 생성되고, 생성될 때 이 영역의 context를 가져온 다음 필요한 정보를 덮어 써 새로운 context를 만듭니다.

예를 들어 이름을 우리가 코루틴의 이름을 직접 지정해주다면, 자식 코루틴은 지정한 코루틴의 이름을 이용해 필요한 데이터를 context에서 가져와 적절히 덮어 쓰고 새로운 context를 갖게 됩니다. 이 과정에서 부모 - 자식 간의 관계도 설정해줍니다.

image.png

그리고 이 원리가 바로 Structured Concurrency를 작동시킬 수 있는 기반이 됩니다.

그리고 이렇게 한 영역에 있는 코루틴들은 영역 자체를 cancel() 시킴으로써 모든 코루틴을 종료시킬 수 있습니다.

예를 들어 아래의 코드처럼 클래스 내부에서 독립적인 CoroutineScope 을 관리한다면, 해당 클래스에서 사용하던 코루틴을 한 번에 종료시킬 수 있게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
class AsyncLogic {
    private val scope = CoroutineScope(Dispatchers.Default)
    fun doSomething() {
        scope.launch {
        // 여기서 어떤 작업을 하고 있다.
        }
    }

    fun destroy() {
        scope.cancel()
    }
}

위에서 만든 AsyncLogic 클래스의 인스턴스를 만들고, doSomething() 함수를 호출해 비동기 작업을 처리 하다가 더 이상 필요가 없어지면, destory() 함수를 호출해 모든 코루틴을 정리 할 수 있는 것입니다.

1
2
3
val asyncLogic = AsyncLogic()
asyncLogic.doSomething()
asyncLogic.destory() // 필요가 없어지면 모두 정리

정리

  • CoroutineScope는 코루틴의 실행 범위를 제공하며, 스코프가 종료되면 실행 중인 코루틴들도 함께 취소됨
  • CoroutineContext는 코루틴의 실행 환경을 설정하며, Dispatcher, Job, 예외 핸들러 등을 포함
  • CoroutineScope 내부에서 CoroutineContext를 활용하면 보다 유연하고 안전한 코루틴 관리가 가능

[출처]

  • https://kotlinlang.org/docs/coroutines-overview.html
  • https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency
This post is licensed under CC BY 4.0 by the author.