코루틴 컨텍스트와 디스패처

코루틴 컨텍스트와 디스패처 (Coroutine context and dispatchers)

코루틴은 항상 어떤 컨텍스트에서 실행돼요. 이 컨텍스트는 Kotlin 표준 라이브러리에 정의된 CoroutineContext 타입의 값으로 표현돼요.

코루틴 컨텍스트는 여러 요소의 집합이에요. 주요 요소로는 우리가 앞에서 본 코루틴의 Job과, 이 섹션에서 다룰 디스패처(dispatcher)가 있어요.

출처: Kotlin 공식 문서

본문

디스패처와 스레드 (Dispatchers and threads)

코루틴 컨텍스트에는 코루틴 디스패처(CoroutineDispatcher 참고)가 포함돼요. 디스패처는 해당 코루틴이 실행에 어떤 스레드(들)를 사용할지 결정해요. 코루틴 디스패처는 코루틴 실행을 특정 스레드에 한정(confine)할 수도, 스레드 풀로 보낼 수도, 제한 없이(unconfined) 실행하게 둘 수도 있어요.

launchasync 같은 모든 코루틴 빌더는 선택적인 CoroutineContext 파라미터를 받아요. 이걸로 새 코루틴의 디스패처와 다른 컨텍스트 요소를 명시적으로 지정할 수 있어요.

다음 예제를 실행해 볼게요.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
    launch { // 부모(메인 runBlocking 코루틴)의 컨텍스트
        println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { // 한정되지 않음 -- 메인 스레드에서 작업할 거예요
        println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // DefaultDispatcher로 보내질 거예요 
        println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // 자신만의 새 스레드를 가질 거예요
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }
//sampleEnd    
}

전체 코드는 여기에서 받을 수 있어요.

출력은 이렇게 나와요(순서는 다를 수 있어요).

Unconfined            : I'm working in thread main
Default               : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking      : I'm working in thread main

launch { ... }를 파라미터 없이 쓰면, launch가 실행되는 CoroutineScope에서 컨텍스트(따라서 디스패처)를 상속받아요. 이 경우에는 main 스레드에서 실행되는 메인 runBlocking 코루틴의 컨텍스트를 상속받아요.

Dispatchers.Unconfined는 특별한 디스패처예요. 겉보기에는 main 스레드에서 실행되는 것처럼 보이지만, 사실은 나중에 설명할 다른 메커니즘이에요.

스코프에서 다른 디스패처가 명시적으로 지정되지 않으면 기본 디스패처가 사용돼요. Dispatchers.Default로 표현되고, 공유 배경 스레드 풀을 사용해요.

newSingleThreadContext는 코루틴이 실행될 스레드를 만들어요. 전용 스레드는 매우 비싼 자원이에요. 실제 애플리케이션에서는 더 이상 필요 없을 때 close 함수로 해제하거나, 최상위 변수에 저장해 두고 애플리케이션 전체에서 재사용해야 해요.

한정되지 않은 디스패처 vs 한정된 디스패처

Dispatchers.Unconfined 코루틴 디스패처는 호출한 스레드에서 코루틴을 시작하지만, 첫 번째 중단 지점까지만이에요. 중단 후에는 호출된 중단 함수에 의해 완전히 결정되는 스레드에서 코루틴을 재개해요. 한정되지 않은 디스패처는 CPU 시간을 소비하지 않고, 특정 스레드에 한정된 공유 데이터(예: UI)도 갱신하지 않는 코루틴에 적합해요.

반면 디스패처는 기본적으로 바깥 CoroutineScope에서 상속돼요. 특히 runBlocking 코루틴의 기본 디스패처는 호출한 스레드에 한정돼 있어서, 이를 상속하면 예측 가능한 FIFO 스케줄링으로 실행이 그 스레드에 한정되는 효과가 있어요.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
    launch(Dispatchers.Unconfined) { // 한정되지 않음 -- 메인 스레드에서 작업할 거예요
        println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
        delay(500)
        println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
    }
    launch { // 부모(메인 runBlocking 코루틴)의 컨텍스트
        println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
        delay(1000)
        println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
    }
//sampleEnd    
}

전체 코드는 여기에서 받을 수 있어요.

출력은 이래요.

Unconfined      : I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined      : After delay in thread kotlinx.coroutines.DefaultExecutor
main runBlocking: After delay in thread main

runBlocking { ... }에서 상속받은 컨텍스트를 가진 코루틴은 main 스레드에서 계속 실행되는 반면, 한정되지 않은 코루틴은 delay 함수가 사용하는 기본 executor 스레드에서 재개돼요.

한정되지 않은 디스패처는 고급 메커니즘이에요. 코루틴 내부의 어떤 연산이 즉시 수행되어야 해서, 나중에 실행하도록 디스패치하는 게 필요 없거나 바람직하지 않은 특정 모서리(corner case)에서 도움이 될 수 있어요. 일반 코드에서 한정되지 않은 디스패처는 쓰지 않는 게 좋아요.

코루틴과 스레드 디버깅 (Debugging coroutines and threads)

코루틴은 한 스레드에서 중단되고 다른 스레드에서 재개될 수 있어요. 단일 스레드 디스패처를 쓰더라도 특별한 도구가 없다면 코루틴이 무엇을, 어디서, 언제 하고 있었는지 파악하기 어려울 수 있어요.

IDEA로 디버깅 (Debugging with IDEA)

Kotlin 플러그인의 코루틴 디버거(Coroutine Debugger)는 IntelliJ IDEA에서 코루틴 디버깅을 간편하게 만들어 줘요.

디버깅은 kotlinx-coroutines-core 1.3.8 이상 버전에서 동작해요.

Debug 도구 창에는 Coroutines 탭이 있어요. 이 탭에서 현재 실행 중인 코루틴과 중단된 코루틴의 정보를 찾을 수 있어요. 코루틴은 실행 중인 디스패처별로 그룹화돼요.

코루틴 디버거로 할 수 있는 작업은:

  • 각 코루틴의 상태 확인
  • 실행 중 및 중단된 코루틴의 로컬 변수와 캡처된 변수의 값 확인
  • 코루틴 생성 스택 전체와 코루틴 내부의 호출 스택 확인. 스택에는 표준 디버깅 중에 사라질 수 있는 변수 값을 가진 모든 프레임이 포함돼요.
  • 각 코루틴의 상태와 스택을 담은 전체 보고서 얻기. Coroutines 탭 안에서 우클릭한 다음 Get Coroutines Dump를 클릭하면 돼요.

코루틴 디버깅을 시작하려면 중단점을 설정하고 애플리케이션을 디버그 모드로 실행하기만 하면 돼요.

코루틴 디버깅에 대해 더 자세히 알고 싶다면 튜토리얼을 참고해요.

로깅으로 디버깅 (Debugging using logging)

코루틴 디버거 없이 스레드가 있는 애플리케이션을 디버깅하는 또 다른 접근 방식은, 모든 로그 문장에서 로그 파일에 스레드 이름을 출력하는 거예요. 이 기능은 로깅 프레임워크에서 보편적으로 지원돼요. 코루틴을 쓰면 스레드 이름만으로는 컨텍스트를 많이 알 수 없어서, kotlinx.coroutines가 더 쉽게 만드는 디버깅 기능을 제공해요.

다음 코드를 -Dkotlinx.coroutines.debug JVM 옵션으로 실행해 볼게요.

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking<Unit> {
//sampleStart
    val a = async {
        log("I'm computing a piece of the answer")
        6
    }
    val b = async {
        log("I'm computing another piece of the answer")
        7
    }
    log("The answer is ${a.await() * b.await()}")
//sampleEnd    
}

전체 코드는 여기에서 받을 수 있어요.

코루틴이 세 개 있어요. runBlocking 내부의 메인 코루틴(#1)과 지연 값을 계산하는 두 코루틴 a(#2), b(#3)죠. 세 코루틴 모두 runBlocking의 컨텍스트에서 실행되고 메인 스레드에 한정돼 있어요. 이 코드의 출력은:

[main @coroutine#2] I'm computing a piece of the answer
[main @coroutine#3] I'm computing another piece of the answer
[main @coroutine#1] The answer is 42

log 함수는 스레드 이름을 꺾쇠 안에 출력하는데, 그 이름이 main에 현재 실행 중인 코루틴의 식별자가 붙은 형태임을 볼 수 있어요. 이 식별자는 디버깅 모드가 켜져 있을 때 생성되는 모든 코루틴에 연속적으로 부여돼요.

디버깅 모드는 JVM을 -ea 옵션으로 실행해도 켜져요. 디버깅 기능에 대한 자세한 내용은 DEBUG_PROPERTY_NAME 프로퍼티 문서에서 확인할 수 있어요.

스레드 사이를 오가기 (Jumping between threads)

다음 코드를 -Dkotlinx.coroutines.debug JVM 옵션으로 실행해 볼게요(debug 참고).

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() {
    newSingleThreadContext("Ctx1").use { ctx1 ->
        newSingleThreadContext("Ctx2").use { ctx2 ->
            runBlocking(ctx1) {
                log("Started in ctx1")
                withContext(ctx2) {
                    log("Working in ctx2")
                }
                log("Back to ctx1")
            }
        }
    }
}

전체 코드는 여기에서 받을 수 있어요.

이 예제는 코루틴 사용의 새로운 기법들을 보여줘요.

첫 번째 기법은 지정된 컨텍스트와 함께 runBlocking을 사용하는 방법이에요. 두 번째 기법은 withContext를 호출하는 건데, 이 함수는 현재 코루틴을 중단하고 새 컨텍스트로 전환할 수 있어요(새 컨텍스트가 기존 컨텍스트와 다를 때). 구체적으로 서로 다른 CoroutineDispatcher를 지정하면 추가 디스패치가 필요해요. 블록이 새 디스패처에 스케줄되고, 끝나면 실행이 원래 디스패처로 돌아오죠.

그 결과 위 코드의 출력은:

[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1

위 예제는 Kotlin 표준 라이브러리의 use 함수로, newSingleThreadContext가 만든 스레드 자원을 더 이상 필요 없을 때 제대로 해제해요.

컨텍스트 안의 Job

코루틴의 Job은 그 컨텍스트의 일부이며, coroutineContext[Job] 표현식으로 컨텍스트에서 가져올 수 있어요.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
    println("My job is ${coroutineContext[Job]}")
//sampleEnd    
}

전체 코드는 여기에서 받을 수 있어요.

디버그 모드에서는 다음과 비슷하게 출력돼요.

My job is "coroutine#1":BlockingCoroutine{Active}@6d311334

CoroutineScope 안의 isActivecoroutineContext[Job]?.isActive == true의 간편한 단축 표현이라는 점에 주의해요.

코루틴의 자식들 (Children of a coroutine)

다른 코루틴의 CoroutineScope에서 코루틴이 시작되면, 그 코루틴은 CoroutineScope.coroutineContext를 통해 컨텍스트를 상속받고, 새 코루틴의 Job이 부모 코루틴의 Job의 자식이 돼요. 부모 코루틴이 취소되면 그 모든 자식도 재귀적으로 취소돼요.

하지만 이 부모-자식 관계는 두 가지 방식 중 하나로 명시적으로 오버라이드할 수 있어요.

  • 코루틴을 시작할 때 다른 스코프를 명시적으로 지정한 경우(예: GlobalScope.launch)에는 부모 스코프에서 Job을 상속받지 않아요.
  • 새 코루틴의 컨텍스트로 다른 Job 객체를 전달한 경우(아래 예제처럼)에는 부모 스코프의 Job을 오버라이드해요.

두 경우 모두, 시작된 코루틴은 시작된 스코프에 묶이지 않고 독립적으로 동작해요.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
    // 들어오는 요청을 처리하기 위한 코루틴을 시작해요
    val request = launch {
        // 두 개의 다른 job을 생성해요
        launch(Job()) { 
            println("job1: I run in my own Job and execute independently!")
            delay(1000)
            println("job1: I am not affected by cancellation of the request")
        }
        // 그리고 다른 하나는 부모 컨텍스트를 상속해요
        launch {
            delay(100)
            println("job2: I am a child of the request coroutine")
            delay(1000)
            println("job2: I will not execute this line if my parent request is cancelled")
        }
    }
    delay(500)
    request.cancel() // 요청 처리를 취소
    println("main: Who has survived request cancellation?")
    delay(1000) // 무슨 일이 일어나는지 보려고 메인 스레드를 1초 지연
//sampleEnd
}

전체 코드는 여기에서 받을 수 있어요.

이 코드의 출력은:

job1: I run in my own Job and execute independently!
job2: I am a child of the request coroutine
main: Who has survived request cancellation?
job1: I am not affected by cancellation of the request

부모의 책임 (Parental responsibilities)

부모 코루틴은 항상 모든 자식의 완료를 기다려요. 부모는 시작한 모든 자식을 명시적으로 추적할 필요도 없고, 마지막에 Job.join으로 기다릴 필요도 없어요.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
    // 들어오는 요청을 처리하기 위한 코루틴을 시작해요
    val request = launch {
        repeat(3) { i -> // 몇 개의 자식 job을 시작
            launch  {
                delay((i + 1) * 200L) // 가변 지연 200ms, 400ms, 600ms
                println("Coroutine $i is done")
            }
        }
        println("request: I'm done and I don't explicitly join my children that are still active")
    }
    request.join() // 자식까지 포함해 request 완료를 기다림
    println("Now processing of the request is complete")
//sampleEnd
}

전체 코드는 여기에서 받을 수 있어요.

결과는:

request: I'm done and I don't explicitly join my children that are still active
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done
Now processing of the request is complete

디버깅을 위한 코루틴 이름 짓기 (Naming coroutines for debugging)

코루틴이 자주 로그를 남기고 같은 코루틴에서 온 로그 레코드를 서로 연결하기만 하면 될 때는 자동으로 부여되는 id가 좋아요. 하지만 코루틴이 특정 요청 처리나 특정 백그라운드 작업에 묶여 있다면, 디버깅 목적으로 이름을 명시적으로 붙이는 게 더 좋아요. CoroutineName 컨텍스트 요소는 스레드 이름과 같은 목적으로 쓰여요. 디버깅 모드가 켜져 있으면 이 요소가 그 코루틴을 실행하는 스레드 이름에 포함돼요.

다음 예제가 이 개념을 보여줘요.

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking(CoroutineName("main")) {
//sampleStart
    log("Started main coroutine")
    // 두 개의 백그라운드 값 계산을 실행
    val v1 = async(CoroutineName("v1coroutine")) {
        delay(500)
        log("Computing v1")
        6
    }
    val v2 = async(CoroutineName("v2coroutine")) {
        delay(1000)
        log("Computing v2")
        7
    }
    log("The answer for v1 * v2 = ${v1.await() * v2.await()}")
//sampleEnd    
}

전체 코드는 여기에서 받을 수 있어요.

-Dkotlinx.coroutines.debug JVM 옵션으로 실행했을 때 출력은 이와 비슷해요.

[main @main#1] Started main coroutine
[main @v1coroutine#2] Computing v1
[main @v2coroutine#3] Computing v2
[main @main#1] The answer for v1 * v2 = 42

컨텍스트 요소 결합 (Combining context elements)

때로는 코루틴 컨텍스트에 여러 요소를 정의해야 해요. 이럴 때 + 연산자를 쓰면 돼요. 예를 들어 디스패처와 이름을 함께 명시적으로 지정해 코루틴을 시작할 수 있어요.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
    launch(Dispatchers.Default + CoroutineName("test")) {
        println("I'm working in thread ${Thread.currentThread().name}")
    }
//sampleEnd    
}

전체 코드는 여기에서 받을 수 있어요.

-Dkotlinx.coroutines.debug JVM 옵션으로 실행했을 때 이 코드의 출력은:

I'm working in thread DefaultDispatcher-worker-1 @test#2

코루틴 스코프 (Coroutine scope)

컨텍스트, 자식, Job에 대해 배운 내용을 한데 모아 볼게요. 애플리케이션에 수명 주기가 있는 객체가 있는데, 그 객체는 코루틴이 아니라고 가정해 봐요. 예를 들어 Android 애플리케이션을 작성하면서 Android 액티비티의 컨텍스트에서 여러 코루틴을 시작해 데이터 가져오기·갱신, 애니메이션 같은 비동기 작업을 수행한다고 해볼게요. 이 코루틴들은 메모리 누수를 피하려고 액티비티가 파괴될 때 취소되어야 해요. 물론 컨텍스트와 Job을 수동으로 다뤄 액티비티와 그 코루틴의 수명 주기를 묶을 수도 있지만, kotlinx.coroutines는 그걸 캡슐화하는 추상화를 제공해요. 바로 CoroutineScope예요. 모든 코루틴 빌더가 이 스코프의 확장으로 선언되어 있으니 이미 익숙할 거예요.

우리는 액티비티의 수명 주기에 묶인 CoroutineScope 인스턴스를 만들어 코루틴의 수명 주기를 관리해요. CoroutineScope 인스턴스는 CoroutineScope() 또는 MainScope() 팩토리 함수로 만들 수 있어요. 전자는 범용 스코프를, 후자는 UI 애플리케이션용 스코프를 만들고 기본 디스패처로 Dispatchers.Main을 사용해요.

class Activity {
    private val mainScope = MainScope()

    fun destroy() {
        mainScope.cancel()
    }
    // 계속 ...

이제 정의된 mainScope를 사용해 이 Activity의 스코프에서 코루틴을 실행할 수 있어요. 데모를 위해 서로 다른 시간 동안 지연되는 코루틴 열 개를 시작해 볼게요.

// class Activity 계속
    fun doSomething() {
        // 데모용으로 10개 코루틴을 실행. 각각 다른 시간 동안 작업
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L) // 가변 지연 200ms, 400ms, ...
                println("Coroutine $i is done")
            }
        }
    }
} // class Activity 끝

메인 함수에서 액티비티를 만들고 테스트용 doSomething 함수를 호출한 뒤, 500ms 후에 액티비티를 파괴해요. 그러면 doSomething에서 시작된 모든 코루틴이 취소돼요. 액티비티 파괴 후에는 조금 더 기다려도 더 이상 메시지가 출력되지 않는 걸로 확인할 수 있어요.

import kotlinx.coroutines.*

class Activity {
    private val mainScope = CoroutineScope(Dispatchers.Default) // 테스트 목적으로 Default 사용

    fun destroy() {
        mainScope.cancel()
    }

    fun doSomething() {
        // 데모용으로 10개 코루틴을 실행. 각각 다른 시간 동안 작업
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L) // 가변 지연 200ms, 400ms, ...
                println("Coroutine $i is done")
            }
        }
    }
} // class Activity 끝

fun main() = runBlocking<Unit> {
//sampleStart
    val activity = Activity()
    activity.doSomething() // 테스트 함수 실행
    println("Launched coroutines")
    delay(500L) // 0.5초 지연
    println("Destroying activity!")
    activity.destroy() // 모든 코루틴 취소
    delay(1000) // 동작하지 않는지 시각적으로 확인
//sampleEnd    
}

전체 코드는 여기에서 받을 수 있어요.

이 예제의 출력은:

Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destroying activity!

보시다시피 첫 두 코루틴만 메시지를 출력하고, 나머지는 Activity.destroy() 안의 mainScope.cancel() 호출 한 번으로 취소돼요.

Android는 수명 주기가 있는 모든 엔티티에서 코루틴 스코프를 일급으로 지원해요. 관련 문서를 참고해 주세요.

스레드 로컬 데이터 (Thread-local data)

어떤 스레드 로컬 데이터를 코루틴에 전달하거나 코루틴 사이에 전달하고 싶을 때가 있어요. 하지만 코루틴은 특정 스레드에 묶여 있지 않아서, 수동으로 하면 코드가 장황해지기 쉬워요.

ThreadLocal에는 asContextElement 확장 함수가 구원투수로 나와요. 이 함수는 주어진 ThreadLocal의 값을 보관하고, 코루틴이 컨텍스트를 전환할 때마다 그 값을 복원하는 추가 컨텍스트 요소를 만들어요.

직접 동작을 확인하는 건 쉬워요.

import kotlinx.coroutines.*

val threadLocal = ThreadLocal<String?>() // 스레드 로컬 변수 선언

fun main() = runBlocking<Unit> {
//sampleStart
    threadLocal.set("main")
    println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) {
        println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
        yield()
        println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    }
    job.join()
    println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
//sampleEnd    
}

전체 코드는 여기에서 받을 수 있어요.

이 예제에서 새 코루틴을 Dispatchers.Default로 백그라운드 스레드 풀에서 시작해요. 그래서 스레드 풀의 서로 다른 스레드에서 작업하지만, threadLocal.asContextElement(value = "launch")로 지정한 스레드 로컬 변수 값은 코루틴이 어느 스레드에서 실행되든 그대로 유지돼요. 따라서 출력(debug 포함)은:

Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
Launch start, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main], thread local value: 'launch'
After yield, current thread: Thread[DefaultDispatcher-worker-2 @coroutine#2,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'

해당 컨텍스트 요소를 설정하는 걸 잊어버리기 쉬워요. 그렇게 되면 코루틴을 실행하는 스레드가 다를 때, 코루틴에서 접근한 스레드 로컬 변수가 예상치 못한 값을 가질 수 있어요. 이런 상황을 피하려면 ensurePresent 메서드를 사용하고, 부적절한 사용에 대해서는 즉시 실패(fail-fast)하도록 권장해요.

ThreadLocal은 일급 지원을 받으며, kotlinx.coroutines가 제공하는 어떤 원시 타입과도 사용할 수 있어요. 다만 핵심적인 한계가 하나 있어요. 스레드 로컬이 변경되면 새 값이 코루틴 호출자에게 전파되지 않아요(컨텍스트 요소가 모든 ThreadLocal 객체 접근을 추적할 수 없기 때문). 그래서 갱신된 값은 다음 중단 때 사라져요. 코루틴 안에서 스레드 로컬 값을 갱신하려면 withContext를 사용하면 돼요. 자세한 내용은 asContextElement를 참고해요.

대안으로 값을 class Counter(var i: Int) 같은 가변 박스에 저장하고, 그 박스를 스레드 로컬 변수에 저장할 수도 있어요. 다만 이 경우 잠재적으로 동시에 일어나는 가변 박스 변수 변경의 동기화는 전적으로 우리 책임이에요.

로그 MDC 통합, 트랜잭션 컨텍스트, 또는 데이터 전달에 내부적으로 스레드 로컬을 쓰는 다른 라이브러리와의 통합 같은 고급 사용법은, 구현해야 하는 ThreadContextElement 인터페이스의 문서를 참고해 주세요.

더 알아보기 (Learn more)