Suspend 한정자 — 내부 동작
suspend 한정자에 대해 알아봅니다.
본 글은 Kotlin Vocabulary에서 소개되는 코틀린 문법에 대한 내용을 번역한 글입니다. 원문 링크👇
Suspend
코틀린 코루틴은 안드로이드 개발자에게 suspend라는 한정자를 제공해주었습니다. 이 한정자는 내부에서 어떤 방식으로 동작할까요? 컴파일러는 코루틴의 실행을 일시 중단 및 재개할 수 있도록 어떻게 코드를 변환하고 있을까요?
이것을 안다면 코루틴이 왜 시작한 작업이 모두 완료될 때까지 반환되지 않는 이유와 코드가 스레드를 차단하지 않고 일시 중단할 수 있는지에 대한 방법을 더 잘 이해할 수 있을 것입니다.
TL;DR; 코틀린 컴파일러는 우리를 위해 코루틴의 실행을 관리하는 모든 작업에 대한 일시 중단 기능을 가지고 있는 상태 머신을 만들어 줍니다!
Coroutines
코루틴은 안드로이드에서 비동기식 작업을 단순화합니다. 문서에 설명된 대로, 메인 스레드를 차단하고 앱이 정지될 수 있는 가능성이 있는 비동기 작업을 관리하는데 사용됩니다.
코루틴은 콜백 기반 API를 필수적으로 사용할 때에도 도움이 됩니다. 예를 들어 콜백을 사용하는 이 비동기적인 코드를 확인해봅시다.
이러한 콜백 지옥에 빠진 코드는 코루틴을 사용하여 순차적인 함수 호출로 변환할 수 있습니다.
코루틴 코드에선 suspend 한정자를 함수에 추가했습니다. 이것은 컴파일러에게 이 함수가 코루틴 안에서 실행되어야 한다는 것을 알려주게 됩니다. 개발자로서 suspend 기능은 어느 시점에 실행이 중단되고 재개될 수 있는 정기적인 기능이라고 생각할 수 있습니다.
콜백과 달리 코루틴은 스레드 간 교환 및 예외를 처리할 수 있는 쉬운 방법을 제공해줍니다.
하지만 우리가 suspend로 함수를 표시했을 때 컴파일러는 실제로 무엇을 하고 있을까요?
Suspend의 내부
loginUser
suspend 함수로 돌아가보면, 이 함수가 호출하는 다른 함수도 suspend 함수임을 알 수 있습니다.
간단히 말해서, 코틀린 컴파일러는 suspend 함수를 가져와서 유한한 상태 머신을 사용하여 최적화된 버전의 콜백으로 변환합니다.
단지 컴파일러에서 그 콜백들을 사용할 뿐인 것입니다!
코루틴이 어떻게 정의되어 있는지 살펴봅시다.
context
는 연속성에 사용될CoroutineContext
가 될 것입니다.resumeWith
은 정지를 불러일으키거나 예외를 발생하는 계산의 결과인 값을 포함할 수 있는Result
를 가진 코루틴의 실행을 재개합니다.
컴파일러는 다음처럼 suspend 함수의 결과를 호출한 코루틴에 전달하는데 사용할 시그니처 파라미터인 completion
(Continuation
타입)을 추가하여 suspend 한정자를 바꾸어줍니다.
단순화를 위해서 예제에서는 User
대신에 Unit
을 반환할 것입니다. 추가된 Continuation
파라미터에서 User
객체가 반환됩니다.
suspend 함수의 바이트 코드는 T | COROUTINE_SUSPEND
의 조합형이기 때문에 실제로 Any?
를 반환합니다. 그것은 사용하는 함수가 사용 가능해질 때 동기적으로 반환할 수 있도록 해주는 역할을 합니다.
다른 곳에서도 Continuation
인터페이스를 살펴볼 수 있습니다.
supplyCoroutine
또는suspendCancelableCoroutine
을 사용하여 콜백 기반 API를 코루틴으로 변환할 때Continuation
과 상호 작용하여 파라미터로 전달된 코드 블록을 실행한 후 일시 중단된 코루틴을 재개합니다.- suspend 함수의
startCoroutine
익스텐션 함수로 코루틴을 시작할 수 있습니다. 새로운 코루틴이 결과 or 예외로 완료될 때 호출되는 파라미터로Continuation
객체를 사용합니다.
다른 Dispatchers 사용하기
여러가지 다른 Dispatcher간에 서로 다른 스레드에서 계산을 실행하도록 스왑할 수가 있습니다. 코틀린은 중단된 연산을 재개할 부분을 어떻게 아는 걸까요?
Continuation
에는 DispatchedContinuation
이라고 하는 서브 타입이 있는데, 여기서 resume 함수가 CoroutineContex
내부의 Dispatcher로 dispatch 호출을 합니다. isDispatchNeed
함수 오버라이드(dispatch 전에 호출)를 사용하여 항상 false
를 리턴하는 Dispathcers.Unconfined
를 제외한 모든 Dispatchers가 dispatch 호출을 하게 됩니다.
생성된 상태 머신
코틀린 컴파일러는 함수가 내부적으로 suspend 되는 시기를 식별합니다. 또한 모든 서스펜션 지점을 유한 상태 머신에서 상태로 나타냅니다. 이런 상태는 컴파일러에 의해 라벨로 표시됩니다.
더 많은 상태를 가지는 상태 머신을 위해서 컴파일러는 다음처럼 여러 상태를 구현하기 위해 when
문을 사용합니다.
다른 상태들이 정보를 공유할 방법이 없기 때문에 이 방법은 불완전합니다. 컴파일러는 함수에서 동일한 Continuation
객체를 사용하여 작업을 수행하는데 이것이 일반적으로 Continuation
이 원래 함수의 반환 유형인 User
대신 Any?
인 이유입니다.
또한 컴파일러는 1. 필요한 데이터를 보유하고 2. loginUser
기능을 재귀적으로 호출하여 실행을 재개하는 private 클래스를 만듭니다. 아래의 클래스에서 이와 비슷한 것을 확인할 수 있습니다.
invokeSuspend
가 Continuation
객체의 정보로 loginUser
함수를 다시 호출하므로 loginUser
함수의 시그니처 파라미터는 nullable입니다. 이 시점에서 컴파일러는 상태 간 이동 방법에 대한 정보를 추가하기만 하면 됩니다.
첫 번째로 해야 할 일은 1. 함수를 처음 호출하는 것인지, 2. 함수를 이전 상태에서 재개한 것인지를 아는 것입니다. 전달된 continuation이 LoginUserStateMachine
타입인지 여부를 확인하여 다음 작업을 수행합니다.
처음 호출된 경우 새로운 LoginUserStateMachine
인스턴스를 생성하고 수신한 completion
를 매개 변수로 저장하여 이 인스턴스를 호출하는 기능을 다시 시작한다는 것을 기억해야 합니다. 그렇지 않으면, 그것은 단지 상태 머신(일시 정지 기능)의 실행을 계속할 것입니다.
이제 컴파일러에서 상태 간 이동과 정보 공유를 위해 생성하는 코드를 살펴봅시다.
위의 코드를 살펴본 후 이전 코드 부분과 차이점을 찾아봅시다. 컴파일러에서 생성하는 내용을 주의 깊게 살펴봅시다.
when
문의 인수는LoginUserStateMachine
인스턴스 내부의 라벨입니다.- 새로운 상태가 처리될 때마다, 이 기능이 정지되어 실패했을 경우를 확인합니다.
- 다음 suspend 함수(
logUserIn
)를 호출하기 전에LoginUserStateMachine
인스턴스의 라벨이 다음 상태로 업데이트 됩니다. - 이 상태 머신 내부에서 다른 suspend 함수에 대한 호출이 있을 때
continuation
(LoginUserStateMachine
) 인스턴스가 매개 변수로 전달됩니다. 호출된 suspend 함수는 컴파일러에 의해 변경되고continuation
객체를 파라미터로 가지는 같은 유형의 또 다른 상태 머신에 의해 관리되고 suspend 함수의 상태 머신이 완료되면, 다시 이 상태 머신의 실행을 재개합니다.
아래 코드에서 볼 수 있듯이, 이 함수를 호출한 함수의 실행을 재개해야 하므로 마지막 상태는 다릅니다. LoginUserStateMachine
에 저장된 상수에 의해 다시 시작됩니다.
이렇게 suspend 함수에서 코틀린 컴파일러는 많은 일을 하고 있습니다.
Conclusion
코틀린 컴파일러는 모든 suspend 함수를 상태 머신으로 변환하여, 함수가 일시 중단될 때마다 콜백을 사용하여 최적화하는 작업을 합니다.
컴파일러가 내부에서 무엇을 하는지 알고 있다면면, 왜 이것이 시작된 모든 작업이 완료될 때까지 suspend 함수가 리턴값을 반환하지 않는 이유를 더 잘 이해할 수 있을 것입니다. 또한 코드가 스레드를 차단하지 않고 일시 중단할 수 있는 이유는 함수가 재개될 때 실행해야 할 사항에 대한 정보가 계속 객체에 저장되어 있기 때문입니다.