CoroutineContext란
CoroutineContext는 코루틴이 실행되는 동안 필요한 모든 정보를 담고 있는 요소이다. 이는 다양한 요소들을 결합하여 코루틴의 동작 방식을 정의한다. CoroutineContext는 일종의 맵과 같이 작동하며, 각 요소는 키로 구분된다.
CoroutineContext는 다음과 같은 요소를 포함할 수 있다.
- Job: 코루틴의 생명주기를 제어하며, 코루틴의 취소와 완료를 추적한다.
- CoroutineDispatcher: 코루틴이 어느 스레드에서 실행될지 결정한다. 예를 들어, Dispatchers.Main은 메인 스레드에서, Dispatchers.IO는 입출력 작업을 위한 백그라운드 스레드에서 코루틴을 실행하도록 지시한다.
- CoroutineName: 디버깅을 위해 코루틴에 이름을 부여한다.
- CoroutineExceptionHandler: 코루틴에서 발생하는 예외를 처리하는 방법을 정의한다.
CoroutineContext는 plus 연산자(+)를 사용하여 결합할 수 있으며, 같은 타입의 요소가 여러 개 있을 경우 나중에 추가된 요소가 이전 요소를 덮어쓴다. 이를 통해 코루틴의 동작 방식을 세밀하게 조절할 수 있다.
Coroutine Builder, Coroutine Scope, Continuation의 내부 구현을 살펴보면 전부 Coroutine Context 를 포함하고 있는 것을 확인할 수 있다.
Builder
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
Scope
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
Continuation
public inline fun <T> Continuation(
context: CoroutineContext,
crossinline resumeWith: (Result<T>) -> Unit
): Continuation<T>
CoroutineContext 가 분명 중요한 역할을 하는 것 같은데, 해당 개념에 대해 알아보자.
CoroutineContext 인터페이스
CoroutineContext의 정의에 대해 책에서는 이렇게 말하고 있다.
Job, CoroutineName, CoroutineDispatcher와 같은 Element(정보)들의 집합으로 Set과 Map 같은 컬렉션의 개념과 유사하다.
Job
public interface Job : CoroutineContext.Element
CoroutineName
public data class CoroutineName(
val name: String
) : AbstractCoroutineContextElement(CoroutineName)
public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element
CoroutineDispatcher
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor
public interface ContinuationInterceptor : CoroutineContext.Element
CoroutineContext.kt
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext
public interface Element : CoroutineContext {
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E?
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R
public override fun minusKey(key: Key<*>): CoroutineContext
}
}
가장 먼저 확인할 수 있는 것은, 모든 원소를 식별할 수 있는 Key를 가지고 있어 고유한 Key 로 Element들을 구별하는 것을 알 수 있다.
fun main() {
val name: CoroutineName = CoroutineName("name")
val element: CoroutineContext.Element = name
val context: CoroutineContext = name
val job: Job = Job()
val jobElement: CoroutineContext.Element = job
val jobContext: CoroutineContext = jobElement
val scope = CoroutineScope(Job() + Dispatchers.Main)
// CoroutineScope의 생성자는 CoroutineContext가 필요하고,
// Job 과 Dispatchers는 CoroutineContext를 상속받으므로 operator 를 사용할 수 있다.
}
CoroutineContext에서 원소 찾기
CoroutineContext 에서 특정 원소를 찾기 위한 방법으로, 제공하는 함수 중 get 함수를 사용해 Key 로 찾을 수 있다.
operator function 이므로 대괄호를 이용해 사용할 수 있고, 만약 원소가 없을 경우 null이 반환된다.
fun main() {
val ctx: CoroutineContext = CoroutineName("A name")
val coroutineName: CoroutineName? = ctx[CoroutineName]
println(coroutineName?.name)
val job: Job? = ctx[Job]
println(job) // Key는 CoroutineName이다.
}
// 결과
A name
null
참고로..
클래스의 이름이 컴패니언 객체에 대한 참조로 사용되는 코틀린의 특징 때문에 ctxt[CoroutineName]는 ctx[CoroutineName.key]가 됩니다.
컨텍스트 더하기
CoroutineContext의 유용한 기능 중 하나로, 두 Context를 더할 수 있다. 이와 같은 경우 Context는 두 가지의 Key 를 가질 수 있습니다.
fun main() {
val job = Job()
val dispatcher = Dispatchers.Main
val scope: CoroutineContext = job + dispatcher
println(scope)
}
// 결과
[JobImpl{Active}@64bfbc86, Dispatchers.Main[missing, cause=java.lang.RuntimeException: Stub!]]
Context에 같은 키를 가진 또다른 원소가 더해지면 맵처럼 새로운 원소가 기존 원소를 대체한다.
fun main() {
val coroutineContext1 = CoroutineName("1")
val coroutineContext2 = CoroutineName("2")
val coroutineContext3 = coroutineContext1 + coroutineContext2
println(coroutineContext3[CoroutineName]?.name)
}
// 결과
2
비어 있는 코루틴 컨텍스트
CoroutineContext는 컬렉션으로 빈 컬렉션 또한 만들 수 있다.
해당 Context에는 원소가 없으므로 다른 Context를 더해도 아무런 변화가 없다.
fun main() {
val empty : CoroutineContext = EmptyCoroutineContext
println(empty[CoroutineName])
println(empty[Job])
}
// 결과
null
null
코루틴 컨텍스트와 빌더
CoroutineContext는 기본적으로 부모-자식의 영향을 받았다. 따라서 자식 context 는 부모 context를 상속받는다고 할 수 있다.
fun main() {
runBlocking(CoroutineName("hello")) {
launch(CoroutineName("word")) {
println(this.coroutineContext[CoroutineName]?.name)
}
println(this.coroutineContext[CoroutineName]?.name)
}
}
// 결과
hello
word
'도서 > 코틀린 코루틴' 카테고리의 다른 글
2장 코루틴 라이브러리 - Job과 자식 코루틴 기다리기 (0) | 2024.02.03 |
---|---|
2장 코루틴 라이브러리 - Cancel (0) | 2024.02.03 |
2장 코루틴 라이브러리 - Coroutine Buidler (0) | 2024.02.03 |
코틀린 코루틴 이해하기 + 코루틴 실전 대입하기 (0) | 2024.01.23 |
1장 코틀린 코루틴 이해하기 - 중단은 어떻게 작동할까? (0) | 2024.01.09 |