반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[코루틴의 정석] 6장. CoroutineContext 본문

Kotlin/코루틴의 정석

[코루틴의 정석] 6장. CoroutineContext

어리둥절범고래 2024. 9. 1. 19:11
반응형

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 객체에서 특정 구성 요소를 제거한 객체를 반환받을 수 있다.

반응형