코루틴 빌더의 정의를 보면 첫 번째 파라미터가 CoroutineContext라는 사실을 알 수 있다.

Untitled

리시버뿐만 아니라 마지막 인자의 리시버도 CoroutineScope 타입이다. CoroutineScope의 정의는 다음과 같다.

Untitled

Continuation 또한 CoroutineContext를 포함하고 있다.

Untitled

코틀린 코루틴에서 가장 중요한 요소들이 CoroutineContext를 사용하고 있는 걸 알 수 있다.

CoroutineContext 인터페이스

Untitled

CoroutineContext는 원소나 원소들의 집합을 나타내는 인터페이스다. Job, CoroutineName, CoroutineDispatcher와 같은 Element 객체들이 인덱싱된 집합이라는 점에서 Map, Set과 같은 컬렉션이랑 개념이 비슷하다. 특이한 점은 각 element 또한 CoroutineContext라는 점이다. 따라서 컬렉션 내 모든 원소는 그 자체만으로 컬렉션이라 할 수 있다.

아래 예제처럼 컨텍스트의 지정과 변경을 편리하게 하기 위해 CoroutineContext의 모든 원소가 CoroutineContext로 되어 있다. 컨텍스트를 지정하고 더하는 건 명시적인 집합을 만드는 것보다 훨씬 쉽다.

launch { CoroutineName("Name1") }
launch { CoroutineName("Name2") + Job() }

컨텍스트에서 모든 원소는 식별할 수 있는 유일한 key를 가지고 있다. 각 key는 주소로 비교가 된다.

예를 들어 CoroutineName이나 Job은 CoroutineContext 인터페이스를 구현한 CoroutineContext.Element를 구현한다.

fun main() {
    val name: CoroutineName = CoroutineName("A name")
    val element: CoroutineContext.Element = name
    val context: CoroutineContext = element
    
    val job: Job = Job()
    val jobElement: CoroutineContext.Element = job
    val jobContext: CoroutineContext = jobElement
}

SupervisorJob, CoroutineExceptionHandler와 Dispatchers 객체의 디스패처도 마찬가지다. 모두 중요하게 사용되는 코루틴 컨텍스트다.

CoroutineContext에서 원소 찾기

CoroutineContext는 컬렉션과 비슷하기 때문에 get을 이용해 유일한 키를 가진 원소를 찾을 수 있다. 대괄호를 사용하는 방법도 가능한데, 코틀린에서 get 메서드가 연산자이기 때문에 명시적인 함수 호출 대신 대괄호를 사용해 실행하는 것이 가능하기 때문이다. 원소가 컨텍스트에 있으면 반환된다는 점에서 Map과 비슷하다. 원소가 없으면 null이 대신 반환된다.

fun main() {
    val ctx: CoroutineContext = CoroutineName("A name")

    val coroutineName: CoroutineName? = ctx[CoroutineName]
    println(coroutineName?.name) // A name
    val job: Job? = ctx[Job]
    println(job) // null
}

CoroutineName을 찾기 위해서는 CoroutineName을 사용하기만 하면 된다. CoroutineName은 타입이나 클래스가 아닌 Companion Object이다. 클래스의 이름이 Companion Object에 대한 참조로 사용되는 코틀린 언어의 특징 때문에, ctx[CoroutineName]은 ctx[CoroutineName.key]가 된다.