CoroutineContext에 관하여

동키·2025년 4월 12일

Kotlin

목록 보기
8/10

해당 내용은 코틀린 코루틴의 정석 6 장을 토대로 공부한 내용입니다.

그동안 코루틴을 launchasync 로 생성하면서 우리는 context 인자에 CoroutineDispatcherCoroutineName 을 전달해 주었다.

전달해 줌으로서 코루틴의 이름을 설정하고 어느 스레드에 보낼지를 결정할 수 있었다.

그렇다면 CoroutineContext 가 뭐길래 이를 가능하게 해주는걸까?

CoroutineContext

코루틴을 실행하는 실행 환경을 설정하고 관리하는 인터페이스

즉, 코루틴의 실행 환경, 정보가 담겨있는 인터페이스다.

CoroutineContext 구성요소

CoroutineDispatcher , CoroutineName , Job , exceptionHandler 로 구성된다.

  • CoroutineName: 코루틴의 이름을 설정한다
  • CoroutineDispatcher: 코루틴을 스레드에 할당해 실행한다.
  • Job: 코루틴의 추상체로 코루틴을 조작하는 데 사용된다 (추적 및 제어)
  • CoroutineExceptionHandler: 코루틴에서 발생한 예외를 처리한다

CoroutineContext 구성하기

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 구성 요소 덮어씌우기

만약 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을 새 인자로 추가되었기 때문에 원래 있던 이름을 덮어 씌우게 됩니다.

즉, 나중에 들어온 값이 덮어씌웁니다!!.

CoroutineContext에 Job 생성해 추가하기

Job 객체는 기본적으로 launch나 runBlocking 같은 코루틴 빌더 함수를 통해 자동으로 생성되지만 Job() 을 호출해 생성할 수도 있습니다.

fun main() = runBlocking<Unit> {
    val job: Job = Job()
    val coroutineContext = CoroutineName("coroutine1") + Dispatchers.Default + job
}

하지만 Job 객체를 직접 생성해 추가하면 코루틴의 구조화 가 깨지게 됩니다.

때문에 새로운 Job 객체를 생성해 CoroutineContext에 추가하는 것은 주의 필요 합니다.

이에 대해서는 추후 구조화된 동시성 에서 다루겠습니다.


CoroutineContext 구성 요소에 접근하기

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 구성 요소에 접근하기

이번에는 해당 키를 사용해 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

이렇게 CoroutineNameDispatcher 을 가져와 봤습니다.

이런 경우 .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를 사용해 연산을 처리하기 때문에 이러한 결과를 얻을 수 있습니다.

구성 요소의 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.KeycoroutineName.key는 서로 같은 객체를 참조합니다.

코드를 통해 확인해보겠습니다.

fun main() = runBlocking<Unit> {
    val coroutineName = CoroutineName("MyCoroutine")
    val dispatcher = Dispatchers.IO

    println(coroutineName.key === CoroutineName.Key)
}
// true

이를 통해 서로 동일한 객체를 가리키는 것을 알 수 있습니다.


CoroutineContext 구성 요소 제거하기

위 내용에서 구성요소를 플러스(+) 연산자를 통해 구성 요소를 추가했씁니다.

이번에는 구성 요소를 제거하는 방법에 대해 알아보겠습니다.

minusKey 사용해 구성 요소 제거하기

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 만 출력되게 됩니다.

CoroutineNameminusKey 로 삭제했기 때문에 null이 출력된 모습을 볼 수 있습니다.

minusKey 함수 사용 시 주의할 점

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는 구성 요소가 제거되지 않은 모습을 확인할 수 있습니다.

profile
오늘 하루도 화이팅

0개의 댓글