Post

coroutine 알아보기 (7) - suspending function

suspending function

suspending function

suspending function이란 우리가 지금까지 사용해왔던 것으로 suspend 지시어가 붙은 함수를 의미합니다.

suspend 함수는 코루틴 내부에서 실행되며, 특정 시점에서 실행을 멈추고 다시 재개(resume)될 수 있는 함수입니다. 단순히 suspend 키워드가 붙었다고 해서 무조건 중지되는 것은 아니며, 중지될 수도 있고 중지되지 않을 수도 있습니다.

Suspend 함수 호출 구조

1
2
3
4
5
fun main(): Unit = runBlocking {
    launch {
        delay(100L)
    }
}

위 코드에서 launch의 함수 시그니처를 보면, suspend CoroutineScope.() -> Unit 형태의 함수를 받습니다. 이는 suspending lambda라고 불립니다.

  • launch 시그니처
    1
    2
    3
    4
    5
    
      public fun CoroutineScope.launch(
          context: CoroutineContext = EmptyCoroutineContext,
          start: CoroutineStart = CoroutineStart.DEFAULT,
          block: suspend CoroutineScope.() -> Unit
      ): Job
    

Suspend 함수 실행

suspend 키워드를 가진 함수는 일시 중단(suspend) 될 수 있는 함수이며, 코루틴 내부에서 실행될 수 있습니다. 이 함수는 특정 시점에서 실행을 멈추고, 나중에 다시 재개(resume)될 수 있습니다.

즉, suspend 함수를 호출한다고 해서 무조건 중지되는 것이 아니고, 중지가 될 수 있고, 중지가 안될 수도 있다는 의미입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fun main(): Unit = runBlocking {
    launch {
        a()
        b()
    }

    launch {
        c()
    }
}

suspend fun a() {
    printWithThread("a")
}

suspend fun b() {
    printWithThread("b")
}

suspend fun c() {
    printWithThread("c")
}

// [main] a
// [main] b
// [main] c

suspend 함수는 호출된다고 해서 반드시 중지되는 것이 아닙니다. 위 예제에서 a(), b(), c()가 차례대로 실행되는 것을 볼 수 있습니다.

Suspend 함수를 활용한 비동기 API 호출

이것을 REST API를 호출해야하는 과정에 대입해보겠습니다. 첫 번째 API 호출에서 나온 결과를 두 번째 API 호출 때 사용해야 할 경우로 대입해서 코드를 작성해보았습니다.

  • Deferred 사용
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
      fun main(): Unit = runBlocking {
          val result1 = async {
              call1()
          }
          val result2 = async {
              call2(result1.await())
          }
    
          printWithThread(result2.await())
      }
    
      fun call1(): Int {
          Thread.sleep(1000)
          return 100
      }
    
      fun call2(num: Int): Int {
          Thread.sleep(1000)
          return num * 2
      }
    

async 와 Deferred 를 활용해 콜백을 활용하지 않고 코드를 작성하였습니다. 하지만, runBlocking 입장에서 result1 과 result2 의 타입이 Deferred 이기에 Deffered 에 의존적인 코드가 되버립니다.

이러면 Deferred 대신에 CompletableFuture 또는 Reactor 와 같은 다른 비동기 라이브러리 코드로 변경될 때 취약할 수 있습니다.

이럴 때 suspend 함수를 활용한다면 아래처럼 변경이 가능합니다.

  • Suspend 함수 적용 예제

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
      fun main(): Unit = runBlocking {
    
          // 반환타입이 deferred가 아니게 되므로 어떤 비동기 라이브러리가 와도 무방하게 됨.
          val result1 = suspendCall1()
    
          val result2 = suspendCall2(result1)
    
          printWithThread(result2)
      }
    
      suspend fun suspendCall1(): Int {
          return CoroutineScope(Dispatchers.Default).async {
              Thread.sleep(1000)
              100
          }.await()
      }
    
      suspend fun suspendCall2(num: Int): Int {
          return CompletableFuture.supplyAsync {
              Thread.sleep(1000)
              num * 2
          }.await()
      }
    

위 코드에서는 suspend 함수를 사용하여 특정 비동기 라이브러리에 대한 의존성을 제거하였습니다.

CompletableFuture.await()와 같은 코루틴 변환 함수를 활용하여, 다양한 비동기 라이브러리를 효과적으로 사용할 수 있습니다.

suspend 함수

코루틴 라이브러리에서 제공하는 suspend 함수들 중 몇가지를 설명합니다.

coroutineScope

launch 나 async 처럼 새로운 코루틴을 만들지만, 주어진 함수 블록이 바로 실행되는 특징을 갖고 있습니다.

새로 생긴 코루틴과 자식 코루틴들이 모두 완료된 이후, 반환됩니다. coroutineScope 으로 만든 코루틴은 이전 코루틴의 자식 코루틴이 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fun main(): Unit = runBlocking {

    printWithThread("start")
    printWithThread(caculateResult())
    printWithThread("end")
}

//coroutineScope 대신 withContext(Dispatchers.Default)로도 사용 가능
suspend fun caculateResult(): Int = coroutineScope {
    val num1 = async {
        delay(1000)
        10
    }

    val num2 = async {
        delay(1000)
        20
    }

    num1.await() + num2.await()
}

// [main] start
// [main] 30
// [main] end

withContext

withContext 역시 주어진 코드 블록이 즉시 호출되며 새로운 코루틴이 만들어지고, 이 코루틴이 완전히 종료되어야 반환된다. 즉 기본적으로는 coroutineScope 과 같습니다.

withContext 를 사용할 때 context에 변화를 줄 수 있어 다음과 같이 Dispatcher를 바꿔 사용할 때 활용해볼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fun main(): Unit = runBlocking {

    printWithThread("start")
    printWithThread(caculateResult())
    printWithThread("end")
}

//coroutineScope 대신 withContext(Dispatchers.Default)로도 사용 가능
suspend fun caculateResult(): Int = withContext(Dispatchers.Default) {
    val num1 = async {
        delay(1000)
        10
    }

    val num2 = async {
        delay(1000)
        20
    }

    num1.await() + num2.await()
}

// [main] start
// [main] 30
// [main] end

withTimeout, withTimeoutOrNull

coroutineScope 과 유사하지만 주어진 함수 블록이 시간 내에 완료되어야 한다는 차이점이 있습니다.

주어진 시간 안에 코루틴이 완료되지 않으면 withTimeout 은 TimeoutCancellationException을 던지게 되고, withTimeoutOrNull 은 null을 반환합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun example2(): Unit = runBlocking {
    val result: Int? = withTimeoutOrNull(1000L) {
        delay(1500)
        10 + 20
    }

    printWithThread(result!!)
}

fun example1(): Unit = runBlocking {
    val result: Int = withTimeout(1000L) {
        delay(1500)
        10 + 20
    }

    printWithThread(result)
}

// Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms

정리

  • suspend 함수는 일시 중단이 가능하며, 코루틴 내부에서 실행됩니다.
  • suspend 함수는 반드시 중지되는 것이 아니며, 특정 지점에서 중지될 수도 있습니다.
  • suspend 함수를 활용하면 특정 비동기 라이브러리에 종속되지 않는 유연한 코드 작성이 가능합니다.
  • [출처]
  • https://kotlinlang.org/docs/coroutines-overview.html
This post is licensed under CC BY 4.0 by the author.