Coroutine

[Coroutine] 8. 코루틴의 동작 방식과 일시 중단 함수

Marchbreeze 2025. 5. 20. 04:11
<코틀린 코루틴의 정석(조세영)> 도서의 "9장. 일시 중단 함수", "11장. 코루틴 심화"을 참고해서 작성된 글입니다.

 


요약

코루틴은 CPS 기반 Continuation으로 내부 상태를 저장 및 전달하며, suspendCancellableCoroutine을 통해 일시 중단과 재개를 직접 제어할 수 있습니다.

suspend fun 키워드로 정의된 일시중단 함수는 코루틴 내부에서 실행 가능한 재사용 가능한 비동기 작업 단위입니다.

일시중단 함수는 코루틴에 의해 실행될 때만 비동기 제어를 받으며, 내부에서 코루틴 빌더를 안전하게 호출할 수 있습니다.


 

1. 코루틴의 동작 방식

Continuation Passing Style(CPS)

코루틴은 일시 중단했다가 재개할 수 있도록, 실행 흐름과 상태 정보를 Continuation 객체에 저장하고 전달하는 방식으로 동작합니다.

컴파일러는 suspend 키워드가 붙은 함수 호출 지점을 CPS로 변환하여, 내부에서 Continuation을 넘기고 resumeWith로 재개합니다.

개발자는 고수준 API(launch, async, withContext, delay 등)를 사용하지만, 이들 모두 CPS를 통해 동작합니다.

 

suspendCancellableCoroutine

Continuation 객체를 CancellableContinuation 타입으로 제공하는 suspendCancellableCoroutine 함수로 확인할 수 있습니다.

fun main() = runBlocking<Unit> {
  println("runBlocking 코루틴 일시 중단 호출")
  suspendCancellableCoroutine<Unit> { continuation: CancellableContinuation<Unit> ->
    println("일시 중단 시점의 runBlocking 코루틴 실행 정보: ${continuation.context}")
    continuation.resume(Unit) // 코루틴 재개 호출
  }
  println("runBlocking 코루틴 재개 후 실행되는 코드")
}
/*
runBlocking 코루틴 일시 중단 호출
일시 중단 시점의 runBlocking 코루틴 실행 정보: [BlockingCoroutine{Active}@551aa95a, BlockingEventLoop@35d176f7]
runBlocking 코루틴 재개 후 실행되는 코드
*/

 

코루틴 재개 시 다른 작업으로부터 결과를 수신받아야하는 경우, 타입 인자에 결과로 반환되는 타입을 입력할 수 있습니다.

fun main() = runBlocking<Unit> {
  val result = suspendCancellableCoroutine<String> { continuation: CancellableContinuation<String> ->
      thread { // 새로운 스레드 생성
        Thread.sleep(1000L) // 1초간 대기
        continuation.resume("실행 결과") // runBlocking 코루틴 재개
      }
    }
  println(result) // 코루틴 재개 시 반환 받은 결과 출력
}
/*
실행 결과
*/

 

 


 

2. 일시 중단 함수

suspend function

함수 내에 일시 중단 지점을 포함할 수 있는 함수입니다.

일시 중단 함수는 코루틴 내부 또는 다른 suspend 함수 안에서만 호출할 수 있습니다.

재사용 가능한 비동기 작업 단위로, 복잡한 비동기를 깔끔하게 분리하고 테스트하기에 유용합니다.

fun main() = runBlocking<Unit> {
  repeat(100) { delayAndPrint() }
}

suspend fun delayAndPrint() {
  delay(1000L)
  println("Hello World")
}

 

자체로는 코루틴이 아니며, 코루틴에 의해 실행될 때만 suspend 제어 권한(블로킹 없이 일시중단 또는 재개)을 가집니다.

fun main() = runBlocking<Unit> {
  launch {
    delayAndPrintHelloWorld()
  }
  println(getElapsedTime())
}

suspend fun delayAndPrintHelloWorld() {
  delay(1000L)
  println("Hello World")
}

/*
// 결과:
지난 시간: 3ms
Hello World
*/
  1. launch 함수가 호출돼 생성된 코루틴들은 delayAndPrintHelloWorld 함수의 호출로 1초 간 스레드 사용 권한을 양보합니다.
  2. 자유로워진 스레드는 다른 코루틴인 runBlocking 코루틴에 의해 사용될 수 있으므로 곧바로 getElapsedTime이 실행됩니다.

 

coroutineScope

기본적으로 일시 중단 함수에서 코루틴 빌더를 호출하는 경우, 순차적으로 실행됩니다.

비동기 처리를 위해서는 내부에서 async 코루틴 빌더를 사용해야 하지만, launch/async CoroutineScope 확장 함수이며, suspend 함수 본문에는 this: CoroutineScope가 없어 호출할 수 없습니다.

따라서 coroutineScope를 사용해 새로운 CoroutineScope를 일시중단 함수 내부에 제공한 후 사용해야 합니다.

suspend fun searchByKeyword(keyword: String): Array<String> = 
    coroutineScope { // this: CoroutineScope
      val dbResultsDeferred = async {
        searchFromDB(keyword)
      }
      val serverResultsDeferred = async {
        searchFromServer(keyword)
      }
      return@coroutineScope arrayOf(*dbResultsDeferred.await(), *serverResultsDeferred.await())
    }
/*
[결과] [[DB]Keyword1, [DB]Keyword2, [Server]Keyword1, [Server]Keyword2]
*/

 

supervisorScope

coroutineScope는 async에서 빌드된 코루틴에서 오류가 발생하는 경우, 부모 코루틴으로 전파되어 모두 취소시키는 문제점이 있습니다.

이에 suspend 함수 내부에 SupervisorJob 기반 스코프를 생성하는 supervisorScope를 활용하여 예외 전파를 제한합니다.

또한 Defered 객체는 await 함수 호출 시 추가로 예외를 노출하므로, try-catch문을 통해 제어해줍니다.

suspend fun searchByKeyword(keyword: String): Array<String> = 
    supervisorScope { // this: CoroutineScope
      val dbResultsDeferred = async {
        throw Exception("dbResultsDeferred에서 예외가 발생했습니다")
        searchFromDB(keyword)
      }
      val serverResultsDeferred = async {
        searchFromServer(keyword)
      }

      val dbResults = try {
        dbResultsDeferred.await()
      } catch (e: Exception) {
        arrayOf() // 예외 발생 시 빈 결과 반환
      }

      val serverResults = try {
        serverResultsDeferred.await()
      } catch (e: Exception) {
        arrayOf() // 에러 발생 시 빈 결과 반환
      }

      return@supervisorScope arrayOf(*dbResults, *serverResults)
    }
/*
[결과] [[Server]Keyword1, [Server]Keyword2]
*/