public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
public CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
launch 함수와 async 함수는 매개변수로 context, start, block을 가진다.
여기서 context 매개변수를 살펴보자
앞서 다뤘던 내용 중에서 “3-4. CoroutineDispatcher 사용해 코루틴 실행하기” 파트가 있었다. 이 파트에서는 context 자리에 CoroutineDispatcher 객체가 사용되었다.
이 외에도 context 매개변수에 CoroutineName 객체를 사용할 수도 있다.
이 둘이 context 인자로 사용될 수 있는 이유는 이 두 객체가 CoroutineContext 객체의 구성 요소이기 때문이다.
CoroutineContext는 코루틴을 실행하는 실행 환경을 설정하고 관리하는 인터페이스로 CoroutineDispatcher, CoroutineName, Job 등의 객체를 조합해 코루틴의 실행 환경을 설정한다.
즉, CoroutineContext 객체는 코루틴을 실행하고 관리하는데 핵심적인 역할을 하며, 코루틴의 실행과 관련된 모든 설정은 CoroutineContext 객체를 통해 이뤄진다.
6장에서 다루는 내용
- CoroutineContext의 구성 요소
- CoroutineContext 구성 방법
- CoroutineContext 구성 요소에 접근하기
- Coroutinecontext 구성 요소를 제거하는 방법
CoroutineContext 객체는 CoroutineName, CoroutineDispatcher, Job, CoroutineExceptionHandler의 네 가지 주요한 구성 요소를 가지며, 이들 구성 요소는 다음과 같은 역할을 한다.
이들 이외에도 다양한 구성요소가 있지만 주로 이 4가지 구성 요소를 사용해 코루틴을 실행시키기 때문에 이들을 중심으로 살펴본다.
CoroutineContext 객체는 키-값 쌍으로 각 구성 요소를 관리한다.

CoroutineContext 객체는 각 구성 요소를 한 개씩만 가질 수 있음
CoroutineContext 객체에 구성 요소를 추가하는 방법에 대해 살펴본다.
CoroutineContext 객체는 키-값 쌍으로 구성 요소를 관리하지만 키에 값을 직접 대입하는 방법을 사용하지 않는다.
대신 CoroutineContext 객체 간에 더하기 연산자를 통해 CoroutineContext 객체를 구성한다.
val coroutineContext: CoroutineContext =
newSingleThreadContext("MyThread1") + CorotineName("MyCoroutine")
이렇게 만들어진 CoroutineContext 객체는 다음과 같은 형태가 된다.
| 키 | 값 |
|---|---|
| CoroutineName 키 | CoroutineName(”MyCoroutine”) |
| CoroutineDispatcher 키 | newSingleThreadContext(”MyThread”) |
| Job 키 | 설정되지 않음 |
| CoroutineExceptionHandler 키 | 설정되지 않음 |
만들어진 CoroutineContext 객체는 코루틴 빌더 함수의 context 인자로 넘겨 코루틴을 실행하는데 사용할 수 있다.
fun main() = runBlocking<Unit> {
val coroutineContext: CoroutineContext =
newSingleThreadContext("MyThread") + CoroutineName("MyCoroutine")
launch(context = coroutineContext) {
println("[${Thread.currentThread().name}] 실행")
}
}
// [MyThread @MyCoroutine#2] 실행
CoroutineContext 객체에 같은 구성 요소가 둘 이상 더해지면 나중에 추가된 구성 요소가 이전의 값을 덮어씌운다.
fun main() = runBlocking<Unit> {
val coroutineContext: CoroutineContext =
newSingleThreadContext("MyThread") + CoroutineName("MyCoroutine")
val newCoroutineContext: CoroutineContext =
coroutineContext + CoroutineName("NewCoroutine")
launch(context = coroutineContext) {
println("[${Thread.currentThread().name}] 실행")
}
}
// [MyThread @NewCoroutine#2] 실행
코루틴의 이름이 변경된 이유는 CoroutineContext 객체의 각 구성 요소는 고유한 키를 갖고 있으므로 만약 같은 구성 요소에 대해 여러 객체가 입력되면 가장 나중에 들어온 값이 앞의 값을 덮어씌우기 때문이다.

여러 구성 요소로 이뤄진 CoroutineContext 객체 2개가 합쳐지고 2개의 CoroutineContext 객체에 동일한 키를 가진 구성 요소가 있다면 나중에 들어온 값이 선택된다.
이를 확인하기 위해 newSingleThreadContext() 함수로 2개의 CoroutineContext 객체를 만들어 두 객체를 합쳐보자
val coroutineContext1 = CoroutineName("MyCoroutine1") + newSingleThreadContext("MyThread1")
val coroutineContext1 = CoroutineName("MyCoroutine2") + newSingleThreadContext("MyThread2")
val combinedCoroutineContext = coroutineContext1 + coroutineContext2

위의 예시에서는 두 CoroutineContext 객체가 동일한 구성 요소들을 갖고있었기 때문에 coroutineContext1의 구성 요소들이 coroutineContext2의 구성요소로 덮어씌워졌다.
만약 coroutineContext1이 coroutineContext2가 갖고있지 않은 구성요소를 갖고 있었다면 해당 구성 요소는 덮어씌워지지 않았을 것이다.

Job 객체는 기본적으로 코루틴 빌더 함수를 통해 자동으로 생성된다.
하지만 Job() 함수를 직접 호출해 생성할 수도 있다.
val myJob = Job()
val coroutineContext: CoroutineContext = Dispatchers.IO + myJob
이 코드를 통해 생성된 coroutineContext는 다음과 같은 구조를 가진다.

다만 Job 객체를 직접 생성해 추가하면 코루틴의 구조화가 깨질 수 있다. 따라서 새로운 Job 객체를 생성해 CoroutineContext 객체에 추가하는 것은 주의가 필요하다.
이에 대해서는 7장. 구조화된 동시성에서 자세히 다룰 예정이다.
CoroutineContext 객체의 구성 요소에 접근하기 위해서는 각 구성 요소가 가진 고유한 키가 필요하다.
각 구성 요소의 키를 얻는 방법부터 알아보자
CoroutineContext 구성 요소의 키는 CoroutineContext.Key 인터페이스를 구현해 만들 수 있다.
일반적으로 CoroutineContext 구성 요소는 자신의 내부에 키를 싱글톤 객체로 구현한다.
public data class CoroutineName.Element {
public companion object Key: CoroutineContext.Key<CoroutineName>
...
}
CoroutineName 클래스 내부에 CoroutineContext.Key을 구현하는 Key 객체가 Companion Object로 선언되어 있는 것을 볼 수 있다.
이 Key를 사용하면 CoroutineContext에서 CoroutineName에 접근할 수 있다.
public interface Job: CoroutineContext.Element {
public companion object Key: CoroutineContext.Key<Job>
}
Job 인터페이스의 내부에도 Key가 Companion Object로 선언된 것을 볼 수 있다.
이는 CoroutineDispatcher와 CoroutineExceptionHandler도 마찬가지다.

fun main() = runBlocking<Unit> {
val coroutineContext = CoroutineName("MyCoroutine") + Dispatchers.IO
val nameFromContext = coroutineContext[CoroutineName.Key]
println(nameFromContext)
}
// CoroutineName(MyCoroutine)
이 코드에는 CoroutineName(”MyCoroutine”)과 Dispatchers.IO로 이뤄진 coroutineContext가 있다.
coroutineContext에 연산자 함수 get(대괄호 대체)의 인자로 CoroutineName.Key를 넘김으로써 coroutineContext를 구성하는 CoroutineName 객체만 가져올 수 있다.
CoroutineContext의 주요 구성 요소인 CoroutineName, Job, CoroutineDispatcher, CoroutineExceptionHandler 객체는 Companion Object로 CoroutineContext.Key를 갖고 있기 때문에 Key를 명시적으로 사용하지 않아도 구성 요소 자체를 키로 사용할 수 있다.
따라서 굳이 뒤에 “.Key”를 붙이지 않아도 구성 요소에 접근할 수 있다.
fun main() = runBlocking<Unit> {
val coroutineContext = CoroutineName("MyCoroutine") + Dispatchers.IO
val nameFromContext = coroutineContext[CoroutineName]
println(nameFromContext)
}
// CoroutineName(MyCoroutine)
연산자 함수 get의 인자로 넘긴 인자의 “.Key”를 제거했지만 출력 결과는 동일한 것을 확인할 수 있다.
CoroutineContext 객체의 구성 요소들은 모두 key 프로퍼티를 가진다.
이 key 프로퍼티를 사용해 구성 요소에 접근할 수 있다.
fun main() = runBlocking<Unit> {
val coroutineName: CoroutineName = CoroutineName("MyCoroutine")
val dispatcher: CoroutineDispatcher = Dispatchers.IO
val coroutineContext = coroutineName + dispatcher
println(coroutineContext[coroutineName.key])
println(coroutineContext[dispatcher.key])
}
// CoroutineName(MyCoroutine)
// Dispatchers.IO
coroutineContext를 구성하는데 사용된 coroutineName과 dispatcher 객체의 key 프로퍼티를 통해 CoroutineContext 객체의 구성 요소에 접근할 수 있음을 알 수 있다.
여기서 중요한 점은 구성 요소의 key 프로퍼티는 Companion Object로 선언된 Key와 동일한 객체를 가리킨다는 것이다.
coroutineName.key와 CoroutineName.Key는 동일한 대상을 가리킨다.
fun main() = runBlocking<Unit> {
val coroutineName = CoroutineName("MyCoroutine")
if (coroutineName.key === CoroutineName.Key)
println("coroutineName.key와 CoroutineName.Key 동일합니다.")
}
// coroutineName.key와 CoroutineName.Key 동일합니다.
동일성 연산자를 통해 coroutineName.key와 CoroutineName.Key가 동일한 객체를 가리킴을 확인할 수 있다.
CoroutineContext 객체는 구성 요소를 제거하기 위한 minusKey 함수를 제공한다.
구성 요소의 Key를 인자로 받아 해당 구성 요소를 제거한 CoroutineContext 객체를 반환한다.
val coroutineName = CoroutineName("MyCoroutine")
val dispatcher = Dispatchers.IO
val myJob = Job()
val coroutineContext: CoroutineContext = coroutineName + dispatcher + myJob

이 coroutineContext에서 CoroutineName 객체를 제거하기 위해서는 coroutineContext에 대해 minusKey 함수를 호출하고 coroutineName을 인자로 넘기면 된다.
이 경우 coroutineContextdptj CoroutineName만 제거돼 반환되며, 반환된 coroutineContext는 deletedCoroutineContext에 할당된다.

fun main() = runBlocking<Unit> {
val coroutineName = CoroutineName("MyCoroutine")
val dispatcher = Dispatchers.IO
val myJob = Job()
val coroutineContext: CoroutineContext = coroutineName + dispatcher + myJob
val deletedCoroutineContext = coroutineContext.minusKey(CoroutineName)
println(deletedCoroutineContext[CoroutineName])
println(deletedCoroutineContext[CoroutineDispatcher])
println(deletedCoroutineContext[Job])
}
// null
// Dispatchers.IO
// JobImple{Active}@65e2dbf3
코드의 출력 결과를 보면 coroutineDispatcher의 CoroutineName이 제거되 null이 출력되고 나머지는 정산적으로 출력되는 것을 확인할 수 있다.
minusKey를 호출한 CoroutineContext 객체는 유지되고, 구성 요소가 제거된 새로운 CoroutineContext 객체가 반환된다.
fun main() = runBlocking<Unit> {
val coroutineName = CoroutineName("MyCoroutine")
val dispatcher = Dispatchers.IO
val myJob = Job()
val coroutineContext: CoroutineContext = coroutineName + dispatcher + myJob
val deletedCoroutineContext = coroutineContext.minusKey(CoroutineName)
println(coroutineContext[CoroutineName])
println(coroutineContext[CoroutineDispatcher])
println(coroutineContext[Job])
}
// CoroutineName(MyCoroutine)
// Dispatchers.IO
// JobImple{Active}@65e2dbf3
코드의 실행 결과를 보면 minusKey 함수가 호출된 coroutineContext는 구성 요소가 제거되지 않기 때문에 CoroutineName 객체가 제거되지 않은 것을 확인할 수 있다.