Kotlin coroutine suspend? 디컴파일을 통해 알아보자.



개인 광고 영역

Kotlin 코루틴에는 suspend 함수를 활용할 수 있습니다. suspend가 추가되면 코루틴에서 동작하도록 강제하게 되는데 단순 강제만은 아닙니다.

코루틴이 종료되면 suspend 함수 역시 종료되어야 합니다.(suspend를 활용한다고 무조건 종료하는 것은 아닌데, 다음에 별도로 작성해보겠다.)

이번 글은 아래의 suspend 함수를 디컴파일해보고, suspend 키워드가 어떤 일을 하는지 직접 구현해본다면 어떻게 접근 가능할지 살펴보겠습니다.

suspend fun doSomething(): Int {
  delay(1000L)
  return 100
}

이 글에서 도움 될 문서는 아래와 같습니다.


이 글에서 알아볼 내용

  • suspend 함수 사용 법
  • suspend 함수 디컴파일
  • Continuation 살펴보기


suspend 함수 사용법

suspend function의 사용법은 간단한데, 일반 코틀린 함수에 suspend를 추가하면 suspend 함수로 변경됩니다.

아래 함수는 공식 문서의 코드를 그대로 가져왔습니다. 첫 번째 코드인 doSomethingUsefulOne() 함수는 1초를 대기하고, 13을 리턴하는 함수입니다.

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

두 번째 코드인 doSomethingUsefulTwo() 함수는 1초를 대기하고, 29를 리턴하게 됩니다.

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

이 코드는 CoroutineScope 내에서 사용할 수 있는데, 아래와 같습니다. 각 함수의 동작은 one을 먼저 받아오고, two를 받아오게 됩니다.

@Test
fun test() = runBlocking {
    val coroutineScope = object : CoroutineScope {
        override val coroutineContext: CoroutineContext
            get() = Dispatchers.IO + SupervisorJob()
    }

    coroutineScope.launch {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("one + two ${one + two}")
    }.join()
}

suspend function을 디컴파일을 통해 어떻게 만들어지는지 살펴보겠습니다.


suspend 함수 디컴파일

과거 코루틴 사용 시에는 디컴파일을 하지 못했습니다.(완전 과거형), 하지만 현재는 가능합니다. 그래서 디컴파일을 시도해보겠습니다.

이 디컴파일 결과물에는 delay() 함수의 코드는 제외했습니다. 실제 $completion 변수를 사용하는 부분은 delay() 함수에서 사용하지만, 여기서는 제외하고 보겠습니다.

@Nullable
public final Object doSomethingUsefulOne(@NotNull Continuation $completion) {
   // delay() 관련 코드 생략 ...
   return Boxing.boxInt(13);
}

suspend 키워드를 함수에 포함하면 자연스럽게 @NotNull Continuation $completion을 함수 파라미터로 전달받습니다. 그리고 결과를 리턴하도록 함수가 구현됩니다. 결국 자바라서 비동기 처리용으로 만들어지게 됩니다.

자바에서는 매우 흔한 패턴인데, callback 파라미터에 값을 넘겨주는 형태로 만들어집니다. 다행히 코루틴에서는 blocking이기 때문에 return 처리하기에 필요하지는 않습니다.


Continuation는 누가 전달해 줄까?

그럼 Continuation은 누가 전달해 줄까요? 보통 CoroutineScope의 launch 함수 내에서 suspend를 호출하게 됩니다.

launch 내부 코드를 잠시 살펴보겠습니다.

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

launch는 start 옵션이 기본 DEFAULT로 동작하도록 구현되어 있습니다. lazy가 아니기 때문에 StandaloneCoroutine을 생성하고, 이를 활용합니다. 넘겨주는 정보들은 이 글에서는 중요하지 않습니다.

StandaloneCoroutine class는 아래와 같이 구현되어 있습니다. 이 중 AbstractCoroutine<> 클래스 정보가 보입니다.

private open class StandaloneCoroutine(
    parentContext: CoroutineContext,
    active: Boolean
) : AbstractCoroutine<Unit>(parentContext, active)

AbstractCoroutine 클래스는 아래와 같습니다. 여기에 보면 Continuation<>을 상속받고 있음을 확인할 수 있습니다.

@InternalCoroutinesApi
public abstract class AbstractCoroutine<in T>(
    /**
     * The context of the parent coroutine.
     */
    @JvmField
    protected val parentContext: CoroutineContext,
    active: Boolean = true
) : JobSupport(active), Job, Continuation<T>, CoroutineScope

결국 launch 내에서 suspend 함수를 부르면 AbstractCoroutine에서 상속받고 있는 Continuation이 있기에 이를 활용해 suspend 함수가 구현되어 있음을 알 수 있습니다.

Continuation는 interface 정의되어 있는데, resumeWith() 함수를 포함하고 있습니다. resumeWith 함수를 통해 suspend 함수의 작업이 완료되었을 경우 resumeWith()으로 복귀시키게 됩니다.

/**
 * Interface representing a continuation after a suspension point that returns a value of type `T`.
 */
@SinceKotlin("1.3")
public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}


suspend를 직접 구현?

suspend 형태를 직접 구현해볼 수 있는데, Continuation을 넘겨줄 수 있는 클래스를 이용하는 방법입니다. 코루틴에는 다양한 방법이 있는데 가장 기본 형태인 [suspendCoroutine](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/suspend-coroutine.html)을 활용하면 유사하게 구현해볼 수 있습니다.

suspendCoroutine의 문서를 보면 아래와 같이 나와있는데, 이 함수의 파라미터를 보면 Continuation를 파라미터로 넘겨주는 Higher-Order function을 정의하고 있습니다.

@SinceKotlin("1.3")
@InlineOnly
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    return suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
        val safe = SafeContinuation(c.intercepted())
        block(safe)
        safe.getOrThrow()
    }
}

suspendCoroutine 함수를 활용해볼 순 있지만 사용 용도에 따라 사용하는 게 당연합니다. 어차피 suspend가 있는데, 굳이 이 방법을 활용할 필요는 없고 서로 다른 Thread를 처리할 때 suspendCoroutine를 이용할 수 있습니다.

suspend를 직접 구현한다면 suspendCoroutine과 함께 아래처럼 구현해 사용할 순 있습니다.

fun doSomething(value: Continuation<Int>) {
    value.resume(1000)
}

이렇게 하면 전형적인 Java에서 보던 Listener로 이벤트 던져주던 방식으로 사용할 수 있게 됩니다.


마무리

suspend 키워드가 해주는 동작이 어떤 게 있는지 간단하게 살펴봤습니다. 결국 자바로 바뀌어야 하기에 자바가 이해하는 코드의 형태로 변경되는 것만 확인했습니다.

다음 글에서 suspend 시에 어떤 경우에 cancel()이 동작하고, 어떤 경우에 동자하지 않는지를 정리해보겠습니다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments