개요
1회차 글에서는 컴파일러가 suspend함수를 Continuation으로 바꾸는 과정과 그렇게 바뀐 Continuation 내부의 동작 및 resumeWith를 통해 발동하는 방법을 살펴봤습니다.
이번에는 Continuation이 필수적으로 받아들이는 CoroutineContext(코루틴 컨텍스트)란 무엇인가와 그 안에 들어가는 Dispatcher와 인터셉터의 개념을 알아봅니다.
보통 코루틴을 어려워하는 시점이 여기부터입니다. 컨텍스트를 명확하게 이해하고 외우면 그 이후는 그렇게 힘들지 않을 지도 모릅니다….아닌가..
CoroutineContext
컨티뉴에이션의 인터페이스는 다음과 같이 정의됩니다.
interface Continuation<in T>{ val context: CoroutineContext fun resumeWith(result: Result<T>) }
저번 글에서 확인한 것처럼 Continuation을 실제 발동시킬 resumeWith외엔 오직 코루틴 컨텍스트만 있으면 됩니다. 결국 코루틴 컨텍스트란 컨티뉴에이션이 생성될 때 알고 있어야 할 내용을 포함하는 컬렉션객체입니다. 컨티뉴에이션이 지속적으로 실행되는 동안 계속 알고 있어야 할 것이 있다면 바로 컨텍스트에 넣어서 컨티뉴에이션에게 전달하는 것이죠.
한마디로 코루틴컨텍스트는 컨티뉴에이션이 실행할 때 참조할 엘리먼트들의 컬렉션입니다.
엘리먼트에는 일반적인 값도 포함하지만 컨티뉴에이션이 사용할 특별한 객체들도 넣을 수 있습니다. 여기에 해당되는 특별한 객체는 3가지가 있습니다.
- Job : 컨티뉴에이션의 상태를 외부에서 감시하기 위한 모니터 객체
- Dispatcher : 컨티뉴에이션을 실행하는 실행기를 별도로 지정
- Exception Handler : 실행도중 예외가 발생하면 이를 처리하는 처리기
코루틴컨텍스트의 인터페이스부터 해서 차근차근 알아가보죠. 우선 인터페이스 정의입니다.
interface CoroutineContext { //내부 엘리먼트 중 키에 해당되는 녀석을 꺼내서 줌 operator fun <E:Element> get(key: Key<E>): E? //모든 엘리먼트를 순회하면서 fold를 계산함. 유일한 순회함수 fun <R> fold(initial: R, operation: (R, Element) -> R): R //엘리먼트를 추가함. 컨텍스트레벨로 추가할 수는 있지만 그러면 key로 찾을 수 없음 operator fun plus(context: CoroutineContext): CoroutineContext //제거할 때도 key를 기반으로 하기 때문에 애당초 엘리먼트레벨만 plus하는게 좋음 fun minusKey(key: Key<*>): CoroutineContext //엘리먼트란 key속성이 추가된 컨텍스트 interface Element:CoroutineContext{ val key: Key<*> //보통 구현할때 자기자신타입의 Key를 사용 } //key는 엘리먼트를 파마메터타입으로 하는 객체 interface Key<E:Element> }
우선 눈에 띄는 것부터 살펴보면 Element라는 내부 인터페이스를 볼 수 있습니다. 엘리먼트도 컨텍스트를 상속했으므로 모든 엘리먼트의 기능을 갖고 있으면서 그 외에 key라는 속성을 추가로 갖습니다.
헌데 이 key속성은 다시 내부에 있는 Key인터페이스의 구현체로 이때 제네릭으로는 다시 엘리먼트가 와야합니다.
이 key를 뭐에 쓰냐면 컨텍스트에서 get으로 특정 엘리먼트를 찾을 때 씁니다. 앞서 설명한대로 코루틴 컨텍스트는 엘리먼트의 컬렉션 같은 녀석인데 셋이면서 맵입니다. 맵처럼 key로 특정 엘리먼트를 찾아주는 get을 제공합니다. 대신 맵처럼 키와 값으로 분리되지 않고 키를 소유한 엘리먼트를 값으로 갖는 형태라고 할 수 있습니다.
그리고 엘리먼트를 순회하면서 어떤 연산을 하기 위해 fold가 제공되는데 이는 나중에 자세히 살펴보겠습니다(사실 말이 순회지 컨텍스트를 상속한 엘리먼트 입장에서 오버라이드하면 그냥 자기 자신을 이용한 연산일 수도 있습니다)
컨텍스트는 일종의 컬렉션같은 역할을 수행하니 plus를 통해 엘리먼트를 추가하거나, minusKey를 통해 엘리먼트를 제거할 수 있습니다.
인터페이스에서는 안보이지만 시스템에 정의된 기본 plus구현을 보면 같은 키가 들어오면 기존의 엘리먼트를 제거하고 새로운 엘리먼트로 대체하게 되어있습니다. 따라서 키가 같은 엘리먼트를 컨텍스트에 하나만 포함될 수 있다고 생각하는 게 좋습니다. 이제 컨텍스트에 plus로 추가될 Element들에 대해서 살펴보죠.
보통 값으로서의 엘리먼트
시스템라이브러리는 key를 포함하는 엘리먼트를 손쉽게 만들 수 있는 AbstractCoroutineContextElement<T>(key:Key<T>) 라는 추상클래스를 별도로 제공합니다. 이를 이용하여 컨티뉴에이션이 실행하는 동안 계속 참고해야하는 Member라는 객체를 엘리먼트로 만들면 다음과 같습니다.
class Member(var name:String, var email:String) : AbstractCoroutineContextElement(Member){ companion object:CoroutineContext.Key<Member> }
AbstractCoroutineContextElement에 넘긴 인자 Member는 Member.Companion으로 해석되며 Companion객체가 이미 Key<Member>인터페이스를 의미하므로 Member는 Key<Member>를 key로 갖는 엘리먼트가 되는 것입니다. 이렇게 만들어진 엘리먼트는 컨티뉴에이션이 생성될 때 주면 컨티뉴에이션 내부에서 참조하여 사용할 수 있게 됩니다.
class Member(var name:String, var email:String) : AbstractCoroutineContextElement(Member){ companion object:CoroutineContext.Key<Member> } suspend action(){ val member = coroutineContext[Member] ?: throw Throwable("no member") println("${member.name}, ${member.email}") } launch(Member("hika", "hika00@gmail.com")){ action() // hika, hika00@gmail.com }
suspend함수 내부에서는 현재 컨티뉴에이션이 실행시 주어진 컨텍스트를 참조할 수 있는 coroutineContext를 속성을 갖고 있습니다(여기 참고)
그리고 실제 실행시 launch등에서 Member엘리먼트를 넘기게 되면 그걸 사용할 수 있게 되는거죠.
이렇게 키를 갖는 엘리먼트의 형태만 취한다면 실행시 컨텍스트로서 넘겨주기만 하면 컨티뉴에이션 실행시에는 계속 참조할 수 있는 값이 됩니다.
이 때 재밌는 점은 컨티뉴에이션이 실행 중인데 외부에서 member의 속성을 변경하면 그것이 반영된다는 것입니다.
class Member(var name:String, var email:String) : AbstractCoroutineContextElement(Member){ companion object:CoroutineContext.Key<Member> } suspend action(){ Thread.sleep(100) //100ms 있다가 실행 val member = coroutineContext[Member] ?: throw Throwable("no member") println("${member.name}, ${member.email}") } val member = Member("hika", "hika00@gmail.com") launch(member){ action() // hika00, hika00@gmail.com 바뀐 값이 들어옴 } //즉시 member의 값을 변경 member.name = "hika00"
이를 이용하면 실행 중인 컨티뉴에이션이 외부와 통신할 수 있는 일종의 채널역할을 수행하게 할 수 있습니다(실제 채널은 좀 다르게 구현되어있습니다만 ^^)
디스패처와 인터셉트
엘리먼트로 올 수 있는 또 다른 유형은 Dispather입니다. 디스패처를 이해하려면 먼저 intercept(인터셉트)의 개념을 이해해야 합니다.
컨티뉴에이션은 resumeWith를 가진 단순한 실행기로 이를 어떻게 실행할 것인가를 결정하지 않습니다.
따라서 기본 구현을 그대로 진행하면 현재 쓰레드에서 동기적인 형태로 쭉 resumeWith가 연쇄되어 작동합니다. 이 상태를 unintercepted 코루틴이라고 합니다. 사실 코루틴의 기본 동작은 이렇듯 인터셉터가 없는 동기적 실행입니다.
이 상태에서 컨티뉴에이션에 인터셉터가 관여하게 되면 resumeWith의 발동을 인터셉터에게 맡기게 됩니다.
특정 컨티뉴에이션이 실행될 때 인터셉터를 지정하고 싶다면 컨티뉴에이션에 전달될 컨텍스트에 디스패처를 포함시키면 됩니다.
즉 디스패처란 컨티뉴에이션의 실행이 동기적으로 자동처리되는 것을 가로채서(인터셉트) 인터셉터에서 실행하게 만드는 객체입니다. 인터셉터의 실 구현체가 디스패처인 것이죠.
우선 인터셉터의 인터페이스를 살펴보죠.
public interface ContinuationInterceptor : CoroutineContext.Element { //인터셉터용 엘리먼트 키 companion object Key : CoroutineContext.Key<ContinuationInterceptor> //어떤 컨티뉴에이션을 받아 인터셉트가 가능한 컨티뉴에이션으로 바꿔서 반환함 fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> //다시 인터셉터가 없는 컨티뉴에이션으로 복원함 fun releaseInterceptedContinuation(continuation: Continuation<*>) {} ... }
엘리먼트로서의 구현을 제외하면 interceptContinuation과 releaseInterceptedContinuation 이라는 두 개의 메서드만 남습니다.
특정 컨티뉴에이션을 넘겨주면 인터셉트가 되는 컨티뉴에이션으로 바꿔주는게 interceptContinuation메서드고 그 컨티뉴에이션에서 다시 인터셉터를 제거하는 것이 releaseInterceptedContinuation입니다.
그리고 이를 구현하는 디스패처도 살펴보죠.
abstract class CoroutineDispatcher: AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { //핵심 메서드 abstract fun dispatch(context: CoroutineContext, block: Runnable) //내부 템플릿 메서드 open fun dispatchYield(context: CoroutineContext, block: Runnable): Unit = dispatch(context, block) //컨티뉴에이션을 DispatchedContinuation로 감싼 객체를 반환 final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = DispatchedContinuation(this, continuation) ... }
코루틴 디스패처는 인터셉터를 상속받아 좀 사용하기 편리하게 메소드를 다시 매핑해둔 상태입니다.
우선 interceptContinuation은 결국 DispatchedContinuation이라는 새로운 객체를 만들어 냅니다. 그리고 핵심적인 메서드는 dispatch인 것이죠. 이어서 DispatchedContinuation의 구현을 보겠습니다.
internal class DispatchedContinuation<in T>( val dispatcher: CoroutineDispatcher, val continuation: Continuation<T> ) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation { ... override fun resumeWith(result: Result<T>) { val context = continuation.context val state = result.toState() if (dispatcher.isDispatchNeeded(context)) { _state = state resumeMode = MODE_ATOMIC //resume을 dispather에게 위임함 dispatcher.dispatch(context, this) } else { executeUnconfined(state, MODE_ATOMIC) { withCoroutineContext(this.context, countOrElement) { continuation.resumeWith(result) } } } } ... inline fun resumeCancelled(state: Any?): Boolean { val job = context[Job] if (job != null && !job.isActive) { val cause = job.getCancellationException() cancelCompletedResult(state, cause) resumeWithException(cause) return true } return false } ... }
좀 길긴 한데 이는 DispatchedContinuation이 기본적으로 컨티뉴에이션이면서 여러 가지를 더 상속하기 때문입니다.
- DispatchedTask : 뭐 여러가지 기능이 있긴 한데 핵심은 이게 Runnable이라는 것입니다. 그래서 이걸 상속한 이상 컨티뉴에이션이 resumeWith뿐만 아니라 run도 제공하게 됩니다.
- CoroutineStackFrame : 이건 디버깅용 정보를 나타내기 위해 상속합니다.
중요한건 컨티뉴에이션으로서의 resumeWith죠. 어렵사리 보이지만 간단히 정리해보면
- 디스패처가 필요하면 dispatcher.dispatch를 하겠다는 거고
- 아니면 Continuation.resumeWith를 하는 원래 로직대로 가겠다는 뜻입니다.
즉 resumeWith를 하면 얌전히 그냥 하지 않고 dispatcher.dispatch를 통해 resumeWith를 처리한다는 거죠. 오히려 별건 없고 run의 구현은 그럼 어딨냐면 DispatchedTask에 있습니다.
internal abstract class DispatchedTask<in T>( public var resumeMode: Int ) : SchedulerTask() { ... public final override fun run() { val taskContext = this.taskContext var fatalException: Throwable? = null try { val delegate = delegate as DispatchedContinuation<T> val continuation = delegate.continuation withContinuationContext(continuation, delegate.countOrElement) { val context = continuation.context val state = takeState() val exception = getExceptionalResult(state) val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null if (job != null && !job.isActive) { val cause = job.getCancellationException() cancelCompletedResult(state, cause) continuation.resumeWithStackTrace(cause) } else { if (exception != null) { continuation.resumeWithException(exception) } else { continuation.resume(getSuccessfulResult(state)) } } } } catch (e: Throwable) { fatalException = e } finally { val result = runCatching { taskContext.afterTask() } handleFatalException(fatalException, result.exceptionOrNull()) } }
이것도 좀 귀찮게 긴데 쓸데 없는걸 쳐내고 핵심만 살펴보면 컨텍스트에 job이 포함되어있는지 체크하고 그넘이 이미 죽어버렸다면 정지시키고 아니라면 resume을 실행시키는 것으로 job컨트롤을 제외하면 그냥 크게 보면 resumeWith를 실행해주는 것입니다. 단지 이 때부터 Job객체가 관여하기 시작합니다.
너무 축약해서 정리하는 감이 없진 않지만 DispatchedContinuation은 평범한 컨티뉴에이션이 Runnable을 갖게 하고 run메소드에서 resumeWith를 실행하게 해주는 래퍼입니다.
그리고 실제 resumeWith의 호출은 dispatcher.dispatch로 위임하죠. 이제 실제 마지막으로 정리해보겠습니다.
- 멀쩡한 unintercepted 컨티뉴에이션이 있다.
- 이걸 intercepted컨티뉴에이션으로 바꾸면
- 내부적으로는 컨티뉴에이션에 DisaptchedContinuation래퍼를 뒤집어 씌우는데
- DisaptchedContinuation의 핵심은 Runnable을 구상하여 run에서 resumeWith를 호출하게 하고
- 원래 컨티뉴에이션으로서의 resumeWith는 dispatcher.dispatch를 호출하게 바꾼다.
- dispatcher는 컨티뉴에이션에 주어진 컨텍스트로부터 얻어내며
- dispatch는 Runnable을 인자로 받아 실행한다.
이상입니다. 머 간단하다면 간단한거죠. 1회차 글이 완전히 소화되었다면 그 컨티뉴에이션을 Runnable로 바꾸고 컨텍스트들에 들어온 디스패처에게 resumeWith를 위임한다정도로 쉽게 이해할 수 있습니다.
이제 Dispatcher 인터페이스를 상속받은 구상 디스패처는 각각 사정에 맞게 dispatch메소드를 오버라이드해서 구현하면 됩니다. 실제 Dispatcher구현체들은 플랫폼마다 제공되며 주로 kotlinx.coroutine등의 외부 패키지에 들어있습니다.
JVM용인 경우 여기에서 구현 예를 볼 수 있고 js라면 여기서 다양한 디스패처 구현을 볼 수 있습니다.
Job엘리먼트
엘리먼트로 올 수 있는 요소 중에 컨티뉴에이션의 실행상태 감시나 실행, 중지 등의 통제를 외부에서 처리하 위한 핸들러가 있는데 이를 Job이라고 합니다. 이미 위의 DispatchedTask의 run에서는 Job의 상태에 따라 더 진행할지 말지를 결정하는 구문이 등장했습니다. 뿐만아니라 DispatchedContinuation의 resumeCancelled에서도 Job의 상태를 평가하여 작동하죠. 근데 여태까지 나온 모든 코드는 Job의 상태를 바꾸는게 아니라 평가만 합니다. Job의 개념을 생각하면
- 컨티뉴에이션의 상태에 따라 Job의 상태를 업데이트해주고
- Job에게 명령을 받는다면 진행 중이던 컨티뉴에이션을 중지하는 등의 통제에 따라야 하는데
코드에는 Job의 현재 상태를 반영하는 코드는 등장했지만 Job의 상태를 바꾸는 코드는 등장하지 않았죠.
맞습니다. Job의 상태를 바꿔주는 코드는 kotlinx.coroutine에 있는 CoroutineScope계열이 처리하기 때문에 기본 패키지인 kotlin.coroutine에서는 Job을 컨텍스트에 넣든 말든 아무런 일도 일어나지 않습니다.
그러므로 Job과 관련된 자세한 내용은 3편에서 다루게 됩니다.
예외처리기로서의 엘리먼트
엘리먼트 중 CoroutineExceptionHandler를 상속한 객체가 있다면 컨티뉴에이션이 resumeWith에서 예외를 반환할 때 이 예외를 캐치할 핸들러가 됩니다.
CoroutineExceptionHandler는 Element를 상속하며 키는 당연하게도 Key<CoroutineExceptionHandler>로 지정됩니다.
interface CoroutineExceptionHandler : CoroutineContext.Element { companion object Key : CoroutineContext.Key<CoroutineExceptionHandler> fun handleException(context: CoroutineContext, exception: Throwable) }
handlerException 시그니처를 살펴보면 컨텍스트와 예외를 수신하도록 되어있습니다. 적절한 예외객체나 컨텍스트의 상황에 맞게 예외 뒷처리를 해주는 핸들러라고 생각하면 되고 자식객체들이 쓰기 쉽게 미리 키가 제공됩니다. 하지만 키만 변경하면 여러 개의 핸들러를 넣을 수도 있습니다.
근데 여태 알아본 여러 코루틴 구조물에서 별달리 예외처리기를 받아주는 곳이 없습니다.
이것 역시 기본 kotlin.coroutine패키지에서는 무시하고 kotlinx 패키지에서 사용하기 때문입니다. 그래서 이것도 3편에서 다룹니다.
sequence
여태까지 컨티뉴에이션이 꼭 받아야하는 코루틴컨텍스트와 그 안에 들어갈 엘리먼트에 대해 알아봤습니다. 특히 엘리먼트 중 인터셉터의 개념과 그 인터셉터가 어떤 구조로 동작하여 디스패쳐로 처리되는지도 자세히 봤죠.
지금까지의 지식을 이용해 코틀린 내장 지연컬렉션인 시퀀스의 코드를 분석해보죠.
- Sequence자체는 그저 iterator만 선언하고 있는 가벼운 인터페이스입니다.
- SequenceScope는 yield나 yieldAll을 선언하고 있는 추상 클래스죠.
- 핵심적인 객체는 SequenceBuilderIterator로 SequenceScope, Iterator, Continuation을 상속합니다.
결국 sequence{..}함수는 내부적으로 SequenceBuilderIterator객체를 생성하는 이터레이터입니다. 하지만 이 과정이 어떻게 되는지 보다 자세히 살펴볼 필요가 있습니다.
//1 시퀀스 빌더 fun <T> sequence(block: suspend SequenceScope<T>.() -> Unit): Sequence<T> = Sequence { iterator(block) } //2 매번 이터레이터 객체를 만들어내는 팩토리함수 fun <T> Sequence(iterator: () -> Iterator<T>): Sequence<T> = object : Sequence<T> { override fun iterator(): Iterator<T> = iterator() } //3 실제 처리되는 이터레이터는 SequenceBuilderIterator임 fun <T> iterator(block: suspend SequenceScope<T>.() -> Unit): Iterator<T> { val iterator = SequenceBuilderIterator<T>() iterator.nextStep = block.createCoroutineUnintercepted(receiver = iterator, completion = iterator) return iterator }
1번 sequence 함수는 2번 Sequence함수를 이용해 Sequence객체를 만들어내고 이때의 iterator동작을 인자로 받은 3번함수의 SequenceBuilderIterator로 제공하게 됩니다.
- 1번 함수가 반환하는 것은 Sequence<T>형으로 이것은 외부에 이터레이터객체로 인식되므로 for등에 사용할 수 있습니다.
- 2번 함수가 Object:Sequence<T>를 반환하지만 이때 iterator메서드의 실질적인 처리는 전부 3번 함수에게 받아온 SequenceBuilderIterator객체가 담당하게 되는 것입니다.
3번 함수를 보면 SequenceBuilderIterator를 만들고 즉시 nextStep에 인자로 받은 suspend함수로부터 컨티뉴에이션을 만들어 넣고 있는 것을 확인할 수 있습니다. 즉 SequenceBuilderIterator는 태어나자마자 nextStep에 컨티뉴에이션을 갖고 시작하는 것이죠.
이제 SequenceBuilderIterator의 메서드를 하나씩 뜯어서 보겠습니다. 전체 코드는 여기에 있습니다.
private const val State_NotReady: State = 0 private const val State_ManyNotReady: State = 1 private const val State_ManyReady: State = 2 private const val State_Ready: State = 3 private const val State_Done: State = 4 private const val State_Failed: State = 5 private class SequenceBuilderIterator<T> : SequenceScope<T>(), Iterator<T>, Continuation<Unit> { //최초 상태는 notready임 private var state = State_NotReady private var nextValue: T? = null private var nextIterator: Iterator<T>? = null //이건 위에 iterator함수에서 이미 지정해줬음 var nextStep: Continuation<Unit>? = null override fun hasNext(): Boolean { while (true) { when (state) { State_NotReady -> {} //아무것도 안함 State_ManyNotReady -> if (nextIterator!!.hasNext()) { state = State_ManyReady return true } else { nextIterator = null } State_Done -> return false //종료 State_Ready, State_ManyReady -> return true //다음 값 있음 else -> throw exceptionalState() } // NotReady면 여기로 나오게 됨 state = State_Failed val step = nextStep!! nextStep = null // 컨티뉴에이션을 실행시키고 나면 state가 변함 step.resume(Unit) } }
우선 시퀀스의 상태를 나타내는 상수가 6가지로 최초 상태는 NotReady입니다. 가장 기본이 되는 hasNext를 분석해보죠.
우선 초기 상태의 NotReady는 아무것도 하지 않고 when을 빠져나오게 합니다. 그러고 나면 state는 Failed가 되고 step은 그 사이에 지정된 nextStep의 컨티뉴에이션을 가리키게 됩니다. 앞서 말한대로 nextStep에는 sequence함수가 받은 suspend함수로부터 생성한 컨티뉴에이션이 들어있는 것을 확정할 수 있으므로 !!를 통해 step변수를 설정하고 nextStep을 바로 비워버립니다. 그리고 resume을 때려서 즉시 실행하게 됩니다.
즉 최초 hasNext를 호출하면 while문이 돌면서 우선 nextStep의 컨티뉴에이션부터 실행하고 보는거죠. 근데 앞의 sequence함수가 받은 suspend람다는 SequenceScope의 확장람다로 그 대상에 SequenceBuilderIterator자신을 지정했습니다(3번 iterator함수 참고)
이는 유저가 다음과 같이 작성했을 때
val seq = sequence{ yield(3) yield(5) }
저 yield는 사실 SequenceBuilderIterator의 메서드라는 것입니다. SequenceBuilderIterator의 yield를 살펴보면
override suspend fun yield(value: T) { nextValue = value state = State_Ready return suspendCoroutineUninterceptedOrReturn { c -> nextStep = c COROUTINE_SUSPENDED } }
이렇게 되어있습니다. 유저가 넘긴 suspend람다는 결국 yield메서드를 기준으로 label이 나눠지고 hasNext에서 resume을 호출했기 때문에 무조건 yield가 있는 지점까지 실행되어 위 메서드가 호출되는 것이죠.
우선 yield시 넘긴 값을 nextValue로 지정하고 state를 Ready로 비로서 바꾸게 됩니다. 여기까지는 어렵지 않죠. 마지막에 suspendCoroutineUninterceptedOrReturn이 어렵죠.
suspend함수가 일반적인 값을 반환하는 형태로 작성되지 않고 직접 진행되고 있는 컨티뉴에이션의 resumeWith를 호출하여 결과를 반환하거나 컨티뉴에이션 그 자체를 얻고 싶다면 suspendCoroutine계열의 함수로 작성할 수 있습니다. 위 코드에서는 nextStep에 현재 진행 중인 컨티뉴에이션을 잡아주면서 동시에 COROUTINE_SUSPENDED를 반환하여 진행 중인 컨티뉴에이션이 정지하게 만듭니다.
정지된 컨티뉴에이션은 nextStep에 잡혀있으므로 다시 resumeWith를 때리면 그 이후가 진행될 것입니다.
이렇듯 개발자가 직접 컨티뉴에이션의 통제를 하기 위해 사용하는 함수가 suspendCoroutine계열로 그중 본문에 나오는 suspendCoroutineUninterceptedOrReturn는 인터셉터가 개입하지 못하게 동기적으로 즉시 실행됩니다. yield가 하는 일이란게 결국 nextValue를 지정하는 것과 일시중단 때리는 것 밖에 없으니까요.
이제 다시 위쪽의 hasNext로 돌아가보면 while때문에 다시 when에 오게 되고 when에서 상태가 Ready로 변경되었으니 true를 반환하게 됩니다.
next함수는 다음과 같습니다.
override fun next(): T { when (state) { State_NotReady, State_ManyNotReady -> return nextNotReady() State_ManyReady -> { state = State_ManyNotReady return nextIterator!!.next() } State_Ready -> { state = State_NotReady val result = nextValue as T nextValue = null return result } else -> throw exceptionalState() } }
일단 상태가 Ready니 외부에는 nextValue를 반환하면 됩니다. 반환하면서 즉시 상태를 NotReady로 되돌리게 됩니다. 이 반복을 통해 hasNext에서는 다시 컨티뉴에이션을 resume으로 전진시켜 Ready로 바꾸면서 또 정지시키고 next에서는 yield를 통해 얻은 nextValue를 반환하는 것을 반복합니다.
SequenceBuilderIterator는 자신이 컨티뉴에이션이므로 resumeWith도 구현되어있습니다.
override fun resumeWith(result: Result<Unit>) { result.getOrThrow() state = State_Done }
더 이상 yield의 개입이 없다면 마지막에는 resumeWith가 호출되므로 상태는 Done에 수렴하고 hasNext는 false를 반환하면서 끝나게 됩니다.
결론
이번 글에서는 코루틴컨텍스트와 그 안에 들어갈 수 있는 다양한 엘리먼트를 살펴봤습니다.
특히 인터셉터와 디스패처가 어떻게 컨티뉴에이션을 통제하고 외부의 실행기에게 resumeWith를 위임하는지도 자세히 알아봤습니다.
하지만 Job이나 예외처리기는 결국 기본 코루틴 시스템에서는 제어하지 않는다는 것도 알게 되었습니다.
다음 시간 부터는 이 두개를 적극 활용하는 kotlinx의 coroutine패키지 안에 여러 기능을 살펴봅니다.
recent comment