Kotlin Coroutine (7) : Coroutine Context

Giyun Kim·2026년 3월 2일

Kotlin Coroutine

목록 보기
7/8

0. 들어가며

저번 글에서 Coroutine Builder를 중점적으로 다뤘다. 이번 글에서는 마르친 모스카와의 Kotlin Coroutine 7장의 내용을 기반으로 Coroutine Context를 살펴보기로 한다.

1. Coroutine Builder와 Coroutine Context

저번 글에서 다룬 Coroutine Builder의 정의를 보면, 별 다른 설명 없이 넘어가긴 했으나 첫 번째 파라미터가 Coroutine Context임을 알 수 있다.

fun <T> runBlocking(
	context: CoroutineContext = EmptyCoroutineContext, /* Coroutine Context*/
    block : suspend CoroutineScope.() -> T
) : T

fun CoroutineScope.launch(
	context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT, 
    block: suspend CoroutineScope.() -> Unit
) : Job

fun <T> CoroutineScope.async(
	context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT, 
    block: suspend CoroutineScope.() -> Unit
) : Deferred<T>

CoroutineScope의 정의를 자세히 확인해보자.

public interface CoroutineScope {
	public val coroutineContext : CoroutineContext
}

CoroutineScope가 마치 CoroutineContext를 감싸는 Wrapper처럼 보인다. Continuation도 다시 복기해보자.

public interface Continuation<in T> {
	public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

Continuation 역시 CoroutineContext를 포함하고 있다. 우연의 일치일까?

2. CoroutineContext

2-1. CoroutineInterface

CoroutineContext란 원소나 원소들의 집합을 나타내는 인터페이스로 정의할 수 있다.

Job이나 CoroutineName, Dispatcher 같은 Element 객체들이 인덱싱된 집합이 CoroutineContext인데, 마치 set, map과 같은 Collection과 개념이 유사하다.

다만 특이한 점은, CoroutineContext Interface의 Element 또한 CoroutineContext라는 것이다.
따라서, Collection 내 모든 원소는 그 자체만으로도 Collection이라고 볼 수 있다.
책에서는 머그잔을 예시로 든다.

- 머그잔은 자체로도 하나의 원소다.
- 머그잔은 단 하나의 원소(액체류)를 포함하는 Collection이기도 하다.
- 머그잔 하나를 추가하면 두 개의 원소를 가진 Collection이 된다.

아래 예시에서 볼 수 있듯, Contxt의 지정/변경을 편리하게 하고자 CoroutineContext의 모든 원소가 CoroutineContext로 되어있음을 확인할 수 있다.

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

여기서 Context 내 모든 원소는 식별가능한 Primary Key를 갖고 있으며, 해당 키는 주소로 비교된다. 이 때, 위의 CoroutineName이나 Job은, CoroutineContext를 구현하는 CoroutineContext.Element를 구현하게 된다.

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

3. CoroutineContext 활용

3-1. CoroutineContext에서 원소 찾기

CoroutineContext는 Collect과 유사하다고 소개했었다. 그렇기에 get을 이용해 Primary Key를 가진 원소를 찾는 것도 가능하다. Kotlin에서 get 메서드를 활용할 때 대괄호([])를 사용할 수도 있기에, 대괄호를 이용한 방식도 가능하다. 원소가 Context에 있다면 반환되고, 그렇지 않다면 null이 반환된다.

fun main() {
	val ctx: CoroutineContext = CoroutineName("A name")
    val coroutineName = ctx[CoroutinName] /* ctx.get(CoroutineName)도 가능하다. */
	...
    val job: Job? = ctx[Job]
    println(job) /* null */
}

상기 예시에서 CoroutineName은 있지만 Job은 없었다. 따라서 null이 반환됨을 알 수 있다.
CoroutineName을 찾으려면 CoroutineName을 사용하기만 하면 되는데, CoroutineName이 타입이나 클래스가 아니라 컴패니언 객체이기 때문이다. Kotlin에서는 클래스 이름이 컴패니언 객체에 대한 참조로 사용되기에 ctx[CoroutineName]은 ctx[CoroutineName.key]가 된다.

3-2. Context 더하기

CoroutineContext의 유용한 기능은, 두 개의 CoroutineContext를 합쳐서 단일 CoroutineContext로 만들 수 있다는 것이다.
각 CoroutinContext는 Key를 가진다고 앞서 소개했는데, 둘을 합치면 어떻게 될까?

정답은 간단하다, 두 가지 Key를 모두 갖게 된다.

fun main() {
	val ctx1: CoroutineContext = CoroutineName("Name1") /* ctx1[CoroutineName]?.name == Name1 */
    val ctx2: CoroutineContext = Job() /* ctx2[Job]?.isActive() == true*/
    val ctx3 = ctx1 + ctx2 /* 이게 가능하다! */
    /* ctx3은 name과 job을 모두 가진다. */
}

그럼 같은 Key를 가진 CoroutineContext를 합치면 어떻게 될까? 새 원소가 기존 원소를 대체한다.

fun main() {
	val ctx1: CoroutineContext = CoroutineName("Name1") /* ctx1[CoroutineName]?.name == Name1 */
    val ctx2: CoroutineContext = CoroutineName("Name2") /* ctx2[CoroutineName]?.name == Name2 */
    val ctx3 = ctx1 + ctx2 /* ctx3[CoroutineName]?.name == Name2 */
}

3-3. 비어있는 Coroutine Context

CoroutineContext는 앞서 소개했듯 Collection이다. 따라서, 빈 Context를 만들 수도 있다. 빈 Context는 원소가 없기에, 다른 Context에 더해도 변화가 없다.

3-4. 원소 제거

minusKey 함수에 Key를 넣는 방식으로 해당 원소를 Context에서 제가할 수 있다.

fun main() {
	val ctx1 = CoroutineName("Name1") /* ctx1[CoroutineName]?.name == Name1 */
    val ctx2 = ctx1.minusKey(CoroutineName)
}

4. CoroutineContext와 Builder

CoroutineContext는 Coroutine의 데이터를 저장하고, 전달하는 방법이다. 보통 부모는 자식에게 Context를 전달하므로, 자식은 부모로부터 Context를 상속받는다고 볼 수 있다.

모든 자식은 Builder의 인자에서 정의된 특정 Context를 가질 수도 있는데, 그런 경우, 해당 인자로 전달된 Context가 부모로부터 상속받은 Context를 대체하게 된다.

CoroutineContext를 계산하는 공식은 아래와 같다.

defaultContext + parentContext + childContext

아까 소개했듯 새 원소가 동일 Key를 가진 기존 원소를 대체하기에, ChildContext가 parentContext 중 동일 Key를 가진 원소를 대체한다. defaultContext는 child든 parent든 해당 key가 지정되지 않은 경우 사용된다.

5. 중단 함수에서 Context에 접근하기

CoroutineScope는 Context를 접근할 때 사용하는 coroutineContext 프로퍼티를 갖고 있다. 중단함수에서 어떻게 접근할 수 있는 걸까?

앞선 글에서 보았듯이, 중단 함수 간 전달되는 Continuation 객체가 Context를 참조하고 있다. 그렇기에 중단함수에서 parent context에 접근하는 것이 가능한 것이다.

따라서, coroutineContext 프로퍼티는 모든 중단 스코프에서 사용가능하며, 그걸 매개로 Context에 접근한다.

6. 개별적으로 Context를 생성하기

Coroutine Context를 Custom하게 만들 수 있을까? 흔치는 않지만 의외로 방법은 간단하다. 가장 쉬운 방법은 CoroutineContext.Element Interface를 구현하는 클래스를 내가 직접 만드는 것이다.

다만, 책의 저자의 경우 테스트 환경과 프로덕션 환경에서 서로 다른 값을 주입하고자 할 때 말고는 일반적으로 사용하지 않는다고 소개했기에 나 역시도 이번 글에서는 다루지 않는다.

7. 마치며

이번 글을 다루는 내용이 방대해 목차도 세부적으로 나눴고, 무엇보다도 길이가 길다.

이번 글을 요약하자면 아래와 같다.

- CoroutineCotext는 우리가 흔히 사용하는 map, set 같은 Collection과 개념적으로 비슷하다.
- CoroutineContext는 Element Interface의 인덱싱 된 집합이며, 그 Element 역시 CoroutineContext이다.
- CoroutineContext 내 원소는 모두 Primary Key를 가진다.
- CoroutineContext는 Coroutine 관련 정보를 그룹화하고 전달하는 일반적인 방법이다.

꽤 깊고, 어려운 부분이 많으니 여러 차례 읽어볼 것을 권유한다.
다음 장에서는 CoroutineContext 중 가장 필수적인 것을 다룰 예정이다.

profile
Android 개발자가 되기까지

0개의 댓글