deep-dive into Coroutines #Continuation — CPS

choi jeong heon
7 min readOct 22, 2022

--

Hollywood L.A Photo by me

목차로 가기

Continuation Passing Style

CPS 는 Continuation Passing Style 의 약어로 함수형 프로그래밍에서 Continuation의 전달로 프로그램 제어가 흐르게하는 프로그래밍 스타일입니다.
코루틴에서도 이 개념이 등장하는데요, 아마 suspend 함수가 컴파일시 Continuation 이라는 객체를 받는 함수로 변환된다는 것을 들어보셨을 겁니다.

그렇다면 Continuation 이란 무엇일까요? 🤔

Continuation

위키피디아 에서는 프로그램 상태의 추상적 표현이라고 설명하고 있습니다. Continuation 은 프로그램 제어 상태를 실체화(reify) 하며, 프로세스 실행 특정 시점에서 프로그램 상태를 나타내는 데이터 구조라고 볼 수 있습니다.(코루틴의 Continuation) 이 데이터 구조는 프로그래밍 언어로 액세스 할 수 있습니다.

위키피디아의 이런 설명들로는 Continuation 의 개념이 잘 잡히지 않습니다.😢
따라서 이글 에서 Continuation 에 대해 잘 설명한 내용을 인용하고자 합니다.

보통 프로그램이 실행될 때는 현재 실행되고 있는 코드가 프로그래머의 관심의 초점이다. 후속문(이글에서 Continuation) 은 프로그래머의 관심을 현재 실행되고 있는 코드가 아니라 뒤이어 실행될 코드로 옮긴다(마치 주식에서 현물이 아닌 선물을 거래하는 것처럼). 만약 현재 실행되고 있는 코드에 뒤이어서 실행될 코드, 즉 후속문을 프로그래머가 마음대로 결정할 수 있다면, 그것은 바로 프로그램 제어를 마음대로 결정한다는 것을 의미한다. 후속문의 제어가 곧 프로그램의 제어 흐름을 결정하는 것이다.

다음의 예시가 이해를 도울 수 있을 것 같습니다.

val a = 1
val b = 2
val c = a + b

프로그램이 2번 라인을 실행하는 시점에서 그 다음 실행되어야할 후속문(Continuation) 은
val c = a + b 입니다.
만약 이 후속문을 함수로 실체화(reify) 한다면 다음과 같이 될 수 있습니다.

val sumPrint: (Int, Int) -> Unit = { a, b -> println(a + b) }val a = 1
val b = 2
sumPrint(a, b)

이 후속문을 코루틴의 Continuation 으로 실체화 한다면?

여기서 코루틴의 핵심인 suspend/resume 에서 resume 개념이 나옵니다. resume 은 ‘재개하다’ 의 의미 그대로 suspend 되고 난 후의 그 다음 후속문 부터 재개하는 함수입니다.

하지만 우리는 일반적으로 코루틴을 사용하면서 suspend / resume 을 거의 신경쓰지 않아도 됩니다. 보통 코루틴 프레임워크에서 다 알아서 처리해줍니다 :)

함수형 프로그래밍에서 이런 CPS 스타일의 구현방식은 대략 이렇습니다. 함수를 호출하고 그 다음, 후속문을 실체화한 객체를 넘겨주는 형태입니다.

코루틴의 CPS — suspend function

코루틴에서의 CPS 구현에 대해 살펴보겠습니다.
suspend 함수는 컴파일시 Continuation 객체를 인자로 받는 함수로 변환됩니다. (여기서부터는 KotlinConf 2017 — Deep Dive into Coroutines on JVM by Roman Elizarov 을 많이 참고했으므로 한 번 영상을 보는 것을 추천합니다 ! 😃)

위와 같은 suspend 함수는 컴파일시 다음과 유사하게 변환됩니다.

초기에 sm 이 어떻게 초기화 되는지, suspend 와 resume 은 내부적으로 어떻게 구현이 되어 있는지 등 자세한 설명은 여기서 하지 않고 다음 포스팅에서 이어서 할 것입니다.

여기서는 suspend 함수의 기본적인 컨셉과 state machine, label 의 개념만 짚고 넘어갑니다.

sm 은 state machine 이고 Continuation 입니다. suspend 되고 다시 resume 될 때 상태들을 어딘가에 store 하고 다시 restore 해야 하는데 그 곳이 바로 Continuation 입니다.

코드를 보면 label 로 코드 영역을 분리해놓았습니다. 이 label 역시 Continuation 에 저장되며 최초에 0으로 초기화됩니다. 따라서 case 0 블록이 실행되고 label 을 1로 수정하고 requestToken 이 호출됩니다. requestToken 을 호출할 때 Continuation 을 함께 전달하는 것에 주목해야 합니다. requestToken 역시 suspend 함수이기 때문에 Continuation 을 전달 받습니다. 그리고 함수가 suspend 되고 네트워크 응답이 도착하면 리턴값(token) 을 sm.result 에 저장해놓습니다. 그리고 나서 postItem 이 다시 resume 될 때 label 을 1로 설정했으므로 case 1 이 실행되고 sm.result 에서 token 을 가져온 후 다음 실행해야할 createPost 를 실행합니다.

이런식의 프로그래밍을 하는 이유가 무엇일까?

아직 이 질문에 대한 명확한 해답은 얻지 못했습니다. 하지만 개인적인 견해를 말해보자면, 기존의 비동기 프로그래밍은 callback 방식이 많이 사용되었습니다. 그런데 이 callback 방식은 callback 지옥에 빠지게 되면 가독성이 상당히 떨어지게 됩니다.

getUser(id) { pId -> 
getProduct(pId) { prod ->
networkCallAgain(prod) {
//....
}

이 콜백 지옥에서 벗어나 가독성이 좋고 직관적인 Direct Style 코드로 비동기 call 을 할 수 있는 마법?과도 같은 방법을 고민했고,,

val user = getUser(id)
val product = getProduct(user.pid)
networkCallAgain(product)
...

그래서 생각해낸 것이 suspend 함수가 아닌가 싶습니다.

사실 CPS 프로그래밍을 하려면 call/cc 등 몇가지 개념을 더 알아야 하는데 우리는 CPS 프로그래밍에 익숙하지 않습니다. 만약 코루틴이 개발자가 직접 CPS 프로그래밍을 하도록 구현되었다면 지금처럼 사랑받지 못했을 것입니다.
코루틴 프레임워크에서 개발자가 작성한 direct style 의 프로그래밍을 CPS 프로그래밍으로 변환해주어 개발자는 이에 대한 개념을 알지 못해도 됩니다. 그래서 코루틴이 더욱 놀랍게 느껴지는 것 같습니다.

지금까지 코루틴의 CPS에 대해 살펴보았습니다. 읽어주셔서 감사합니다 🙇
다음 포스팅에서는 Continuation 과 suspend function 을 조금 더 자세히 살펴보겠습니다.

참고자료

--

--