제목은 코틀린 중단(suspend) 동작으로 하겠습니다. 근데 이제 Continuation 객체를 곁들인
코틀린 코루틴의 중단
코틀린 코루틴의 핵심은 코루틴을 특정 지점에서 멈춘 뒤, 이후에 다시 재개할 수 있다는 것입니다.
예를 들어, 스레드에서 API를 호출하여 외부에서 데이터를 얻어올 때 잠시 코루틴을 중단시키지만, 스레드는 블로킹되지 않고 다른 코루틴을 실행시키는 것과 같은 작업이 가능합니다.
즉, 많은 동작을 하기 위해, 많은 스레드를 생성하는 것보다 코루틴을 사용하는 게 메모리 및 프로세스 비용적으로 이점이 있으며, 이러한 중단은 코루틴의 핵심입니다.
코루틴이 중단되었을 때 반환하는 Continuation 객체
코루틴을 중단하는 것은 코루틴 실행을 중간에 멈추는 것을 의미하며, 이후에 중단한 코루틴의 실행이 필요할 때 재개할 수 있습니다.
그렇다면, 어떻게 중단이 된 곳에서 코루틴 재개를 할 수 있는 것일까요? 이는 코루틴이 중단되었을때 Continuation 객체를 반환하기 때문입니다.
중단된 코루틴이 재개되는 원리
중단된 코루틴이 재개되는 원리를 에제 코드를 통해 확인해 보겠습니다. 예제 코드에서는 코루틴을 재개하기 위해, 중단(suspend) 함수를 사용해서 코루틴을 중단할 수 있는 환경을 만들었습니다.
main 함수를 중단 가능한 함수로 사용하여, 코루틴을 중단할 수 있는 환경을 제공
1
2
3
4
suspend fun main() {
println("Before")
println("After")
}
실행 결과
1 2
Before After
이때, 함수 내부에서 suspendCoroutine 함수를 이용하여, “Before”와 “After”를 출력하는 출력문 사이를 중단 지점으로 하여, 중단해 보겠습니다.
suspendCoroutine 함수를 사용한 중단
1
2
3
4
5
6
7
suspend fun main() {
println("Before")
suspendCoroutine<Unit> { }
println("After")
}
suspendCoroutine 함수란?
1
suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T
suspendCoroutine 람다 표현식(
{ })에 인자로 들어가는 람다 함수는 중단이 되기 전에 실행되는데, 해당 함수가 Continuation 객체를 인자로 받습니다.실행 결과
1
Before- “After”는 출력되지 않으며 실행이 끝나지 않은 상태로 유지됩니다.
- 이는 “Before”가 출력된 이후 프로그램이 멈추고, 재개되지 않았기 때문입니다.
위 예시에서 중단된 프로그램을 재개하는 방법의 핵심은 위에서 언급한, 코루틴이 중단되었을 때 반환하는 Continuation 객체에 있습니다.
이때, 람다 함수는 Continuation 객체를 저장한 뒤 코루틴을 다시 실행할 시점을 결정하기 위해 사용됩니다.
Continuation 객체를 이용해 코루틴을 중단 한 뒤 곧바로 실행
1
2
3
4
5
6
7
8
9
suspend fun main() {
println("Before")
suspendCoroutine<Unit> { continuation ->
continuation.resume(Unit)
}
println("After")
}
실행 결과
1 2
Before After
suspendCoroutine가 호출된 뒤에는 이미 중단되어 Continuation 객체를 사용할 수 없기 때문에, Continuation 객체를 사용하기 위해서는 suspendCoroutine 함수의 인자로 들어가 람다 함수에서 중단되기 전에 사용할 수 있습니다.- suspendCoroutine의 코루틴이 중단되기 전에 Continuation 객체의
resume을 호출하여, 중단된 코루틴이 재개되어 “After”가 출력되고 프로그램 실행이 종료될 수 있었습니다.- 하지만, 실제로는 중단된 코루틴이 곧바로 재개될 경우 최적화로 인해 아예 중단되지 않습니다.
suspendCoroutine에서 1초 뒤 재개되는 다른 스레드를 실행
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun continueAfterSecond(continuation: Continuation<Unit>) {
thread {
Thread.sleep(1000) // 다른 스레드에서 1초 동안 정지
continuation.resume(Unit)
}
}
suspend fun main() {
println("Before")
suspendCoroutine<Unit> { continuation ->
continueAfterSecond(continuation)
}
println("After")
}
실행 결과
1 2
Before After
- “Before” 출력 후 1초 뒤 “After”를 출력합니다.
호출된 다른 스레드에서 1초 동안 정지된 뒤, 다시 코루틴을 재개하였습니다.
중단된 코루틴을 값으로 재개
위 예시에서 suspendCoroutine 타입의 인자로 Unit을 사용하고, resume 함수에 Unit 인자를 넣은 이유는, Unit이 중단 함수의 리턴 타입이고, Continuation의 제네릭 타입 인자이기 때문입니다.
1
2
3
val result: Unit = suspendCoroutine<Unit> { continuation: Continuation<Unit> ->
continuation.resume(Unit)
}
suspendCoroutine을 호출할 때 Continuation 객체로 반환될 값의 타입을 지정할 수 있으며, resume을 통해 반환되는 값은 반드시 지정된 타입과 같은 타입이어야 합니다.
코루틴에서 값으로 중단된 코루틴을 재개하는 상황은 외부 API를 호출해 특정 데이터를 기다리려고 중단하는 상황을 예시로 들 수 있습니다.
코루틴이 없다면 스레드는 응답을 기다리고 있을 수밖에 없지만, 코루틴을 통해 중단함과 동시에 “데이터를 받고 나면, 받은 데이터를 resume 함수를 통해 보내줘”라고 Continuation 객체를 통해 전달하면 스레드는 다른 일을 할 수 있습니다. 그리고 데이터가 도착하면 스레드는 코루틴이 중단된 지점에서 재개하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
suspend fun requestUser(name: String): User {
...
}
suspend fun main() {
println("Before")
val user = requestUser(name = "jude")
println(user)
println("After")
}
실행 결과
1 2 3
Before User(name=jude) After
중단된 코루틴을 예외로 재개하기
외부 API를 통해 데이터를 가져오려고 할 때, 예외가 발생하면 데이터를 가져올 수 없으므로, 코루틴이 중단된 곳에서 예외를 발생시켜야 합니다.
이때, resumeWithException이 호출되면 중단된 지점에서 인자로 넣어준 예외를 던집니다.
resumeWithException를 사용해 중단된 지점에서 예외 발생 예시 코드
1
2
3
4
5
6
7
8
9
10
11
class MyException: Throwable("Just an exception")
suspend fun main() {
try {
suspendCoroutine<Unit> { continuation ->
continuation.resumeWithException(MyException())
}
} catch (e: MyException) {
println("Caught!")
}
}
중단 함수는 함수를 중단시키는게 아닌, 코루틴을 중단시킨다
중단 함수는 코루틴이 아니고, 단지 코루틴을 중단할 수 있는 함수라고 할 수 있습니다.
→ 단, 중단 가능한 main 함수(suspend fun main)와 같은 경우는 특별한 경우로, 코틀린 컴파일러가 main 함수를 코루틴으로 실행시킵니다.
변수에 Continuation 객체를 저장하고, 함수를 호출한 다음에 재개하는 상황
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var varContinuation: Continuation<Unit>? = null
suspend fun suspendAndSetContinuation() {
suspendCoroutine<Unit> { continuation ->
varContinuation = continuation
}
}
suspend fun main() {
println("Before")
suspendAndSetContinuation()
varContinuation?.resume(Unit)
println("After")
}
실행 결과
1
Before- 실행이 끝나지 않고, 실행된 상태로 유지됩니다.
- 위 코드의 실행은, 다른 스레드나 다른 코루틴을 재개하지 않으면 계속 실행된 상태로 유지하게 됩니다.
- 즉,
continuation?.resume(Unit)가 호출되는 시점에서 코루틴이 이미 종료 상태이므로, 프로그램이 정지 상태에 빠지게 됩니다.
정리
효과적인 동작을 위해 코틀린 중단(suspend)을 사용하는데, 이러한 코틀린 중단이 어떻게 중단되고 재개될 수 있는지에 대해 알아보며, Continuation 객체와 밀접한 관계가 있다는 것을 알 수 있었습니다.
Marcin Moskała, ⌜코틀린 코루틴 Kotlin Coroutines : Deep Dive⌟, 신성열 옮김, 도서출판 인사이트, 2023, p.10, p.23-34