Suspend 한정자 — 내부 동작

hongbeom
hongbeomi dev
Published in
7 min readJun 24, 2020

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 클래스를 만듭니다. 아래의 클래스에서 이와 비슷한 것을 확인할 수 있습니다.

invokeSuspendContinuation 객체의 정보로 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 함수가 리턴값을 반환하지 않는 이유를 더 잘 이해할 수 있을 것입니다. 또한 코드가 스레드를 차단하지 않고 일시 중단할 수 있는 이유는 함수가 재개될 때 실행해야 할 사항에 대한 정보가 계속 객체에 저장되어 있기 때문입니다.

읽어주셔서 감사합니다! 🙌

--

--