[Coroutine] 중단과 재개 (with. CPS 패턴)

Ham's Velog·2024년 6월 18일
post-thumbnail

이전 포스팅에서 살펴본것처럼 suspendCancellableCoroutinesuspendCoroutine의 공통점은 코루틴 스코프 내부에서 '중단과 재개' 를 지원하는 것입니다.
이 뿐만 아니라 코틀린 함수의 가시성 수정자 중 suspend 키워드가 붙은 함수(함수 내부에서 중단을 나타내는 코드가 존재할 시 [예시] delay )도 '중단과 재개'를 지원할 수 있습니다.

위에 언급한 함수들이 '중단과 재개' 가 가능한 이유는 Continuation 객체를 활용하기 때문이었는데요, 이번 포스팅에서는 코루틴 내부에서 중단 가능한 함수들이 Continuation을 어떻게 활용하는지 살펴보겠습니다.

Continuation-Passing Style (CPS 패턴)

코틀린 컴파일러는 suspend 함수State Machine으로 변환하여 CPS 패턴을 구현합니다.
State Machine은 함수의 중단 지점을 상태로 관리하고 함수가 재개될 때 적절한 상태에서 실행을 재개합니다.

이해하기 쉽게 코드로 설명해보겠습니다.

suspend fun myFunction() {
	println("Before")
    delay(1000) // 중단 함수
    println("After")
}

해당 코드에서는 중단 가능한 함수인 myFunction() 함수내에 중단 지점을 나타내는 delay 함수와 앞 뒤로 출력문을 넣어주었습니다.

해당 코드를 컴파일 하면 다음과 같은 형태로 코드가 변환됩니다.
(이해를 돕도록 코틀린으로 나타내어 보았습니다.)

// 변환된 myFunction 함수 - 'State Machine' 으로 변경
fun myFunction(continuation: Continuation<Unit>): Any { // 함수의 시그니처 변경 - 'Continuation' 매개 변수 추가
	val continuation = continuation as? MyFunctionContinuation
    	?: MyFunctionContinuation(continuation)
        
    if (continuation.label == 0) {
    	println("Before")
        
        continuation.label = 1 // 중단 되기 전 `label` 값 증가
        
        /* 
           `COROUTINE_SUSPENDED`은 실제로 
           'kotlin.coroutines.intrinsics' 패키지 내부에
           `val COROUTINE_SUSPENDED: Any`라고 정의되어 있습니다.
        */
        if (delay(1000, continuation) == COROUTINE_SUSPENDED) { // 중단함수 `delay()`로 인한 중단 지점 생성
        	return COROUTINE_SUSPENDED 
        }
    }
    
    if (continuation.label == 1) {
    	println("After")
        return Unit
    }
    
    error("Impossible")
}

// 'State Machine'에 활용되는 MyFunctionContinuation 객체 - 'State'로 활용
class MyFunctionContinuation(
	val completion: Continuation<Unit>
} : Continuation<Unit> {
	
    override val context: CoroutineContext
    	get() = completion.context
    
    var label = 0
    var result: Result<Any>? = null
    
    override fun resumeWith(result: Result<Unit>) { // 중단된 코루틴을 재개해주는 함수
    	this.result = result
        
        val res = try {
        	val r = myFunction(this) // `myFunction()` 함수를 다시 호출
            
            if (r == COROUTINE_SUSPENDED) {
            	return Result.success(r as Unit)
            }
        } catch (e: Throwable) {
        	Result.failure(e)
        }
        completion.resumeWith(res)
    }
}

함수의 흐름을 따라가면 최초 실행된 myFunction() 함수는 if (delay(1000, continuation) == COROUTINE_SUSPENDED)... 부분에서 COROUTINE_SUSPENDED 라는 '중단 상태'를 나타내는 상수를 반환하며 종료가 됩니다.
종료가 되기 전, label 값을 증가 시킨 MyFunctionContinuation 클래스를 delay 중단 함수의 인자로 넣어 delay 함수를 실행시키는 모습을 확인할 수 있습니다.

그렇다면 어떻게 'Before'를 출력한지 1초 뒤에 'After'를 출력할 수 있었을까요?
'After'를 출력하기 위해선 MyFunctionContinuationlabel이 1이 된 상태로 myFunction() 함수를 호출해야만 합니다.

사진과 같이 delay 함수에서는 내부적으로 suspendCancellableCoroutine을 사용하여 중단된 코루틴을 재개 해주는 모습을 확인할 수 있습니다.
(scheduleResumeAfterDelay 함수 내부 구현을 확인해보면 CancellableContinuation 인터페이스에 정의된 CoroutineDispatcher의 확장함수 resumeUndispatched를 호출하는 것을 확인할 수 있습니다.)

이전 포스팅에서 간략하게 살펴본것 처럼 suspendCancellableCoroutineContinuation 인터페이스의 확장 인터페이스인 CancellableContinuation을 람다 매개변수로 가지고 있습니다.
특히 CancellableContinuation에서는 다양한 재개 함수를 가지고 있어, 이를 통해 중단된 코루틴을 재개 시킬수 있습니다.
때문에 delay 함수 내부 깊숙한 곳에서 컴파일 된 myFunction() 함수의 상태(MyFunctionContinuation)를 가지고 중단된 코루틴을 다시 재개하는 것을 알 수 있습니다.

정리

  • suspend 키워드가 붙은 함수는 컴파일시 함수 시그니처에 Continuation 매개변수가 추가된다.
  • State Machine은 함수의 실행 상태를 추적하며 Continuation 객체의 label 값을 통해 중단 및 재개 지점을 관리한다.
  • COROUTINE_SUSPENDED 상수는 코루틴이 중단된 상태임을 나타내고 특정 조건이 충족되면 Continuation 객체의 resume 메서드를 통해 코루틴이 재개된다.
  • 중단이 가능한 함수들은 재개할수 있는 매커니즘을 가지고 있다.

참고 문헌 - 코틀린 코루틴

profile
#안드로이드개발자

0개의 댓글