내부적으로는 JVM에 들어갈 때 바이트코드로 컴파일되면서 같은 createPost(…)인데 Continuation이 생성되어 Continuation Passing Style로 변환된다.
호출했던 함수의 끝에 매개변수가 하나 추가되서 Continuation이라는 객체를 넘겨주는 것으로 변환되는 것이다.
Labels
1 2 3 4 5 6 7 8
suspen funpostItem(item: Item) { // LABEL 0 ↛ val token = requestToken() // LABEL 1 ↛ val post = createPost(token, item) // LABEL 2 processPost(post) }
먼저 Labael이라는 작업을 하게 되는데 코루틴에서 순차적으로 작성했던 코드들이 suspend 함수가 되면 컴파일할 때 Label이 찍히게 된다.
이 함수가 재개되어야 하는데, 재개될 때 필요한 Suspention Point(중단 지점과 재개 지점)가 요구된다. 그래서 이 지점들을 Label로 찍어놓는 것이다. 이런 작업을 코틀린 컴파일러가 내부적으로 하게 된다.
대략적으로 아래와 같은 형태가 되는데, 작성했던 함수가 내부적으론 switch-case문처럼 바뀌어 case문이 3개가 생성되고 세 번을 실행하는 것을 알 수 있다. 함수를 실행할 때 0번이든, 1번이든, 2번이든 함수를 재개할 수 있는 지점이 생긴 것이다. 그리고 이 함수를 호출한 지점은 중단점이 될 수도 있는 것이다.
1 2 3 4 5 6 7 8 9 10
suspendfunpostItem(item: Item) { switch (label) { case 0: val token = requestToken() case 1: val post = createPost(token, item) case 2: processPost(post) } }
Label들이 다 완성되고 나면 Continuation Passing Style로 변환을 하게 된다.
1 2 3 4 5 6 7 8 9 10 11
funpostItem(item: Item, cont: Continuation) { val sm = object : CoroutineImpl { … } switch (sm.label) { case 0: val token = requestToken(sm) case 1: val post = createPost(token, item, sm) case 2: processPost(post) } }
Continuation이라는 객체가 있고, 매 번 함수를 호출할 때마다 continuation을 넘겨준다. continuation은 Callback 인터페이스 같은 것으로, 재개를 해주는 인터페이스를 가진 객체인 것이다.
위의 코드에서 sm이라고 하는 것은 state machine을 의미하는데, 각각의 함수가 호출될 때 상태(지금까지 했던 연산의 결과)를 같이 넘겨줘야 한다. 이 state machine의 정체는 결국 Continuation이고, Continuation이 어떠한 정보값을 가진 형태로 Passing이 되면서 코루틴이 내부적으로 동작하게 되는 것이다.
switch (sm.label) { case 0: sm.item = item sm.label = 1 requestToken(sm) case 1: createPost(token, item, sm) … } }
각각의 suspend function이 Continuation(위 코드에선 sm)을 마지막 매개변수로 가져가게 된다.
만약 requestToken(sm)이 완료되었다면 sm(continuation)에다가 resume()을 호출하게 된다.
다시 createPost(token, item, sm)가 호출되고 이것이 완료되었을 때도 sm(continuation)에다가 resume()을 호출하는 형태가 반복되는 것이다.
그렇다면 resume()은 정체가 무엇일까? 위의 코드에서 resume()은 결국 자기 자신을 불러주는 것이다. (postItem(…) 내부에서 postItem(…)을 다시 호출하고 있음)
예시로, suspend function인 requestToken(sm)의 연산이 끝났을 때 resume()을 통해 다시 postItem(…)이 호출되는데, 그때 Label 값을 하나 올려서 다른 케이스가 호출되도록 하는 것이다. 이렇게 되면 내부적으로는 마치 suspend function이 호출되고 다음 번 케이스, 그리고 또다시 다음 번 케이스로 넘어가는 형태가 되는 것이다.
Decomplie된 코드 살펴보기
1 2 3 4 5 6 7 8 9 10 11 12 13
funmain(): Unit { GlobalScope.launch { val userData = fetchUserData() val userCache = cacheUserData(userData) updateTextView(userCache) } }
suspendfunfetchUserData() = "user_name"
suspendfuncacheUserData(user: String) = user
funupdateTextView(user: String) = user
위의 코드를 코틀린의 바이트코드로 만든 다음, Decompile하여 Java 코드로 만들어보자.
publicfinalclassExample_nomagic_01Kt{ public static final void main() { BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) { int label;
@Nullable publicfinal Object invokeSuspend(@NotNull Object $result) { Object var10000; label17: { Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch(this.label) { case 0: ResultKt.throwOnFailure($result); this.label = 1; var10000 = Example_nomagic_01Kt.fetchUserData(this); if (var10000 == var4) { return var4; } break; case 1: ResultKt.throwOnFailure($result); var10000 = $result; break; case 2: ResultKt.throwOnFailure($result); var10000 = $result; break label17; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); }
funfetchUserData(cont: MyContinuation) { println("fetchUserData(), called") val result = "[서버에서 받은 사용자 정보]" println("fetchUserData(), 작업완료: $result") cont.resumeWith(Result.success(result)) }
funcacheUserData(user: String, cont: MyContinuation) { println("cacheUserData(), called") val result = "[캐쉬함 $user]" println("cacheUserData(), 작업완료: $result") cont.resumeWith(Result.success(result)) }