본문 바로가기
Android/Coroutine

[Android] 코루틴(Coroutine)의 Context란 ?

by 태크민 2024. 11. 9.

코루틴 컨텍스트(Coroutine Context)란?

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

CoroutineContext 객체는 CoroutineDispatcher, CoroutineName, Job 등의 객체를 조합하여 코루틴의 실행 환경을 설정합니다.

따라서, CoroutineContext 객체는 코루틴 실행 및 관리의 핵심 역할을 합니다.

CoroutineContext는 launch나 async 코루틴 빌더의 파라미터로 설정할 수 있으며, 해당 파라미터에 CoroutineContext를 명시적으로 지정하면 특정 스레드풀로 수행 제한, 코루틴 이름 설정 등을 할 수 있습니다.

 

CoroutineContext 구성 요소

  • CoroutineDispatcher
    • 코루틴 실행 시, 어떤 스레드에서 수행(특정 스레드로 수행 제한, 특정 스레드 풀에서 실행하도록 함)할지 결정합니다.
  • CoroutineName
    • 코루틴의 이름을 설정합니다. 디버깅할 때 유용함
  • CoroutineExceptionHandler
    • 코루틴에서 발생한 예외처리를 결정합니다.
  • Job
    • 코루틴에 대한 핸들러입니다. launch 또는 async에 의해 생성된 모든 코루틴에 대해 코루틴을 식별하고, 코루틴의 라이프 사이클을 관리합니다.

 

 

그렇다면 CoroutineContext는 어떤 코드로 구성되어 있을까요?

구현 코드를 살펴보겠습니다.

public interface CoroutineContext {
  /**
   * Returns the element with the given [key] from this context or `null`.
   * Keys are compared _by reference_, that is to get an element from the context the reference to its actual key
   * object must be presented to this function.
   */
  public operator fun <E : Element> get(key: Key<E>): E?
  /**
   * Accumulates entries of this context starting with [initial] value and applying [operation]
   * from left to right to current accumulator value and each element of this context.
   */
  public fun <R> fold(initial: R, operation: (R, Element) -> R): R
  /**
   * Returns a context containing elements from this context and elements from  other [context].
   * The elements from this context with the same key as in the other one are dropped.
   */
  public operator fun plus(context: CoroutineContext): CoroutineContext = ...impl...
  /**
   * Returns a context containing elements from this context, but without an element with
   * the specified [key]. Keys are compared _by reference_, that is to remove an element from the context
   * the reference to its actual key object must be presented to this function.
   */
  public fun minusKey(key: Key<*>): CoroutineContext
}

/**
 * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
 * Keys in the context are compared _by reference_.
 */
public interface Key<E : Element>

/**
 * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
 */
public interface Element : CoroutineContext {
  /**
   * A key of this coroutine context element.
   */
  public val key: Key<*>
  
  ...overrides...
}

 

위 코드는 CoroutineContext.kt 파일에 있는 코드 입니다. CoroutineContext 는 4가지 메서드를 가지고 있는데 그 역할은 다음과 같습니다.

 

1. get() : 연산자(operator) 함수로써 주어진 key 에 해당하는 컨텍스트 요소를 반환합니다.

fun main() {
    val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
        println("예외 발생: ${throwable.message}")
    }
    val context: CoroutineContext = Dispatchers.IO + CoroutineName("ExampleCoroutine") + Job() + exceptionHandler

	//이름 요소 가져오기
    val coroutineName = context[CoroutineName] // 특정 키를 가진 요소 가져오기
    println("코루틴 이름: ${coroutineName?.name}") // 코루틴 이름 출력
    
    //job 요소 가져오기
    val job = context[Job] // Job 요소 가져오기
    println("Job 상태: ${job?.isActive}") // Job이 활성 상태인지 확인
    
    //디스패쳐 가져오기
    val dispatcher = context[CoroutineDispatcher] // 디스패처 가져오기
    println("코루틴 디스패처: $dispatcher")
    
    //CoroutineExceptionHandler 가져오기
    val handler = context[CoroutineExceptionHandler] // 예외 처리 핸들러 가져오기
    println("CoroutineExceptionHandler 존재 여부: ${handler != null}")   
}

//코루틴 이름: ExampleCoroutine
//Job 상태: true
//코루틴 디스패처: kotlinx.coroutines.scheduling.IOParallelDispatcher@xxxxxxxx
//CoroutineExceptionHandler 존재 여부: true


2. fold() : 초기값(initialValue)을 시작으로 제공된 병합 함수를 이용하여 대상 컨텍스트 요소들을 병합한 후 결과를 반환합니다.
예를들어 초기값을 0을 주고 특정 컨텍스트 요소들만 찾는 병합 함수(filter 역할)를 주면 찾은 개수를 반환할 수 있고,

초기값을 EmptyCoroutineContext 를 주고 특정 컨텍스트 요소들만 찾아 추가하는 함수를 주면 해당 요소들만드로 구성된 코루틴 컨텍스트를 만들 수 있습니다.

fun main() {
    val context: CoroutineContext = Dispatchers.IO + CoroutineName("ExampleCoroutine") + Job()

    val count = context.fold(0) { acc, _ -> acc + 1 } // 요소 개수 카운트
    println("컨텍스트 요소 개수: $count")
}

//컨텍스트 요소 개수: 3
fun main() {
    val context: CoroutineContext =
        Dispatchers.Default + CoroutineName("FilteredCoroutine") + Job()

    // CoroutineName 요소만 포함된 새로운 컨텍스트 만들기
    val filteredContext = context.fold(EmptyCoroutineContext) { acc, element ->
        if (element is CoroutineName) acc + element else acc
    }

    println("필터링된 컨텍스트: ${filteredContext[CoroutineName]}")
}
//필터링된 컨텍스트: CoroutineName(FilteredCoroutine)

 

3. plus() : 현재 컨텍스트와 파라미터로 주어진 다른 컨텍스트가 갖는 요소들을 모두 포함하는 컨텍스트를 반환합니다. 현재 컨텍스트 요소 중 파라미터로 주어진 요소에 이미 존재하는 요소(중복)는 버려집니다.

fun main() {
    val mergedContext1: CoroutineContext = Dispatchers.IO + CoroutineName("FirstCoroutine")
    val mergedContext2: CoroutineContext = Job() + CoroutineName("SecondCoroutine")

    val mergedContext3 = mergedContext1 + mergedContext2 // 두 컨텍스트 합치기
    
    println("코루틴 이름1: ${mergedContext1[CoroutineName]?.name}")
    println("코루틴 이름2: ${mergedContext2[CoroutineName]?.name}")
    println("코루틴 이름3: ${mergedContext3[CoroutineName]?.name}")   
}

//코루틴 이름1: FirstCoroutine
//코루틴 이름2: SecondCoroutine
//코루틴 이름3: SecondCoroutine

 

4. minusKey() : 현재 컨텍스트에서 주어진 키를 갖는 요소들을 제외한 새로운 컨텍스트를 반환합니다.

fun main() {
    val context: CoroutineContext = Dispatchers.IO + CoroutineName("ExampleCoroutine") + Job()

    val newContext = context.minusKey(CoroutineName) // CoroutineName 제거
    println("제거 후 CoroutineName 존재 여부: ${newContext[CoroutineName]}")
}

//제거 후 CoroutineName 존재 여부: null

 

 

그리고 CoroutineContext에서는 Key 에 대한 인터페이스 정의가 있는데 Key 는 Element 타입을 제네릭 타입으로 가져야 합니다.

여기서 Element는 CoroutineContext를 상속하며 CoroutineId, Job, CoroutineName, CroutineDispatcher, CorotuineExceptionHandler 등을 들 수 있습니다.

이런 요소(element)들은 각각의 Key를 기반으로 CoroutineContext에 등록됩니다.

 

요약하자면 코루틴 컨텍스트(CoroutineContext)에는 코루틴 컨텍스트를 상속한 요소(Element) 들이 등록될 수 있고, 각 요소들이 등록 될때는 요소의 고유한 키를 기반으로 등록된다는 것입니다.

 

CoroutineContext 는 인터페이스로써 이를 구현한 구현체로는 다음과같은 3가지 종류가 있습니다.

  • EmptyCoroutineContext: 특별히 컨텍스트가 명시되지 않을 경우 이 singleton 객체가 사용됩니다.
    이는 GlobalScope에 사용된 Context로, GlobalScope == launc{ }는 같은 싱글톤 EmptyContext를 사용하는 의미입니다.
  • CombinedContext: 두개 이상의 컨텍스트가 명시되면 컨텍스트 간 연결을 위한 컨테이너역할을 하는 컨텍스트 입니다.
     * 명시란? - launch(Dispatchers.Main + job) 이렇게 코루틴 생성 시 프로퍼티를 설정한다는 의미
  • Element: 컨텍트스의 각 요소들도 CoroutineContext 를 구현합니다.
    * launch(), launch(Dispatcher.IO), launch(CoroutineName("example)) 등이 있습니다.

위 이미지는 우리가 GlobalScope.launch{} 를 수행할 때 launch 함수의 첫번째 파라미터인 CoroutineContext 에 어떤 값을 넘기는지에 따라서 변화되어 가는 코루틴 컨텍스트의 상태를 보여줍니다.

각각의 요소를 + 연산자를 이용해 연결하고 있는데 이는 앞서 설명한 것처럼CoroutineContext 가 plus 연산자를 구현하고 있기 때문입니다. Element + Element + … 는 결국 하나로 병합 된 CoroutineContext (e.g. CombinedContext)를 만들어냅니다.

 

주황색 테두리는 CombinedContext 로서 CoroutineContext 와 Element 를 묶어 하나의 CoroutineContext 가 되는 개념입니다. 그리고 내부 코드에서 ContinuationInterceptor 는 이 병합 작업이 일어날 때 항상 마지막에 위치하도록 고정되는데 이는 인터셉터로의 빠른 접근을 위해서라고 커멘트 되어 있습니다.

 


참고자료

https://earth-95.tistory.com/m/198

 

[Kotlin Coroutine] 코루틴 컨텍스트와 디스패처의 이해

목표코루틴 컨텍스트와 디스패처의 역할 및 중요성 학습다양한 디스패처(Main, IO, Default) 사용 시나리오 실습컨텍스트 전환과 예외 처리 방법 탐구CoroutineContextcoroutineContext는 코루틴을 실행하는

earth-95.tistory.com

https://android-devpia.tistory.com/10

 

코틀린 코루틴에서의 취소 및 예외 처리 #1 - CoroutineScope, Job, CoroutineContext

이 포스팅은 아래 게시글을 번역 및 일부 수정하여 작성하였습니다. https://medium.com/androiddevelopers/coroutines-first-things-first-e6187bf3bb21 이 일련의 포스팅은 코루틴의 취소와 예외에 대하여 자세히 설

android-devpia.tistory.com

https://myungpyo.medium.com/reading-coroutine-official-guide-thoroughly-part-1-7ebb70a51910

 

코루틴 공식 가이드 읽고 분석하기- Part 1 — Dive1

CoroutineContext와 CoroutineScope 란 무엇인가?

myungpyo.medium.com

https://jaejong.tistory.com/62

 

[Kotlin] 코루틴 #2 - CoroutineContext와 CoroutineScope란?

코루틴 #2 - CoroutineContext와 CoroutineScope란? CoroutineContext와 CoroutineScope란 무엇인가? Coroutine 이전 글 코루틴 #1 - 기본 CoroutineContext Coroutine(코루틴)을 어떻게 처리할 것인지에 대한 여러가지 정보(Elemen

jaejong.tistory.com