하지만, 비동기 요청 후 다시 비동기 요청, 다시 비동기 요청을 하게 된다면 콜백의 들여쓰기는 계속 깊어져 가독성이 떨어지게 된다.
예를 들어, 아침 루틴을 프로그래밍한다고 해보자. 아침에 일어나서 씻고 밥 먹고 준비하고 학교를 가야하는데 각각의 과정은 오래걸리기 때문에 비동기로 처리한다고 하자.
wakeup() { 씻은 상태 ->
eatBreakfast() { 밥 먹은 상태 ->
prepare() { 준비한 상태 ->
goToSchool()
}
}
}
아마 콜백으로 처리한다면 이런 식으로 구현이 될 것이다.
만약 이를 코루틴으로 호출을 한다면?
suspend fun morningRoutine() : Routine {
val wakeupTime = wakeup()
val breakfast = eatBreakfast()
val outfit = prepare()
val transportation = goToSchool()
return transportation
}
이런 식으로 마치 동기 프로그래밍하듯이 작성을 할 수 있게된다.
어떻게 이렇게 작성을 할 수 있는가?
suspend 키워드를 붙였기 때문이다.
suspend 키워드가 뭐길래 콜백을 없애주는 것일까?
- CPS 변환과 상태 기계를 활용해서 코루틴을 구현하기 때문이다.
위 코루틴 함수를 CPS로 변환한다면
fun morningRoutine(completion: Continuation<Any?>) {
val wakeupTime = wakeup()
val breakfast = eatBreakfast()
val outfit = prepare()
val transportation = goToSchool()
return completion.resumeWith(transportation)
}
다음과 같이 suspend 키워드가 없어지고 Continuation이 추가가 되었다.
그리고 반환값이 없어지고 completion이 resumeWith를 통해서 리턴값을 받게 되었다.
Continuation의 타입이 Any?인 이유는 여러 suspend 함수에 대한 리턴값들을 다 받아야 하기 때문에 최상위 타입으로 명시하는 것 같다.
근데 suspend되는 지점이 result값으로는 구분하기 힘들텐데 여러군데일 경우 현재 어디까지 실행되었는지는 어떻게 아는 것일까?
fun morningRoutine(completion: Continuation<Any?>) {
when(label) {
0 -> {
val wakeupTime = wakeup()
}
1 -> {
val breakfast = eatBreakfast()
}
2 -> {
val outfit = prepare()
}
3 -> {
val transportation = goToSchool()
}
4 -> {
completion.resumeWith(transportation)
}
}
이렇게 label이라는 값을 통해서 현재 어느 상태까지 왔는지를 파악한다.
suspend 될 수 있는 지점를 나눠서 코루틴 컴파일러가 이렇게 when으로 나눠준다.
label은 어디서 나온 걸까?
Continuation이 가지고 있다.
val continuation = ContinuationImpl(var0) {
val result: Any? = null
val label = 0
//suspend 시 필요한 변수들 값
val wakeupTime = 0
val breakfast = 0
val outfit: Any? = null
override fun invokeSuspend(result: Any?) {
this.result = result
return morningRoutine(this)
}
}
다음과 같이 Continuation을 구현한 ContinuationImpl이 label과 suspend 함수가 종료되고 반환될 때 저장할 값 result를 가지고 있다.
그리고 suspend 할 때 가지고 있어야 하는 변수들, 예를 들어 prepare를 호출할 때 wakeupTime과 breakfast값이 있어야 하므로 이 값들도 Continuation이 가지고 있는다.
디컴파일하면 L$0, I$0 이런 값으로 설정이 되는데 이해하기 쉽게 변경했다.
invokeSuspend가 resumeWith의 역할을 하는 것 같다.
이를 통해 다시 when 문을 구성해보면
fun morningRoutine(completion: Continuation<Any?>) {
val continuation = ContinuationImpl(completion)
when(continuation.label) {
0 -> {
continuation.label = 1
val wakeupTime = wakeup(continuation)
}
1 -> {
continuation.wakeupTime = result.toInt()
continuation.label = 2
val breakfast = eatBreakfast(continuation)
}
2 -> {
continuation.breakfast = result.toInt()
continuation.label = 3
val outfit = prepare(continuation)
}
3 -> {
continuation.outfit = result.toString()
continuation.label = 4
val transportation = goToSchool(continuation)
}
4 -> {
val transportation = Transportation(continuation.wakeupTime, ... result.toString())
return transportation
}
else -> throw IllegalStateException()
}