코루틴 공식 가이드 자세히 읽기 — Part 1 — Dive 3
중단(suspend)함수란 무엇이고, 어떻게 동작할까?
Korean [English]
(목차로 돌아가기)
일반 함수와 중단 함수는 겉보기에 함수명 앞에 suspend 키워드의 유무 외에는 큰 차이가 없어 보입니다. 하지만 함수명 앞에 suspend 라는 키워드를 붙임으로써 마법은 시작됩니다.
suspend 키워드가 붙은 함수들은 코틀린 컴파일러의 특별 대우(?)를 받게됩니다(CPS — Continuation-Passing-Style — 로 사용될 수 있도록 Continutation 파라미터가 함수의 마지막 파라미터로 추가되며 반환 값이 Any? 로 변경됨). 이렇게 생성된 중단 함수는 코루틴이나 다른 중단 함수 안에서만 호출될 수 있다는 제약이 생기긴 하지만 코루틴이 제공하는 유용한 다른 중단함수들을 사용할 수 있게 된다는 장점 또한 갖게 됩니다.
중단 함수는 suspend 키워드 자체가 의미 하듯이 호출될 경우 코루틴의 실행 흐름을 멈추게하는, 다시말해 실행의 분절점이 될 수 있다는 것을 나타냅니다. 이러한 suspend 키워드는 우리가 만든 어떤 함수에든 적용하여 해당 함수를 중단함수로 만들 수 있는데, 그 대상은 예를들어 top-level function, extension function, member function, local function, operator function 등이 될 수 있습니다.
이러한 중단함수가 코루틴에서 호출되면 그 시점에서의 실행정보들을 Continutation 객체로 만들어 캐시 해 두었다가 실행이 재개(Resume)되면 저장된 실행 정보를 기반으로 실행을 다시 이어 나가게 됩니다.
중단함수가 Continuation 이 되는 과정을 살펴봅시다. 이해를 돕기 위해서 우리가 아래와 같이 두 수를 더해주는 함수를 만들었다고 생각해 봅시다.
너무너무 어려운 로직이라서 2초나 걸립니다. 😄
이 함수는 중단함수이므로 당연히 코루틴 혹은 다른 중단함수 안에서 호출되어야 합니다.
이제 이코드를 빌드하면 코틀린 컴파일러는 sum 함수를 다음과 같은 형식으로 변경합니다.
INVOKESTATIC com/smp/coroutinesample/basic/BasicSample4Kt.sum (IILkotlin/coroutines/Continuation;)Ljava/lang/Object;
위 형식에서 IILkotlin/coroutines/Continuation; 부분은 이 sum 함수의 파라미터 형식을 나타내는데, 이 함수가 I : Integer, I : Integer, L : Object (with package name) 이렇게 3개의 파라미터를 갖고 있음을 나타냅니다 (Function signature). 컴파일러가 함수의 가장 마지막 파라미터로 Continuation object 를 임의로 추가했음을 알 수 있습니다.
이제 중단함수에 Continuation 이라는 추가 파라미터가 생겼으니 코루틴은 이 함수를 CPS(Continuation Passing Style) 형식으로 이용할 수 있게 되었고, 코루틴 프레임워크는 이를 통해 앞서 이야기 한 중단점의 suspend/resume 전환 동작을 수행하는 것입니다.
다음 예제를 잠깐 살펴봅시다.
longRunningTask() 중단함수는 두번의 delay() 함수(delay 역시 중단함수)를 호출하는 간단한 함수입니다 (수행이 오래걸리는 함수라고 생각합시다). 이 함수는 main() 함수에서 생성된 코루틴에서 두번 수행됩니다. 이 과정을 그림으로 나타내면 다음과 같습니다.
메인함수에서 시작된 LongRunningTask1 (중단함수)은 첫번째 delay() 함수를 만나면 일정 시간 그 수행을 멈춥니다. (이를 중단점(suspension point)라고 부릅시다.) 그러면 다른 멈춰있는 함수에게 실행 기회가 주어집니다. 그러면 LongRunningTask2 가 수행되고 마찬가지로 delay() 함수를 만나 실행을 멈춥니다. 이런식으로 하나의 스레드 안에서 실행 시간을 분할해가며 수행되는 모습이 됩니다.
만약 메인 코루틴에서 각각의 중단함수를 호출하지 않고 특정 중단함수가 또 다른 중단함수를 호출하도록 변경한다면 달라지는 부분이 있을까요? 그렇지 않습니다. 코루틴이 중첩될 때처럼 중단함수가 중첩될 때도 Continutation 상태로 호출 정보가 저장되며 마지막 호출까지 완료되면 최초 호출 함수가 그 결과를 받을 수 있습니다.
위 그림을 보면 어딘가 많이 본 모습 같지 않나요? 일반적으로 우리가 함수안에서 또다른 함수를 호출하면 스택에 이전 함수포인터를 저장함으로써 현재 함수 수행 후 결과를 돌려줄 위치를 기록해 놓습니다. 이런식으로 A func -> B func -> C func 로 호출 하면 그 정보가 스택에 쌓이고, 다시 C func -> B func -> A func 로 돌아오면서 스택의 정보가 해제 됩니다 (Stack unwinding).
(실제로 Continuation 의 구현체들은 CoroutineStackFrame 이라는 인터페이스 또한 구현하는데 여기에는 호출자 정보(caller stackframe) 또한 가지고 있습니다.)
일반적인 함수의 호출은 운영체제에서 그 호출 스택 관리를 해줍니다. 그럼 중첩된 코루틴이나 중첩된 중단함수의 스택관리는 누가 해주는 걸까요? 지금까지 이야기 한 것과 같이 코루틴 프레임워크가 CPS 방식으로 호출 정보(Continuation)를 스택 형태로 유지하고 있다가 호출 스택의 가장 마지막 함수가 실행을 종료하면 결과 값이 직전 호출 함수들로 전파되며 직전 함수를 재개(resume)해 나갑니다. 만약 스택상의 어떤 함수가 예외를 발생시키면 예외 정보를 최초 호출함수까지 Continuation 을 통해 전달합니다.
끝.
(목차로 돌아가기)