관리 메뉴

피터의 개발이야기

[kotlin] 코틀린 코루틴의 정석- CoroutineContext, 코루틴 실행환경 설정 본문

Programming/Kotlin

[kotlin] 코틀린 코루틴의 정석- CoroutineContext, 코루틴 실행환경 설정

기록하는 백앤드개발자 2024. 6. 2. 10:10
반응형

 

ㅁ 들어가며

 코틀린 코루틴의 정석 책을 보고 정리한 글입니다.

 

6장 CorotineContext

Coroutine Context는 코루틴을 실행하는 실행환경을 설정하고 관리하는 인터페이스로 CoroutineContext 객체는CoroutineDispatcher, CoroutineName, Job 등의 객체를 조합해 코루틴의 실행환경을 설정한다. 코루틴을 실행하고 관리하는 핵심적인 역할을 하며, 코루틴의 실행과 관련된 모든 설정은 CoroutineContext객체를 통해 이뤄진다.

 

- CoroutineContext 구성 요소
- CoroutineContext 구성 방법
- CoroutineContext 구성 접근
- CoroutineContext 구성 제거

 

 

ㅁ CoroutineContext 구성 요소

CoroutineContext는 코루틴의 실행환경을 설정 관리하며 아래의 구성요소 등의 객체를 조합해 코루틴 실행 환경을 정의한다. 

- CoroutineName : 코루틴의 이름 설정
- CoroutineDispatcher : 코루틴을 스레드로 보내어 실행
- Job : 코루틴을 조작
- CoroutineExceptionHandler: 코루틴 예외처리

ㅇ CorotineContext는 키-값 쌍으로 구성 요소를 관리하며, 동일한 키에 대해 중복된 값을 허용하지 않는다.

ㅇ 따라서 각 구성 요소를 한 개씩만 가질 수 있다.

 

ㅁ CoroutineContext 구성 방법

fun main() = runBlocking<Unit> {
  val coroutineContext: CoroutineContext = newSingleThreadContext("ThreadName") + CoroutineName("CoroutineName")

  launch(context = coroutineContext) {
    println("[${Thread.currentThread().name}] 더하기를 사용해 스레드이름과 코루틴 이름을 설정할 수 있다.")
  }
}
/*
[ThreadName @CoroutineName#2] 더하기를 사용해 스레드이름과 코루틴 이름을 설정할 수 있다.
*/

ㅇ 더하기 연산자(+)를 사용해 CoroutineContext의 구성요소를 조합할 수 있다.

 

fun main() = runBlocking<Unit> {
  val coroutineContext: CoroutineContext = newSingleThreadContext("ThreadName") + CoroutineName("CoroutineName")
  val newCoroutineContext: CoroutineContext = coroutineContext + CoroutineName("코루틴이름덮어씌우기")

  launch(context = newCoroutineContext) {
    println("[${Thread.currentThread().name}] 코루틴의 구성요소를 추가하는 경우 이전 값을 수정한다.")
  }
}
/*
[ThreadName @코루틴이름덮어씌우기#2] 코루틴의 구성요소를 추가하는 경우 이전 값을 수정한다.
*/

ㅇ 동일한 키를 가진 구성 요소가 여러 개 추가될 경우 나중에 추가된 구성요소가 이전 값을 덮어씌운다. 

 

fun main() = runBlocking<Unit> {
    val coroutineContext: CoroutineContext = newSingleThreadContext("ThreadName") + CoroutineName("CoroutineName")
    val coroutineContext2 = newSingleThreadContext("ThreadName2") + CoroutineName("CoroutineName2")
    val newCoroutineContext: CoroutineContext = coroutineContext + coroutineContext2

    launch(context = newCoroutineContext) {
        println("[${Thread.currentThread().name}] 코루틴의 여러 구성요소를 합칠 경우 나중 것으로 치환된다.")
    }
}
/*
[ThreadName2 @CoroutineName2#2] 코루틴의 여러 구성요소를 합칠 경우 나중 것으로 치환된다.
 */

ㅇ 코루틴의 여러 구성요소를 합칠 경우 나중 것으로 치환된다.

 

fun main() = runBlocking<Unit> {
    val addJob = Job()
    val newCoroutineContext = CoroutineName("newCoroutine") + Dispatchers.IO + addJob
}

ㅇ 코루틴의 이름과 Dispathcer, Job을 '+' 연산자로 설정할 수 있다.

 

ㅁ CoroutineContext 구성 접근

/**
 * User-specified name of coroutine. This name is used in debugging mode.
 * See [newCoroutineContext][CoroutineScope.newCoroutineContext] for the description of coroutine debugging facilities.
 */
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>

    /**
     * Returns a string representation of the object.
     */
    override fun toString(): String = "CoroutineName($name)"
}

ㅇ 구성요소의 동반 객체로 선언된 Key 프로퍼티를 사용해 키 값에 접근할 수 있다. 예를 들어 CoroutineName의 키 값은 CoroutineName, Key를 통해 접근할 수 있다. 

 

fun main() = runBlocking<Unit> {
    val addJob = Job()
    val newCoroutineContext = CoroutineName("newCoroutine") + Dispatchers.IO + addJob

    // Single tone Key
    println(newCoroutineContext[CoroutineName.Key]?.name)
    
    // get 함수
    println(newCoroutineContext.get(CoroutineName.Key)?.name)
    
    // CoroutineName 자체 참조
    println(newCoroutineContext[CoroutineName]?.name)
    
    // dispatcher 구성요서 key 프로퍼티 접근
    println(newCoroutineContext[dispatcher.key])
    
}
/*
newCoroutine
newCoroutine
newCoroutine
Dispatchers.IO
 */

ㅇ 키를 연산자 함수인 get과 함께 사용해 CoroutineContext 객체에 설정된 구성 요소에 접근할 수 있다. 

ㅇ CoroutineName, CoroutineDispatcher, Job, CoroutineExceptionHandler는 동반 객체인 key를 통해  coroutineContext.Key를 구현하기 때문에 그 자체를 키로 사용할 수 있다.

ㅇ newCoroutineContext[CoroutineName]은 coroutineContext[CoroutineName.Key]와 같은 연산을 한다.

 

ㅁ CoroutineContext 구성 제거

fun main() = runBlocking<Unit> {
    val newCoroutineContext = CoroutineName("newCoroutine") + Dispatchers.IO + Job()

    // minusKey로 해당 Context를 제거한 객체를 생성하여 반환한다.
    println(coroutineContext.minusKey(CoroutineName)[CoroutineName]?.name)
    // CoroutineName를 조회 시 그대로 남아 있다.
    println(newCoroutineContext[CoroutineName]?.name)

    println("------------")

    // minusKey로 dispatcher 제거
    println(coroutineContext.minusKey(Dispatchers.IO.key)[Dispatchers.IO.key])
    // get으로 CoroutineName를 조회 시 그대로 남아 있다.
    println(newCoroutineContext[Dispatchers.IO.key])

    println("------------")

    // Key
    println(coroutineContext.minusKey(Job).get(Job))
    println(newCoroutineContext[Job])
}
/*
null
newCoroutine
------------
null
Dispatchers.IO
------------
null
JobImpl{Active}@35d176f7
 */

ㅇ CoroutineContext의 miniusKey 함수를 사용하면 CoroutineContext에서 특정 구성 요소를 제거한 객체를 반환받을 수 있다.

ㅇ 그래서 기존 CoroutineContext는 제거가 되지 않는다.

 

ㅁ 함께 보면 좋은 사이트

 조세영 - 코틀린 코루틴의 정석 

반응형
Comments