해당 내용은 코틀린 코루틴의 정석 6 장을 토대로 공부한 내용입니다.
그동안 코루틴을 launch 나 async 로 생성하면서 우리는 context 인자에 CoroutineDispatcher 나 CoroutineName 을 전달해 주었다.
전달해 줌으로서 코루틴의 이름을 설정하고 어느 스레드에 보낼지를 결정할 수 있었다.
그렇다면 CoroutineContext 가 뭐길래 이를 가능하게 해주는걸까?
코루틴을 실행하는 실행 환경을 설정하고 관리하는 인터페이스
즉, 코루틴의 실행 환경, 정보가 담겨있는 인터페이스다.
CoroutineDispatcher , CoroutineName , Job , exceptionHandler 로 구성된다.
CoroutineName: 코루틴의 이름을 설정한다CoroutineDispatcher: 코루틴을 스레드에 할당해 실행한다.Job: 코루틴의 추상체로 코루틴을 조작하는 데 사용된다 (추적 및 제어)CoroutineExceptionHandler: 코루틴에서 발생한 예외를 처리한다CoroutineContext 객체는 키-값 쌍으로 각 구성 요소를 관리한다
| 키 | 값 |
|---|---|
| CoroutineName 키 | CoroutineName 객체 |
| CoroutineDispatcher 키 | CoroutineDispatcher 객체 |
| Job 키 | Job 객체 |
| CoroutineExceptionHandler 키 | CoroutineExceptionHandler 객체 |
각 구성요소는 고유한 키를 가지며, 키에 대한 중복된 값은 허용되지 않습니다 .
따라서 각 개체마다 한개씩만 가질 수 있습니다.
여기서 주의할 점은 키-값 을 사용한다고 해서 [key] = 객체 로 접근하는 방식이 아닌 더하기 연산자(+) 를 사용해 CoroutineContext 객체를 구성합니다.
fun main() = runBlocking<Unit> {
launch(context = CoroutineName("DK") + Dispatchers.IO) {
// 비동기 작업 실행
}
}
바로 이런식으로 사용합니다.
설정되지 않은 Job , CoroutineExceptionHandler 구성 요소는 설정되지 않은 상태로 유지 됩니다.
fun main() = runBlocking<Unit> {
val myContext = CoroutineName("MyCoroutine") + Dispatchers.IO
launch(context = myContext) {
// 비동기 코드...
}
}
이런식으로도 사용이 가능합니다.
만약 CoroutineContext 객체에 같은 구성 요소가 둘 이상 더해진다면 나중에 추가된 구성 요소가 이전의 값을 덮어 씌우게 됩니다. 예시를 통해 확인해보겠습니다.
fun main() = runBlocking<Unit> {
val myContext = CoroutineName("OldCoroutine") + Dispatchers.IO
launch(context = myContext + CoroutineName("NewCoroutine")) {
println("Coroutine Name : ${coroutineContext[CoroutineName]}")
}
}
Coroutine Name : CoroutineName(NewCoroutine)
launch 에 CoroutineName을 새 인자로 추가되었기 때문에 원래 있던 이름을 덮어 씌우게 됩니다.
즉, 나중에 들어온 값이 덮어씌웁니다!!.
Job 객체는 기본적으로 launch나 runBlocking 같은 코루틴 빌더 함수를 통해 자동으로 생성되지만 Job() 을 호출해 생성할 수도 있습니다.
fun main() = runBlocking<Unit> {
val job: Job = Job()
val coroutineContext = CoroutineName("coroutine1") + Dispatchers.Default + job
}
하지만 Job 객체를 직접 생성해 추가하면 코루틴의 구조화 가 깨지게 됩니다.
때문에 새로운 Job 객체를 생성해 CoroutineContext에 추가하는 것은 주의 필요 합니다.
이에 대해서는 추후 구조화된 동시성 에서 다루겠습니다.
CoroutineContext를 설정했으니 이번에는 각 구성 요소에 어떻게 접근하는지 알아보겠습니다.
각 구성 요소에 접근하기 위해서는 각 구성 요소가 가진 고유한 키가 필요합니다.
우선 각 구성 요소의 키를 얻는 방법부터 알아보겠습니다.
구성 요소의 키는 CoroutineContext.Key 인터페이스를 구현해 만들 수 있는데 일반적으로 CoroutineContext 구성 요소는 자신의 내부에 키를 싱글톤 객체로 구현합니다.
직접 확인해볼가요?
public data class CoroutineName(
/**
* User-defined coroutine name.
*/
val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
/**
* Key for [CoroutineName] instance in the coroutine context.
*/
public companion object Key : CoroutineContext.Key<CoroutineName>
...
public interface Job : CoroutineContext.Element {
/**
* Key for [Job] instance in the coroutine context.
*/
public companion object Key : CoroutineContext.Key<Job>
Key를 companion object 로 설정했습니다.
Job 또한 마찬가지로 인터페이스 내부에 Key가 동반 객체로 선언된 것을 볼 수 있습니다.
이는 Dispatcher , ExceptionHandler 또한 마찬가지 입니다.
이번에는 해당 키를 사용해 CoroutineContext 구성 요소에 접근해보겠습니다.
@OptIn(ExperimentalStdlibApi::class)
fun main() = runBlocking<Unit> {
val myCoroutineContext = CoroutineName("MyCoroutine") + Dispatchers.IO
val myCoroutineName = myCoroutineContext[CoroutineName.Key]
val myCoroutineDispatcher = myCoroutineContext[CoroutineDispatcher.Key]
println("CoroutineName : $myCoroutineName\nCoroutineDispatcher : $myCoroutineDispatcher")
}
// CoroutineName : CoroutineName(MyCoroutine)
// CoroutineDispatcher : Dispatchers.IO
이렇게 CoroutineName 과 Dispatcher 을 가져와 봤습니다.
이런 경우 .Key 를 붙이지 않아도 동일한 결과를 얻을 수 있습니다.
@OptIn(ExperimentalStdlibApi::class)
fun main() = runBlocking<Unit> {
val myCoroutineContext = CoroutineName("MyCoroutine") + Dispatchers.IO
val myCoroutineName = myCoroutineContext[CoroutineName]
val myCoroutineDispatcher = myCoroutineContext[CoroutineDispatcher]
println("CoroutineName : $myCoroutineName\nCoroutineDispatcher : $myCoroutineDispatcher")
}
// CoroutineName : CoroutineName(MyCoroutine)
// CoroutineDispatcher : Dispatchers.IO
키가 들어갈 자리에 CoroutineName을 사용하면 자동으로 CoroutineName.Key를 사용해 연산을 처리하기 때문에 이러한 결과를 얻을 수 있습니다.
fun main() = runBlocking<Unit> {
val coroutineName = CoroutineName("MyCoroutine")
val dispatcher = Dispatchers.IO
val coroutineContext = coroutineName + dispatcher
println(coroutineContext[coroutineName.key])
println(coroutineContext[dispatcher.key])
}
// CoroutineName(MyCoroutine)
// Dispatchers.IO
이런 식으로 구성요소들의 key 프로퍼티를 통해서도 구성 요소에 접근할 수 있습니다.
여기서 중요한점은 구성 요소의 key 프로퍼티는 동반 객체로 선언된 Key와 동일한 객체를 가리킵니다.
예를 들어 CoroutineName.Key와 coroutineName.key는 서로 같은 객체를 참조합니다.
코드를 통해 확인해보겠습니다.
fun main() = runBlocking<Unit> {
val coroutineName = CoroutineName("MyCoroutine")
val dispatcher = Dispatchers.IO
println(coroutineName.key === CoroutineName.Key)
}
// true
이를 통해 서로 동일한 객체를 가리키는 것을 알 수 있습니다.
위 내용에서 구성요소를 플러스(+) 연산자를 통해 구성 요소를 추가했씁니다.
이번에는 구성 요소를 제거하는 방법에 대해 알아보겠습니다.
fun main() = runBlocking<Unit> {
val coroutineName = CoroutineName("MyCoroutine")
val dispatcher = Dispatchers.IO
val myCoroutineContext = coroutineName + dispatcher
//myCoroutineContext의 CoroutineName 제거해보기
val deletedCoroutineContext = myCoroutineContext.minusKey(CoroutineName.Key)
println(deletedCoroutineContext[CoroutineName])
println(deletedCoroutineContext[CoroutineDispatcher])
}
// null
// Dispatchers.IO
위 코드의 경우 myCoroutineContext에서 CoroutineName만 제거돼 반환되며, 반환된 CoroutineContext는 deletedCoroutineContext에 할당됩니다.
따라서 deletedCoroutineContext는 CoroutineDispatcher 만 출력되게 됩니다.
CoroutineName 은 minusKey 로 삭제했기 때문에 null이 출력된 모습을 볼 수 있습니다.
minusKey 함수 사용 시 주의할 점은 minusKey를 호출한 CoroutineContext 객체는 그대로 유지되고, 구성 요소가 제거된 새로운 CoroutineContext 객체가 반환된다는 점입니다.
바로 확인해볼가요?
fun main() = runBlocking<Unit> {
val coroutineName = CoroutineName("MyCoroutine")
val dispatcher = Dispatchers.IO
val myCoroutineContext = coroutineName + dispatcher
//myCoroutineContext의 CoroutineName 제거해보기
val deletedCoroutineContext = myCoroutineContext.minusKey(CoroutineName.Key)
println(myCoroutineContext[CoroutineName])
println(myCoroutineContext[CoroutineDispatcher])
}
// CoroutineName(MyCoroutine)
// Dispatchers.IO
결과를 보시면 minusKey 가 호출된 myCoroutineContext는 구성 요소가 제거되지 않은 모습을 확인할 수 있습니다.