코틀린 코루틴 1.3 - 중단은 어떻게 작동할까?

Seogi·2025년 6월 28일

Kotlin

목록 보기
6/27

중단 함수는 코틀린 코루틴의 핵심이며, 코틀린 코루틴의 다른 모든 개념의 기초가 되는 필수적인 요소이다.

코루틴은 중단되었을 때 Continuation 객체를 반환한다. 이 객체를 이용하면 멈췄던 곳에서 다시 코루틴을 실행할 수 있다. 중단했을 때 코루틴은 어떤 자원도 사용하지 않고 다른 스레드에서 시작할 수 있다.

여기서 코루틴과 스레드의 차이를 알 수 있는데, 스레드는 저장이 불가능하고 멈추는 것만 가능하기 때문이다.

재개

작업을 재개하려면 코루틴이 필요하다. 코루틴은 runBlocking이나, launch와 같은 코루틴 빌더를 통해 만들 수 있다. 더 간단한 방법도 있지만 지금은 중단 가능한 함수를 사용한 예제로 진행하겠다.

중단 함수는 말 그대로 코루틴을 중단할 수 있는 함수이다. 중단 함수는 반드시 코루틴 또는 다른 중단 함수에 의해 호출되어야 한다.

suspend fun main() {
    println("Before")

    println("After")
}
// Before
// After


suspend fun main() {
    println("Before")

    suspendCoroutine<Unit> { } // 중단 지점

    println("After")
}
// Before

중간에 중단 지점이 있는 경우 "After"는 출력되지 않으며, 코루틴은 "Before"이후에 중단되고 프로그램은 멈춘 뒤 재개되지 않는다.

public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit)

suspend fun main() {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
        println("Before too")
    }

    println("After")
}
// Before
// Before too

suspendCoroutine의 인자로 들어간 람다 함수는 Continuation객체를 인자로 받는다.

중단되기 전에 Continuation객체를 사용할 수 있고, suspendCoroutine이 호출된 뒤에는 이미 중단되어 Continuation객체를 사용할 수 없다.

이 람다 함수는 Continuation객체를 저장한 뒤 코루틴을 다시 실행할 시점을 결정하기 위해 사용된다.

suspend fun main() {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
        continuation.resume(Unit)
    }

    println("After")
}
// Before
// After

Continuation객체를 이용해 코루틴을 중단한 후 곧바로 실행할 수 있다.
위 예제에서 "After"가 출력되는 것은 suspendCoroutine에서 resume을 호출했기 때문이다.

public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))

public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit = resumeWith(Result.failure(exception))    

코틀린 1.3 이후로 Continuation 클래스의 형태가 달라졌다 한다. 원래는 resumeresumeWithException을 사용했지만, 지금은 Result를 반환하는 resumeWith함수 하나만 남아있고, resumeresumeWithExceptionresumeWith를 사용하는 표준 라이브러리의 확장 함수가 되었다.

suspend fun main() {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
        thread {
            println("Suspended")
            Thread.sleep(1000)
            continuation.resume(Unit)
            println("Resumed")
        }
    }

    println("After")
}
// Before
// Suspended
// (1초 후)
// After
// Resumed

suspendCoroutine에서 잠깐 동안 정지(sleep)된 뒤 재개되는 다른 스레드를 실행할 수도 있다.

값으로 재개하기

suspend fun main() {
    val i: Int = suspendCoroutine<Int> { cont ->
        cont.resume(42)
    }
    println(i) // 42

    val str: String = suspendCoroutine<String> { cont ->
        cont.resume("Some text")
    }
    println(str) // Some text

    val b: Boolean = suspendCoroutine<Boolean> { cont ->
        cont.resume(true)
    }
    println(b) // true
}

suspendCoroutine을 호출할 때 Continuation객체로 반환될 값의 타입을 지정할 수 있다. resume을 통해 반환되는 값은 반드시 지정된 타입과 같은 타입이어야 한다.

API를 호출해 네트워크 응답을 기다리는 것 처럼 특정 데이터를 기다리려고 중단하는 상황은 자주 발생한다.

코루틴이 있으면 중단함과 동시에 Continuation객체를 통해 라이브러리에 데이터를 resume 함수를 통해 보내달라고 요청할 수 있다. 그리고 데이터가 도착하면 스레드는 코루틴이 중단된 지점에서 재개하게 된다.

suspend fun requestUser(): User {
    return suspendCoroutine<User> { cont ->
        requestUser { user ->
            cont.resume(user)
        }
    }
}

suspend fun main() {
    println("Before")
    val user = requestUser()
    println(user)
    println("After")
}

중단 함수는 Retrofit과 Room 같은 널리 사용되는 라이브러리에 의해 이미 지원되고 있다. 그렇기에 중단 함수 내에서 콜백 함수를 사용하는 일은 거의 없다.

만약 필요하다면 suspendCoroutine 대신 suspendCancellableCoroutine을 사용하는 것이 좋다.

예외로 재개하기

class MyException : Throwable("Just an exception")

suspend fun main() {
    try {
        suspendCoroutine<Unit> { cont ->
            cont.resumeWithException(MyException())
        }
    } catch (e: MyException) {
        println("Caught!")
    }
}
// Caught!

resumeWithException이 호출되면 중단된 지점에서 인자로 넣어준 예외를 던진다.

suspend fun requestUser(): User {
    return suspendCancellableCoroutine<User> { cont ->
        requestUser { resp ->
            if (resp.isSuccessful) {
                cont.resume(resp.data)
            } else {
                val e = ApiException(
                    resp.code,
                    resp.message
                )
                cont.resumeWithException(e)
            }
        }
    }
}

이러한 방법은 문제가 발생했을 때 사용된다. 예를 들어 네트워크 관련 예외를 알릴 때 사용할 수 있다.

0개의 댓글