모두
CoroutineContext를 사용하고 있는데, 도대체 무엇일까?
// launch의 구현
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 객체들이 인덱싱된 집합// 다음과 같은 구조로 이루어져있음
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
}
get또는 [](대괄호) 를 이용해 유일한 키를 가진 원소를 찾을 수 있음null이 반환됨fun main() {
val ctx: CoroutineContext = CoroutineName("A name")
val coroutineName: CoroutineName? = ctx[CoroutineName]
println(coroutineName?.name) // A name
val job: Job? = ctx[Job]
println(job) // null
}
CoroutineContext를 합쳐 하나의 CoroutineContext로 만들 수 있음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) // 'Active' 상태이므로 true
// 빌더를 통해 생성되는 잡의 기본상태가 'Active' 상태이므로 true
val ctx3 = ctx1 + ctx2
println(ctx3[CoroutineName]?.name) // Name1
println(ctx3[Job]?.isActive) // true
}
CoroutineContext에 같은 키를 가진 또 다른 원소가 더해지면 맵처럼 새로운 원소가 기존 원소를 대체
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
}
CoroutineContext는 컬렉션이므로 빈 컨텍스트를 만들 수 있음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 ctx = CoroutineName("Name1") + Job()
println(ctx[CoroutineName]?.name) // Name1
println(ctx[Job]?.isActive) // true
val ctx2 = ctx.minusKey(CoroutineName)
println(ctx2[CoroutineName]?.name) // null
println(ctx2[Job]?.isActive) // true
val ctx = (ctx + CoroutineName("Name")).minusKey(CoroutineName)
println(ctx3[CoroutineName]?.name) // null
println(ctx3[Job]?.isActive) // true
fold와 유사한 fold 메서드를 사용할 수 있음fun main() {
val ctx = CoroutineName("Name1") + Job()
ctx.fold("") { acc, element -> "$acc$element " }
.also(::println)
// CoroutineName(Name1) JobImpl{Active}@dbab622e
val empty = emptyList<CoroutineContext>()
ctx.fold(empty) { acc, element -> acc + element }
.joinToString()
.also(::println)
// CoroutineName(Name1), JobImpl{Active}@dbab622e
}
CoroutineContext는 코루틴의 데이터를 저장하고 전달하는 방법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 {
delay(500)
log("Running async") // [main] Running async
42
}
launch {
delay(1000)
log("Running launch") // [main] Running launch
}
log("The answer is ${v1.await()}") // [main] The answer is 42
}
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
}
defaultContext + parentContext + childContext일반적인 중단 함수에서 어떻게 컨텍스트에 접근할 수 있을까?
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 인터페이스를 구현하는 클래스 만들기CoroutineContext.Key<*> 타입의 key 프로퍼티를 필요로 함class MyCustomContext : CoroutineContext.Element {
override val key: CoroutineContext.Key<*> = Key
companion object Key : CoroutineContext.Key<MyCustomContext>
}
// 예시 (연속된 숫자를 출력하도록 설계된 컨텍스트)
class CounterContext(
private val name: String
): CorotineContext.Element {
override val key: CoroutineContext.Key<*> = Key
private var nextNumber = 0
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
}
// 예시 : 테스트 환경과 프로덕션 환경에서 서로 다른 값을 쉽게 주입하기 위한 커스텀 컨텍스트
data class User(val id: String, val name: String)
abstract class UuidProviderContext: CoroutineContext.Element {
abstract fun nextUuid(): String
override val key: CoroutineContext.Key<*> = Key
companion object Key: CoroutineContext.Key<UuidProviderContext>
}
class RealUuidProviderContext: UuidProviderContext() {
override fun nextUuid(): String = UUID.randomUUID().toString()
}
class FakeUuidProviderContext(
private val fakeUuid: String
): UuidProviderContext() {
override fun nextUuid(): String = fakeUuid
}
suspend fun nextUuid(): String =
checkNotNull(coroutineContext[UuidProviderContext]) {
"UuidProviderContext not present"
}.nextUuid()
// 테스트하려는 함수
suspend fun makeUser(name: String) = User(
id = nextUuid(),
name = name
)
suspend fun main(): Unit {
// 프로덕션 환경일 때
withContext(RealUuidProviderContext()) {
println(makeUser("Michael"))
// 예를 들어 User(id=d260482a-..., name=Michael)
}
//테스트 환경일 때
withContext(FakeUuidProviderContext("FAKE_UUID")) {
val user = makeUser("Michael")
println(user) // User(id=FAKE_UUID, name=Michael)
assetEquals(User("FAKE_UUID", "Michael"), user)
}
}