반응형
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
관리 메뉴

안드로이드 개발자 노트

[코틀린 코루틴] 7장. 코루틴 컨텍스트 본문

Kotlin/코틀린 코루틴

[코틀린 코루틴] 7장. 코루틴 컨텍스트

어리둥절범고래 2024. 3. 24. 19:16
반응형

코루틴 빌더의 정의를 보면 첫 번째 파라미터가 CoroutineContext인 것을 볼 수 있습니다.

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    ...
}

 

코루틴 빌더의 리시버인 CoroutineScope의 정의는 다음과 같습니다.

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

코루틴 스코프는 코루틴 컨텍스트를 감싸는 래퍼 형태인 것을 볼 수 있습니다.

Continuation의 정의도 살펴보겠습니다.

public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

코틀린 코루틴에서 중요한 요소들의 정의를 살펴보았습니다.

공통적으로 CoroutineContext를 사용하고 있는 것을 알 수 있습니다.

 

코루틴 컨텍스트는 원소나 원소들의 집합을 나타내는 인터페이스입니다.

코루틴 컨텍스트는 Job, CoroutineName, CoroutineDispatcher와 같은 Element 객체들이 인덱싱된 집합이며, 컬렉션과 개념이 비슷합니다.

Job과 CoroutineName, CoroutineDispatcher는 Element를 상속받으며, Element는 CoroutineContext를 상속받는 구조입니다.

public interface Job : CoroutineContext.Element {
    ...
}
public interface CoroutineContext {
    public operator fun <E : Element> get(key: Key<E>): E?
    public interface Key<E : Element>
    public interface Element : CoroutineContext {
        ...
    }
}

 

컨텍스트에서 모든 원소는 Key를 가지고 있으며, 각 키는 주소로 비교할 수 있습니다.

예를 들어 CoroutineName이나 Job은 CoroutineContext 인터페이스를 구현한 CoroutineContext.Element를 구현합니다.

fun main() {
    val name: CoroutineName = CoroutineName("A name")
    val element: CoroutineContext.Element = name
    val context: CoroutineContext = element
    
    val job: Job = Job()
    val jobElement: CoroutineContext.Element = job
    val jobContext: CoroutineContext = jobElement
}

 

 

CoroutineContext에서 원소 찾기

 

코루틴 컨텍스트는 컬렉션과 비슷하기 때문에 get을 이용해 원소를 찾을 수 있습니다.

원소가 컨텍스트에 있으면 반환된다는 점에서 Map과 비슷합니다.

원소가 없으면 null이 대신 반환됩니다.

fun main() {
    val ctx: CoroutineContext = CoroutineName("A name")

    val coroutineName: CoroutineName? = ctx[CoroutineName] // ctx.get(CoroutineName)
    println(coroutineName?.name) // A name
    val job: Job? = ctx[Job]
    println(job) // null
}

CoroutineName은 타입이나 클래스가 아닌 컴패니언 객체입니다.

클래스의 이름이 컴패니언 객체에 대한 참조로 사용되는 코틀린 언어의 특징 때문에, ctx[CoroutineName]은 ctx[CoroutineName.key]가 됩니다.

public data class CoroutineName(
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {

    public companion object Key : CoroutineContext.Key<CoroutineName>

    override fun toString(): String = "CoroutineName($name)"
}

컴패니언 객체를 키로 사용하여 원소를 찾는 경우는 일반적이며, 키(CoroutineName과 같은)는 클래스나 같은 키를 사용하는 클래스가 구현한 인터페이스를 가리킵니다.

 

 

 

컨텍스트 더하기

 

코루틴 컨텍스트의 유용한 기능은 두 개의 코루틴 컨텍스트를 합쳐 하나의 코루틴 컨텍스트로 만들 수 있는 것입니다.

다른 키를 가진 두 원소를 더하면 만들어진 컨텍스트는 두 가지 키를 모두 가집니다.

fun main() {
    val ctx1: CoroutineContext = CoroutineName("Name1")
    println(ctx1[CoroutineName]?.name) // Name1
    println(ctx1[Job]?.isActive) // null
    
    val ctx2: CoroutineContext = Job()
    println(ctx2[CoroutineName]?.name) // null
    println(ctx2[Job]?.isActive) // true

    val ctx3 = ctx1 + ctx2
    println(ctx3[CoroutineName]?.name) // Name1
    println(ctx3[Job]?.isActive) // true
}

코루틴 컨텍스트에 같은 키를 가진 또 다른 원소가 더해지면 맵처럼 새로운 원소가 기존 원소를 대체합니다.

fun main() {
    val ctx1: CoroutineContext = CoroutineName("Name1")
    println(ctx1[CoroutineName]?.name) // Name1

    val ctx2: CoroutineContext = CoroutineName("Name2")
    println(ctx2[CoroutineName]?.name) // Name2

    val ctx3 = ctx1 + ctx2
    println(ctx3[CoroutineName]?.name) // Name2
}

 

 

 

비어 있는 코루틴 컨텍스트

 

코루틴 컨텍스트는 컬렉션이므로 빈 컨텍스트 또한 만들 수 있습니다.

빈 컨텍스트는 원소가 없으므로, 다른 컨텍스트에 더해도 아무런 변화가 없습니다.

fun main() {
    val empty: CoroutineContext = EmptyCoroutineContext
    println(empty[CoroutineName]) // null
    println(empty[Job]) // null

    val ctxName = empty + CoroutineName("Name1") + empty
    println(ctxName[CoroutineName]) // CoroutineName(Name1)
}

 

 

 

원소 제거

 

minusKey 함수에 키를 넣는 방식으로 원소를 컨텍스트에서 제거할 수도 있습니다.

fun main() {
    val ctx1: CoroutineContext = CoroutineName("Name1") + Job()
    println(ctx1[CoroutineName]?.name) // Name1
    println(ctx1[Job]?.isActive) // true

    val ctx2 = ctx1.minusKey(CoroutineName)
    println(ctx2[CoroutineName]?.name) // null
    println(ctx2[Job]?.isActive) // true

    val ctx3 = (ctx1 + CoroutineName("Name2")).minusKey(CoroutineName)
    println(ctx3[CoroutineName]?.name) // null
    println(ctx3[Job]?.isActive) // true
}

 

 

 

컨텍스트 폴딩

 

컨텍스트의 각 원소에 조작해야 하는 경우 다른 컬렉션의 fold와 유사한 fold 메서드를 사용할 수 있습니다.

fold는 다음을 필요로 합니다.

 

  • 누산기의 첫 번째 값
  • 누산기의 현재 상태와 현재 실행되고 있는 원소로 누산기의 다음 상태를 계산할 연산
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
fun main() {
    val ctx: CoroutineContext = CoroutineName("Name1") + Job()

    ctx.fold("") { acc, element -> "$acc$element"}
        .also(::println)
    // CoroutineName(Name1)JobImpl{Active}@37a71e93

    val empty = emptyList<CoroutineContext>()
    ctx.fold(empty) { acc, element -> acc + element }
        .joinToString()
        .also(::println)
    // CoroutineName(Name1), JobImpl{Active}@37a71e93
}

 

 

 

코루틴 컨텍스트와 빌더

 

코루틴 컨텍스트는 코루틴의 데이터를 저장하고 전달하는 방법입니다.

부모-자식 관계의 영향 중 하나로 부모는 기본적으로 컨텍스트를 자식에게 전달합니다.

모든 자식은 빌더의 인자에서 정의된 부모의 컨텍스트로 대체합니다.

fun CoroutineScope.log(msg: String) {
    val name = coroutineContext[CoroutineName]?.name
    println("[$name] $msg")
}

fun main() = runBlocking(CoroutineName("main")) {
    log("Started") // [main] Started
    val v1 = async(CoroutineName("c1")) {
        delay(500)
        log("Running async") // [c1] Running async
        42
    }
    launch(CoroutineName("c2")) { 
        delay(1000)
        log("Running launch") // [c2] Running launch  
    }
    log("The answer is ${v1.await()}") // [main] The answer is 42
}

 

 

 

중단 함수에서 컨텍스트에 접근하기

 

중단 함수에서 부모의 컨텍스트에 접근하는 것이 가능합니다.

이것이 가능한 이유는 맨 위에서 본 정의들처럼, 다음과 같습니다.

 

  • 코루틴 빌더의 인자에 coroutineContext가 있다.
  • CoroutineScope에 coroutineContext 프로퍼티가 있다.
  • Continuation 객체에 coroutineContext 프로퍼티가 있다.

 

따라서 중단 함수에서 부모의 컨텍스트에 접근하는 것이 가능합니다.

coroutineContext 프로퍼티는 모든 중단 스코프에서 사용이 가능하며, 이를 통해 컨텍스트에 접근할 수 있습니다.

suspend fun printName() {
    println(coroutineContext[CoroutineName]?.name)
}

suspend fun main() = withContext(CoroutineName("Outer")) {
    printName() // Outer
    launch(CoroutineName("Inner")) {
        printName() // Inner
    }
    delay(10)
    printName() // Outer
}

 

 

 

컨텍스트를 개별적으로 생성하기

 

커스텀 코루틴 컨텍스트를 만드는 방법은 CoroutineContext.Element 인터페이스를 구현하는 클래스를 만드는 것입니다.

이러한 클래스는 컨텍스트를 식별할 수 있는 key를 구현해줘야 하는데, 일반적으로 클래스의 컴패니언 객체를 키로 사용하면 됩니다.

class MyCoroutineContext : CoroutineContext.Element {
    
    override val key: CoroutineContext.Key<*> = Key
    
    companion object Key: CoroutineContext.Key<MyCoroutineContext>
}

이렇게 만들어진 컨텍스트 또한, 부모에서 자식으로 전달되며 같은 키를 가진 또 다른 컨텍스트로 대체할 수 있습니다.

예시로, 연속된 숫자를 출력하는 커스텀 컨텍스트를 보겠습니다.

class CounterContext(
    private val name: String
) : CoroutineContext.Element {
    override val key: CoroutineContext.Key<*> = Key
    private var nextNumber = 0

    suspend fun printNext() {
        println("$name: $nextNumber")
        nextNumber++
    }

    companion object Key: CoroutineContext.Key<CounterContext>
}

suspend fun printNext() {
    coroutineContext[CounterContext]?.printNext()
}

suspend fun main(): Unit =
    withContext(CounterContext("Outer")) {
        printNext() // Outer: 0
        launch {
            printNext() // Outer: 1
            launch {
                printNext() // Outer: 2
            }
            launch(CounterContext("Inner")) {
                printNext() // Inner: 0
                printNext() // Inner: 1
                launch {
                    printNext() // Inner: 2
                }
            }
        }
        printNext() // Outer: 3
    }

 

 


요약

 

  • CoroutineContext는 Map이나 Set과 같은 컬렉션이랑 개념적으로 비슷하다.
  • CoroutineContext는 Element 인터페이스의 인덱싱된 집합이며, Element 또한 CoroutineContext이다.
  • CoroutineContext는 코루틴에 관련된 정보를 객체로 그룹화하고 전달하는 방법이다.
  • CoroutineContext는 코루틴에 저장되며, CoroutineContext를 사용해 코루틴의 상태가 어떤지 확인할 수 있다.
반응형