코루틴의 개념, suspend 함수

코틀린의 코루틴

코틀린 코루틴은 비동기 프로그래밍을 위한 강력한 도구입니다.

코루틴은 코드의 일부를 일시 중단하고 나중에 재개할 수 있는 경량 스레드로 볼 수 있습니다. 코루틴은 스레드 하나에서 여러 개의 코루틴으로 나눠서 병렬로 수행할 수 있습니다. 즉, 스레드에서 수행되는 개별적인 작업의 단위로 각각의 작업을 수행하는 것을 코루틴이라고 합니다.

코루틴은 코틀린에만 존재하는 것이 아닙니다. 이미 오래전에 1990년대부터 존재하던 개념이고, c, c++, javascript, go 등 현존하는 모든 프로그래밍 언어에 존재하지만, Kotlin 에는 도입되었지만 아직 Java 에는 도입되지 않았습니다.

suspend 함수

suspend 함수는 코루틴을 구현하는 데 중요한 역할을 합니다.

suspend 함수를 선언하려면 suspend 라는 키워드를 함수 앞에 붙여서 선언하면 suspend 함수가 됩니다. 이렇게 하면 컴파일러가 suspend 가 붙은 함수를 코루틴 내에서 사용할 수 있는 함수로 인식합니다. 코루틴 내에서 일시 중단이 가능한 모든 작업은 반드시 suspend 함수에서 수행되어야 합니다.

suspend 함수는 뭐고 코루틴은 뭔가요?

코틀린에서는 suspend 키워드를 붙인 함수가 코루틴으로 실행됩니다. 코루틴은 일시 중단할 수 있고 재개할 수 있는 개념이며 스레드보다 작은 단위의 개념입니다.

코루틴은 코루틴 내에서 여러 개의 코루틴이 분기할 수 있는데 이때 여러 개의 개별 코루틴 컨텍스트로 동작하거나 개발자가 직접 지정한 코루틴 컨텍스트 내에서 함께 동작하거나 하는 것을 지정하는 것이 가능합니다.

이런 코루틴을 사용하기 위해서는 suspend 키워드가 붙어 있어야만 가능하며 suspend 키워드는 함수에 붙일 수 있습니다. suspend 함수는 코틀린 언어가 실행될 수 있는 환경인 JVM 에 의해 실행됩니다.

코틀린이 기본으로 제공하는 라이브러리 들 중 대표적인 suspend 기반의 함수들에는 launch, async , runBlocking, withContext 등이 있는데 이들 모두를 코루틴 빌더라고 이야기합니다.

launch 함수는 반환값이 없는 Fire-and-forget 스타일의 코루틴을 생성하고 실행하며, async 함수는 비동기 작업의 결과를 리턴하는 코루틴을 만들어냅니다.

코루틴은 멀티 스레딩과 함께 사용되어 병렬 처리를 가능하게 하지만, 코루틴은 기본적으로 단일 스레드에서 실행됩니다. 따라서 코루틴은 비동기 작업을 보다 간편하게 처리할 수 있는 동시성 프로그래밍 모델을 제공합니다.

코틀린의 코루틴은 비동기적인 코드를 작성하고 관리하는 데 매우 유용한 도구이며, 코틀린 표준 라이브러리에 포함되어 있어 바로 사용할 수 있습니다.


코틀린에서 기본적으로 제공하는 suspend 함수들

코틀린에서 기본적으로 제공하는 대표적인 suspend 함수는 kotlinx.coroutines패키지 내의 Builders.common.kt에 있으며 launch, async, withContext 등이 있습니다.


async, launch, withContext 는 CoroutineScope 의 확장함수

async, launch, withContext 함수는 suspend 함수라고 부르기도 합니다. 한가지 알아두고 넘어가야 할 것은 async, launch, withContext 는 CoroutineScope 안에서 동작한다는 점입니다. 그리고 async, launch,withContext 는 CoroutineScope 의 확장함수로 선언되어 있습니다.


suspend 함수의 주요 특징

suspend 함수의 주요 특징은 아래와 같습니다.

  • suspend 함수는 일시 중단할 수 있는 함수입니다.
    • suspend 가 적용된 함수가 실행되는 동안 일시 중단될 경우, suspend 를 실행하는 동안 내부적으로는 다른 작업을 수행하거나 대기하게 됩니다. 즉, 논블로킹 기반으로 동작합니다. suspend 함수를 호출할 때에는 이 suspend 함수를 호출한 코루틴이 일시 중단됩니다.
  • suspend 함수를 사용하면 비동기적인 작업을 수행하는 동안 UI를 블로킹하지 않고도 작업을 수행할 수 있습니다.
    • 예를 들어 네트워크 호출이나 파일 I/O와 같은 작업을 수행할 때 suspend 함수를 사용하여 코루틴을 일시 중단하고 나중에 결과를 받아서 처리할 수 있습니다.
  • suspend 가 적용된 함수는 suspend 가 붙은 함수에서만 호출이 가능합니다.
    • Spring Webflux 는 Controller 에 suspend 함수를 지원합니다. 따라서 Controller 내에서는 suspend 함수를 실행이 가능합니다.
    • suspend 가 적용되지 않은 함수이지만 라이브러리 함수여서 수정이 불가능하고 호출역시 불가능할 경우가 있습니다. 이 경우 아래의 두 방식으로 일반함수내에서 suspend 함수를 감싸서 실행하는 방식으로 사용이 가능합니다.
      • kotlin-coroutines-reactor 의 mono 함수 내부에서 suspend 함수를 실행
      • CoroutineScope(CoroutineDispatcher).future { ... } 를 사용하는 방식

suspend 기반의 kotlin 내장 함수들

async, launch, withContext 말고도 코틀린의 kotlinx-coroutines-core 에서 제공하는 suspend 가 적용된 함수들은 많습니다. kotlin 라이브러리에서 제공하는 대표적으로 잘 알려진 suspend 함수들은 아래와 같습니다.

  • withContext : 코루틴 컨텍스트를 변경하는 함수입니다. 다른 스레드에서 코드를 실행하거나, 특정 디스패처에 작업을 보내는 것이 가능합니다.
  • delay : 일정 시간 동안 코루틴을 일시 중단하는 함수입니다. 주로 테스트나 간단한 딜레이를 구현할 때 사용됩니다.
  • async: 비동기 작업의 결과를 반환하는 코루틴을 생성합니다. 생성된 코루틴은 Deferred 객체를 반환하며, 이를 통해 결과를 가져올 수 있습니다.
  • await: Deferred 객체의 결과를 기다리는 함수로, async 함수로 생성된 Deferred 객체의 결과를 가져올 때 사용됩니다.
  • launch: 백그라운드에서 비동기적으로 새로운 코루틴을 실행하는 함수로, 반환 값이 없는 Fire-and-forget 스타일의 코루틴을 생성합니다.
  • runBlocking: 새로운 블록 내에서 코루틴을 실행하는 함수로, 주로 메인 함수나 테스트 코드에서 사용되며, 코루틴을 기다리는 동안 블로킹을 유지합니다.
  • coroutineScope: 지정된 블록 내에서 새로운 코루틴 스코프를 생성하는 함수로, 지정된 블록 내의 코루틴이 완료될 때까지 대기합니다.
  • select: 여러 개의 중단 가능한 조건을 동시에 처리할 수 있는 함수로, 먼저 발생하는 이벤트를 처리하고 나머지는 무시합니다.
  • supervisorScope: 자식 코루틴이 실패하더라도 부모 코루틴이 중단되지 않도록 하는 슈퍼바이저 스코프를 생성하는 함수로, 자식 코루틴의 실패를 격리합니다.

suspend 함수는 어디에서 실행되나요?

중단된 suspend 함수는 재개될 때 CoroutineDispatcher 가 어느 스레드에서 실행할지 결정합니다.

CoroutineDispatcher 역시 CoroutineContext 의 일종입니다. CoroutineContext 에는 CoroutineName, Job, ThreadLocalElement, ReactorContext 등이 있습니다.

CoroutineContext 의 개념과 원리, 예제, CoroutineDispatcher 의 개념, 종류 등의 개념은 이 블로그 내에서 별도의 카테고리에 분류해서 정리하고 있습니다. 정리가 완료되면 링크를 추가하겠습니다.

TOOD : 링크 추가


suspend 함수를 통해 만들어진 코루틴은 하나의 스레드에서 실행되다가 중지된 후 다른 스레드에서 이어서 실행할 수 있습니다. 비동기연산을 논 블로킹하게 수행하기 때문입니다.

TODO : 그림 추가


suspend 함수는 비동기인가요 ? 동기 연산인가요?

suspend 함수는 비동기 연산으로 수행되게끔 할 수도 있고 비동기 코드인데 동기연산처럼 수행하게끔 await() 등의 함수를 사용해서 동기코드처럼 사용하는 것도 가능합니다. 다만 이렇게 비동기 코드를 동기 코드 처럼 순차적으로 처리되는 식으로 작성한 코드를 흔히 블로킹 방식 코드로 착각하게 된다는 점을 주의해야 합니다.

비동기 적인 연산을 하는 suspend 함수를 동기 코드 처럼 동작하도록 await() 함수 등을 사용할 때, await() 함수에 대한 IO Bound 작업이 끝날 때 까지 뒷단에서 코루틴 디스패처는 다른 작업들을 관리해서 수행시키고 조율시킵니다.


FSM, CPS, suspend 함수

suspend 가 붙은 함수를 suspend 함수 없이 내부적인 구현을 구현하면 결국은 FSM(Finite State Machine), CPS(Continuation Passing Style) 을 기반으로 한 재귀 호출구문으로 이뤄지게 됩니다.

FSM 이라는 것은 유한 상태 기계라는 의미인데 일종의 label 을 통해 상태를 인식해서 다음 상태로 넘어가게 해주는 것을 의미하고 CPS 는 다음 상태로 지속이 가능한 객체인 Continuation 을 넘겨주는 방식을 의미합니다. Continuation 객체 내에는 주로 데이터를 처리하거나 바인딩하기 위한 구조체 같은 데이터가 포함됩니다.


더 자세한 내용은 FSM, CPS 문서에 따로 정리해두겠습니다.


suspend 함수 예제

suspend 함수는 일시 중단할 수 있는 함수

suspend 함수는 일시 중단하는 함수입니다. 아래는 그 코드입니다.

 

그런데 이렇게 일시중단한다고 해서 코드 자체가 블로킹 상태가 되는 것이 아닙니다. 블로킹이라고 하는 것은 다른 작업을 수행하지 못하는 상태를 의미하는데, suspend 함수로 일시 중단 상태가 되었을 때 뒷단에서는 코루틴 디스패처 안에 쌓여있는 다른 작업을 수행하면서 suspend 함수의 동작이 끝나기를 기다립니다.

이렇게 하는 이유는 현대적인 애플리케이션들이 계산작업으로 인한 CPU 점유 보다는 IO로 인한 응답대기를 하는 것으로 인한 CPU 점유가 더 높기 때문입니다. IO의 응답이 오기 전까지 스레드 하나를 차지하고 있는 것은 낭비이기 때문에 하나의 스레드 내에서 여러 개의 코루틴으로 분리하고 하나의 코루틴이 IO 작업을 완료하기 전까지 다른 코루틴을 코루틴 디스패처가 분배하면서 작업을 합니다.

저 역시도 코루틴의 suspend 함수를 학습하면서 위의 코드가 블로킹이라고 착각했었고 코틀린을 배우느라 알게된 패캠 강사님도 위의 코드를 처음에는 블로킹 코드로 착각했다고 하셨었습니다. 많은 사람들이 착각하는 부분입니다. 다른 일을 오랫동안 하다가 다시 코틀린 코루틴을 다룰 때도 까먹었다가 다시 생각날 수도 있는 개념이기에 코루틴은 블로킹이 아니라 논블로킹이고 작업을 중단해두었지만, 내부적으로는 다른 작업을 처리하게끔 해둔 상태에서 중단한 것이라고 개념을 주의해서 기억해둬야 할 필요가 있습니다.


suspend 함수는 논블로킹 기반의 비동기 연산을 수행


suspend 함수는 suspend 함수에서만 호출 가능

Spring Webflux 의 에서는 suspend 를 지원

Spring Webflux 는 Controller 에 suspend 함수를 지원합니다. 따라서 아래 코드와 같이 Controller 내에서는 suspend 함수를 실행이 가능합니다.

package io.chagchagchag.demo.kotlin_coroutine
 
// ...
 
@RestController
@RequestMapping("/healthcheck")
class HealthCheckController {
 
  private suspend fun ok(): String = "OK"
 
  @GetMapping("/ready")
  suspend fun ready(): String{
    return ok()
  }
}

Mono, CompletableFuture 를 반환하는 라이브러리 함수가 suspend 함수가 아닐 경우

  • kotlin-coroutines-reactor 의 mono 함수 내부에서 suspend 함수를 실행
  • CoroutineScope(CoroutineDispatcher).future { ... } 를 사용하는 방식
    • CoroutineScope(CoroutineDispatcher).future { ... } 내에서 suspend 함수를 실행하고, 반환되는 CompletableFuture를 thneAccept 등을 통해서 실행

TODO : 그림 추가