안드로이드 개발자 노트
[코루틴의 정석] 6장. CoroutineContext 본문
CoroutineContext는 코루틴을 실행하는실행 환경을 설정하고 관리하는 인터페이스로 CoroutineContext 객체는 CoroutineDispatcher. CoroutineName, Job 등의 객체를 조합해 코루틴의 실행 환경을 설정하는 등 핵심적인 역할을 한다.
6.1. CoroutineContext의 구성요소
CoroutineContext 객체는 CoroutineName, CoroutineDispatcher, Job, CoroutineExceptionHandler의 네 가지 구성 요소를 가진다.
- CoroutineName: 코루틴의 이름을 설정한다.
- CoroutineDispatcher: 코루틴을 스레드에 할당해 실행한다.
- Job: 코루틴의 추상체로 코루틴을 조작하는 데 사용된다.
- CoroutineExceptionHandler: 코루틴에서 발생한 예외를 처리한다.
6.2. CoroutineContext 구성하기
6.2.1. CoroutineContext가 구성 요소를 관리하는 방법
CoroutineContext 객체는 키-값 쌍으로 각 구성 요소를 관리한다.
키 | 값 |
CoroutineName 키 | CoroutineName 객체 |
CoroutineDispatcher 키 | CoroutineDispatcher 객체 |
Job 키 | Job 객체 |
CoroutineExceptionHandler 키 | CoroutineExceptionHandler 객체 |
6.2.2. CoroutineContext 구성
CoroutineContext 객체에 구성 요소를 추가하는 방법으로, CorotineContext 객체 간에 더하기 연산자(+)를 사용할 수 있다.
fun main() = runBlocking<Unit> {
val coroutineContext: CoroutineContext = newSingleThreadContext("MyThread") + CoroutineName("MyCoroutine")
launch(context = coroutineContext) {
println("[${Thread.currentThread().name}] 실행")
}
}
/*
// 결과:
[MyThread @MyCoroutine#2] 실행
*/
이렇게 만들어진 CoroutineContext 객체는 아래와 같다.
키 | 값 |
CoroutineName 키 | CoroutineName("MyCoroutine") |
CoroutineDispatcher 키 | newSingleThreadContext("MyThread") |
Job 키 | 설정되지 않음 |
CoroutineExceptionHandler 키 | 설정되지 않음 |
6.2.3. CoroutineContext 구성 요소 덮어씌우기
CoroutineContext 객체에 같은 구성 요소가 둘 이상 더해진다면 나중에 추가된 CoroutineContext 구성 요소가 이전 값을 덮어씌운다.
fun main() = runBlocking<Unit> {
val coroutineContext: CoroutineContext = newSingleThreadContext("MyThread") + CoroutineName("MyCoroutine")
val newCoroutineContext: CoroutineContext = coroutineContext + CoroutineName("NewCoroutine")
launch(context = newCoroutineContext) {
println("[${Thread.currentThread().name}] 실행")
}
}
/*
// 결과:
[MyThread @NewCoroutine#2] 실행
*/
CoroutineContext 객체가 합쳐질 때 동일한 키를 가진 구성요소는 나중에 들어온 값이 선택된다.
6.2.5. CoroutineContext에 Job 생성해 추가하기
Job 객체는 코루틴 빌더 함수를 통해 자동으로 생성되지만 Job() 을 호출해 생성할 수도 있다.
val myJob = Job()
val myCoroutineContext: CoroutineContext = Dispatchers.IO + myJob
myCoroutineContext는 다음과 같은 구성 요소를 가진다.
키 | 값 |
CoroutineName 키 | 설정되지 않음 |
CoroutineDispatcher 키 | Dispatchers.IO |
Job 키 | myJob |
CoroutineExceptionHandler 키 | 설정되지 않음 |
6.3. CoroutineContext 구성 요소에 접근하기
6.3.1. CoroutineContext 구성 요소의 키
CoroutineContext 객체의 각 구성 요소에 접근할 수 있는 고유한 키가 있다.
이 Key는 일반적으로 CoroutineContext 자신의 내부에 CoroutineContext.Key 인터페이스를 구현한 싱글톤 객체로 구현한다.
public interface Job : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<Job>
...
}
public data class CoroutineName(
) : AbstractCoroutineContextElement(CoroutineName) {
public companion object Key : CoroutineContext.Key<CoroutineName>
...
}
코루틴 구성 요소의 키는 고유하며, 모두 같은 키를 반환한다.
fun main() {
val dispatcherKey1 = Dispatchers.IO.key
val dispatcherKey2 = Dispatchers.Default.key
println("dispatcherKey1과 dispatcherKey2는 동일한가 >> ${dispatcherKey1 === dispatcherKey2}")
}
/*
// 결과:
dispatcherKey1과 dispatcherKey2는 동일한가 >> true
*/
키를 사용해 CoroutineContext 구성 요소에 접근하는 방법은 다음과 같다.
- 싱글톤 키를 사용해 CoroutineContext 구성 요소에 접근하기
fun main() = runBlocking<Unit> {
val coroutineContext = CoroutineName("MyCoroutine") + Dispatchers.IO
val nameFromContext = coroutineContext[CoroutineName.Key]
println(nameFromContext)
}
/*
// 결과:
CoroutineName(MyCoroutine)
*/
- 구성 요소 자체를 키로 사용해 CoroutineContext 구성 요소에 접근하기
fun main() = runBlocking<Unit> {
val coroutineContext = CoroutineName("MyCoroutine") + Dispatchers.IO
val nameFromContext = coroutineContext[CoroutineName] // '.Key'제거
println(nameFromContext)
}
/*
// 결과:
CoroutineName(MyCoroutine)
*/
- 구성 요소의 key 프로퍼티를 사용해 CoroutineContext 구성 요소에 접근하기
fun main() = runBlocking<Unit> {
val coroutineName : CoroutineName = CoroutineName("MyCoroutine")
val dispatcher : CoroutineDispatcher = Dispatchers.IO
val coroutineContext = coroutineName + dispatcher
println(coroutineContext[coroutineName.key]) // CoroutineName("MyCoroutine")
println(coroutineContext[dispatcher.key]) // Dispatchers.IO
}
/*
// 결과:
CoroutineName(MyCoroutine)
Dispatchers.IO
*/
중요한 점은 구성 요소의 key 프로퍼티와 Companion object로 선언된 Key는 동일한 객체를 가리킨다는 것이다.
fun main() = runBlocking<Unit> {
val coroutineName: CoroutineName = CoroutineName("MyCoroutine")
if (coroutineName.key === CoroutineName.Key) {
println("coroutineName.key와 CoroutineName.Key 동일합니다")
}
}
/*
// 결과:
coroutineName.key와 CoroutineName.Key 동일합니다
*/
6.4. CoroutineContext 구성 요소 제거하기
6.4.1. minusKey 사용해 구성 요소 제거하기
CoroutineContext 객체는 구성 요소를 제거하기 위한 minusKey 함수를 제공한다.
예를 들어, 다음과 같은 coroutineContext가 있다.
키 | 값 |
CoroutineName 키 | CoroutineName("MyCoroutine") |
CoroutineDispatcher 키 | Dispatchers.IO |
Job 키 | myJob |
val deleteCoroutineContext = coroutineContext.minusKey(CoroutineName)
deleteCoroutineContext는 다음과 같다.
키 | 값 |
CoroutineName 키 | 설정되지 않음 |
CoroutineDispatcher 키 | Dispatchers.IO |
Job 키 | myJob |
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
JobImpl{Active}@65e2dbf3
*/
설정되지 않은 CoroutineName은 null로 표현되는 것을 확인할 수 있다.
6.4.2. minusKey 함수 사용 시 주의할 점
minusKey 함수 사용 시 주의할 점은 minusKey를 호출한 CoroutineContext 객체는 그대로 유지되고, 구성 요소가 제거된 새로운 CoroutineContext 객체가 반환된다는 것이다.
fun main() = runBlocking<Unit> {
val coroutineName = CoroutineName("MyCoroutine")
val dispatcher = Dispatchers.IO
val job = Job()
val coroutineContext: CoroutineContext = coroutineName + dispatcher + job
val deletedCoroutineContext = coroutineContext.minusKey(CoroutineName)
println(coroutineContext[CoroutineName])
println(coroutineContext[CoroutineDispatcher])
println(coroutineContext[Job])
}
/*
// 결과:
CoroutineName(MyCoroutine)
Dispatchers.IO
JobImpl{Active}@65e2dbf3
*/
요약
1. CoroutineContext 객체는 코루틴의 실행 환경을 설정하고 관리하는 객체로 CoroutineDispatcher, CoroutineName, Job, CoroutineExceptionHandler 등의 객체를 조합해 코루틴 실행 환경을 정의한다.
2. CoroutineContext의 네 가지 주요한 구성 요소는 코루틴의 이름을 설정하는 CoroutineName 객체, 코루틴을 스레드로 보내 실행하는 CoroutineDispatcher 객체, 코루틴을 조작하는데 사용하는 Job 객체, 코루틴의 예외를 처리하는 CoroutineExceptionHandler 객체이다.
3. CoroutineContext 객체는 키-값 쌍으로 구성 요소를 관리하며, 동일한 키에 대해 중복된 값을 허용하지 않는다. 따라서 각 구성 요소를 한 개씩만 가질 수 있다.
4. 더하기 연산자(+)를 사용해 CoroutineContext의 구성 요소를 조합할 수 있다.
5. 동일한 키를 가진 구성 요소가 여러 개 추가될 경우 나중에 추가된 구성 요소가 이전 값을 덮어씨운다. 즉, 마지막에 추가된 구성 요소만 유효하다.
6. 일반적으로 구성 요소의 동반 객체로 선언된 key 프로퍼티를 사용해 키 값에 접근할 수 있다. 예를 들어 CoroutineName의 키 값은 CoroutineName.Key를 통해 접근할 수 있다.
7. 키를 연산자 함수인 get과 함께 사용해 CoroutineContext 객체에 설정된 구성 요소에 접근할 수 있다. 예를 들어 CoroutineContext 객체인 coroutineContext의 CoroutineName 구성 요소에 접근하고 싶다면 coroutineContext.get(CoroutineName.Key)와 같이 사용하면 된다.
8. get 연산자 함수는 대괄호([])로 대체할 수 있다. 따라서 앞의 coroutineContext.get(CoroutineName.Key)를 coroutineContext[CoroutineName.Key]로 대체할 수 있다.
9. CoroutineName, CoroutineDispatcher, Job, CoroutineException Handler는 동반 객체인 Key를 통해 CoroutineContext.Key를 구현하기 때문에 그 자체를 키로 사용할 수 있다. 따라서 coroutineContext[CoroutineName]은 coroutineContext[CoroutineName.Key]와 같은 연산을 한다.
10. CoroutineContext 객체의 minusKey 함수를 사용하면 CoroutineContext 객체에서 특정 구성 요소를 제거한 객체를 반환받을 수 있다.
'Kotlin > 코루틴의 정석' 카테고리의 다른 글
[코루틴의 정석] 7장. 구조화된 동시성 (0) | 2024.09.08 |
---|---|
[코루틴의 정석] 5장. async와 Deferred (0) | 2024.09.01 |
[코루틴의 정석] 4장. 코루틴 빌더와 Job (0) | 2024.08.24 |