Android/Coroutine

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

태크민 2024. 11. 9. 00:07

코루틴 컨텍스트(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