CoroutineContext란 코루틴을 실행하는 실행 환경을 설정하고 관리하는 interface이다. 쉽게 말해서 CoroutineContext 객체는 코루틴의 실행 환경 정보를 담은 객체라고 할 수 있다.
CoroutineContext 객체는 CoroutineName, CoroutineDispatcher, Job, CoroutineExceptionHandler 이렇게 네 가지 주요한 구성 요소를 가진다. 이 네 가지 이외에도 다양한 구성 요소가 있지만 주요한 구성 요소는 이 네 가지이다.
public interface CoroutineContext {
// ...
/**
* An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
*/
public interface Element : CoroutineContext {
/**
* A key of this coroutine context element.
*/
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}
}
CoroutineContext interface 안에 CoroutineContext interface를 상속하는 Element라는 interface가 존재한다. CoroutineContext의 구성 요소는 바로 이 Element라는 interface의 구현체이다.
하나씩 확인해보자.
// CoroutineName.kt
public data class CoroutineName(
/**
* User-defined coroutine name.
*/
val name: String
) : AbstractCoroutineContextElement(CoroutineName)
// CoroutineContextImpl.kt
public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element
CoroutineName 클래스는 AbstractCoroutineContextElement 추상 클래스를 상속하고 AbstractCoroutineContextElement는 Element 인터페이스를 구현하는 것을 확인할 수 있다.
// CoroutineDispatcher.kt
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor
CoroutineDispatcher도 마찬가지로 Element 인터페이스를 구현하는 AbstractCoroutineContextElement 추상 클래스를 상속하는 것을 확인할 수 있다.
// Job.kt
public interface Job : CoroutineContext.Element
// CoroutineExceptionHandler.kt
public interface CoroutineExceptionHandler : CoroutineContext.Element
Job 인터페이스와 CoroutineExceptionHandler 인터페이스는 Element 인터페이스를 상속한다.
이렇게 네 가지 주요 구성 요소가 Element 인터페이스의 하위 객체라는 것을 알 수 있다.
/**
* Persistent context for the coroutine. It is an indexed set of [Element] instances.
* An indexed set is a mix between a set and a map.
* Every element in this set has a unique [Key].
*/
@SinceKotlin("1.3")
public interface CoroutineContext
CoroutineContext가 정의된 곳을 보면 CoroutineContext가 Element 객체들의 indexed set라고 설명되어 있다. 추가적으로 indexed set은 set과 map을 섞어놓은 형태이며 이 set의 모든 요소는 고유한 Key를 가진다고 설명되어 있다.
그래서 CoroutineContext 객체는 Key-Value 쌍으로 각 구성 요소를 관리하고 동일한 키에 대해 중복된 값을 허용하지 않는다. 만약 동일한 키에 대한 새로운 값을 추가하면 이전에 존재하던 값은 새로운 값으로 덮어쓰기 된다. 따라서 CoroutineContext 객체는 CoroutineName, CoroutineDispatcher, Job, CoroutineExceptionHandler 객체를 각각 한 개씩만 가질 수 있다. 이에 대한 설명은 이 글 하단의 "CoroutineContext 구성요소 조합"에서 좀더 자세히 설명한다.
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
/** @suppress */
@ExperimentalStdlibApi
public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>(
ContinuationInterceptor,
{ it as? CoroutineDispatcher })
// ...
}
예시로 CoroutineContext 구성요소 중 하나인 CoroutineDispatcher를 보면 Key가 companion object로 선언되어 있으므로 싱글톤 객체로 되어 있다는 것을 알 수 있다. 즉 서로 다른 코루틴 컨텍스트여도 구성요소의 키는 고유하며 동일하다는 것이다.
fun main() = runBlocking<Unit> {
val coroutineName1: CoroutineName = CoroutineName("Coroutine1")
val coroutineName2: CoroutineName = CoroutineName("Coroutine2")
println(coroutineName1.key === coroutineName2.key) // true
println(Dispatchers.IO.key === Dispatchers.Default.key) // true
}
위의 코드에서는 key
프로퍼티를 사용하였는데 key
프로퍼티는 companion object로 선언된 Key
와 동일한 객체를 가리킨다. 서로 다른 CoroutineName인데도 두 객체의 Key는 하나인 것을 알 수 있다. Key가 싱글톤 객체이기 때문이다. 마찬가지로 서로 다른 Dispatcher여도 Key는 싱글톤 객체로 하나이다.
아까 "CoroutineContext가 구성 요소를 관리하는 방법" 파트에서 CoroutineContext 객체는 CoroutineName, CoroutineDispatcher, Job, CoroutineExceptionHandler 객체를 각각 한 개씩만 가질 수 있다고 하였다.
그래서 만약 기존의 CoroutineContext 객체에 같은 구성 요소가 추가된다면 나중에 추가된 구성 요소가 반영되고 이전의 구성 요소는 제거된다. 무슨 말인지 아래의 예시 코드를 보자.
fun main() = runBlocking<Unit> {
val coroutineContext: CoroutineContext = Dispatchers.IO + CoroutineName("OldCoroutine")
launch(coroutineContext + CoroutineName("NewCoroutine") + EmptyCoroutineContext) {
println(Thread.currentThread().name)
}
}
// 출력 :
// DefaultDispatcher-worker-1 @NewCoroutine#2
기존의 coroutineContext
의 CoroutineName은 "OldCoroutine"이었다. 하지만 코루틴을 생성하여 실행할 때 기존의 coroutineContext
에 CoroutineName("NewCoroutine")을 plus하였더니 출력결과에서는 나중에 추가했던 NewCoroutine만 이름으로 표시된다. CoroutineName이 아닌 다른 구성요소도 모두 동일한 원리가 적용된다. (Coroutine 이름을 표시하는 세팅은 이 글 참조)
CoroutineContext 객체는 위 예시 코드의 val coroutineContext: CoroutineContext = Dispatchers.IO + CoroutineName("MyCoroutine")
처럼 구성요소 객체를 plus로 조합하여 만들 수 있다. 여기서 plus operator 함수를 잠깐 살펴보자.
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
여기서 this는 기존의 CoroutineContext이고 context
파라미터는 새로운 CoroutineContext 객체이다.
context
가 EmptyCoroutineContext일 때는 기존의 CoroutineContext를 반환한다. 이것은 EmptyCoroutineContext를 더했을 때는 새로운 값으로 반영되지 않는다는 말이다.
context
가 EmptyCoroutineContext가 아니라면 초깃값을 this(기존의 컨텍스트)
로 하고 fold한다.
여기서 val removed = acc.minusKey(element.key)
를 통해서 기존에 존재하던 구성요소를 제거하는 것을 알 수 있다. minusKey는 구성요소의 키를 인자로 받아서 해당 구성 요소를 제거한 새로운 CoroutineContext 객체를 반환하는 함수이다.
acc
라는 누적된 컨텍스트에서 처리 중인 요소 element의 key와 같은 key를 가진 요소를 제거하는 것이다. 아까 같은 구성 요소가 둘 이상이면 나중에 추가된 구성요소가 반영된다는 말이 이를 통해 설명이 가능하다.
그 다음 removed
가 EmptyCoroutineContext이면 반환할 것이 처리 중인 요소 element 하나밖에 남아있지 않으므로 element만 반환한다. 예를 들어 기존의 컨텍스트에 구성 요소가 CoroutineDispatcher밖에 없었고, 해당 CoroutineDispatcher를 제거했기 때문에 EmptyCoroutineContext가 되는 경우이다. 그렇다면 새로운 CoroutineDispatcher만 반환하면 되는 상황인 것이다.
그렇지 않다면 조건에 따라 조합된 Context 객체인 CombinedContext 객체를 반환한다.