Coroutine은 내부적으로 콜백으로 동작을 합니다. Coroutine이 컴파일된 코드를 보면 결국 자바 코드가 되는데, Continuton 이 콜백에 대한 정보를 담고있는 클래스라고 보시면 됩니다.
코루틴 개발자가 프레젠테이션을 했던 예제를 가지고 설명을 해볼게요.. 글을 서버에 올리는 서비스가 있다고 하죠. 이 서비스를 이용하기 위해서는 먼저 토큰을 가져와서, 서버에 글을 올리고, 생성된 글을 가지고 추가 처리를 해야한다고 가정해 보죠.
이 경우, 콜백을 이용한 코드를 작성하면 아래처럼 될 겁니다.
fun PostItem(item: Item) {
requestToken { token ->
createPost(toke, item) { post ->
processPost(post)
}
}
}
콜백에 콜백을 호출하는 되는 'Callack hell"이라고 불리우는 중첩 콜백을 작성하게 됩니다. 코드를 읽기도 힒들고 실수를 하기도 쉽죠. 위의 코드를 아래처럼 비동기이지만 순차적으로 코드를 작성할 수 있다면 훨씬 가독성이 뛰어나고 유지보수가 쉬운 코드를 작성할 수 있게 됩니다.
suspend fun PostItem(item: Item) {
val token = requestToken()
val token = createPost(toke, item)
processPost(post)
}
Continuation은 이것을 가능하게 해주는 특별한 callback wrapper입니다. Continuation의 아랫처럼 생겼습니다.
interface Continuation<in T> {
val context: CoroutineContext,
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
위의 createPost함수는 실제로는 Continuation을 사용하여 아래와 같이 변환됩니다.
suspend fun createPost(token: Token, item: Item): Post {...}
Object createPost(Token token, Item item, Continuation<Post> continuation) { ... }
즉, 코틀린 함수가 suspend 키워드를 가지고 있으면, 자바로 변환될 때 Continuation 인자를 만들어 주게 됩니다. Continuation 인터페이스에서 보시는 것처럼, 이 인터페이스는 다음에 실행할 함수에 대한 정보를 담고 있습니다.
postItem함수는 실제로 아래와 같은 형태로 동작한다고 보시면 됩니다.
fun postItem(item: Item, continuation: Continuation) {
val sm = object: CoroutineImpl {.... }
switch (sm.label) {
case 0:
sm.item = item
sm.label = 1
requestToken(sm)
case 1:
val item = sm.item
val token = sm.result as Token
sm.label = 2
createPost(token, item, sm)
case 2:
processPost(post)
}
}
postItem함수가 재귀적으로 호출되는데, continuation 인자에 따라 적절한 함수를 호출하고 있습니다.
suspendCoroutine은 콜백을 Coroutine 으로 변환하도록 해주는 함수입니다. 결국은 이것도 Continuation이 내부적으로 사용됩니다.