중요개념

컨티뉴에이션 전달 방식

컨티뉴에이션은 함수에서 함수로 인자를 통해 전달된다. 관례상 컨티뉴에이션은 마지막 파라미터로 전달된다.

suspend fun getUser(): User?
suspend fun setUser(user: User)
suspend fun checkAvailability(flight: Flight): Boolean

// 자세히 들여다 보면
fun getUser(continuation: Continuation<*>): Any?
fun setUser(user: User, continuation: Continuation<*>): Any
fun checkAvailability(flight: Flight, continuation: Continuation<*>): Any

중단 함수 내부를 들여다 보면 원래 선언했던 형태와 반환 타입이 달라졌다. Any 또는 Any?로 바뀌었다. 이는 중단 함수를 실행하는 도중에 중단되면 선언된 타입의 값을 반환하지 않을 수 있기 때문이다. 이때 중단 함수는 COROUTINE_SUSPENDED 마커(marker)를 반환한다. 따라서 getUser 함수는 User?와 마커를 모두 반환할 수 있는 Any?로 반환 타입이 바뀐 것이다.

아주 간단한 함수

좀 더 자세히 살펴보기 위해 지연이 일어나기 전후에 출력되는 함수로 시작해보자.

suspend fun myFunction() {
    println("Before")
    delay(1000)
    println("After")
}

이 함수는 상태를 저장하기 위해 자신만의 컨티뉴에이션 객체가 필요하다. 실제로 컨티뉴에이션은 이름이 없는 객체의 표현식이지만, 설명을 위해 MyFunctionContinuation이라고 하자. 함수의 body가 시작될 때 MyFunction은 파라미터인 continuation을 자신만의 컨티뉴에이션인 MyFunctionContinuation으로 포장한다.

val continuation = MyFunctionContinuation(continuation)

Continuation 클래스에 포장이 없는 경우에만 클래스를 포장해야 한다. 만약 코루틴이 재실행되고 있으면 컨티뉴에이션 객체는 이미 래핑되어 있을 것이므로 객체를 그대로 둬야 한다. 나중에 왜 그런지 자세히 살펴본다.

<aside> 📢 실제 작동하는 방식은 label의 첫 번째 비트가 바뀌고 중단 함수가 바뀐 값을 확인하는 과정이 있어 더 복잡하다. 이 과정은 중단 함수가 재귀를 지원하기 위해 필요하다.

</aside>

val continuation = if (continuation is MyFunctionContinuation) continuation 
	else MyFunctionContinuation(continuation)

// 아래와 같이도 가능
val continuation = continuation as? MyFunctionContinuation 
	?: MyFunctionContinuation(continuation)

이제 함수의 본체를 다시 보자.

suspend fun myFunction() {
    println("Before")
    delay(1000) // 중단 함수
    println("After")
}

함수가 시작되는 지점은 함수의 시작점(함수가 처음 호출될 때)과 중단 이후 재개 시점(Continuation이 resume을 호출할 때) 두 곳이다.

현재 상태를 저장하려면 label이라는 필드를 사용한다. 함수가 처음 시작될 때 이 값은 0으로 설정된다. 이후에는 중단되기 전에 다음 상태로 설정되어 코루틴이 재개된 시점을 알 수 있게 도와준다.