루피도 코딩한다

[Coroutine Basics 7] Suspending function 본문

Coroutine

[Coroutine Basics 7] Suspending function

xiaolin219 2024. 1. 24. 00:44

Suspend함수는 Suspend함수 내부에서 호출 가능하다. delay() 또한 suspend 함수의 일종이다.

아래 코드에는 suspend라는 키워드가 명시적으로 사용되고 있지 않은데 어떻게 delay()를 활용할 수 있을까?

fun main(): Unit = runBlocking {
  launch {
    delay(100L)
  }
}

그 이유는 runBlocking에 있다. 지난 포스트에서도 한번 봤었지만 다시 runBlocking 함수의 내부 구조를 살펴보도록 하자!

Builders.kt 내부

public actual fun <T> runBlocking(
  context: CoroutineContext, 
  block: suspend CoroutineScope.() -> T)
: T { ...}

여기서 block 부분을 보면 함수 타입에 suspend가 붙어있다. 이러한 것을 Suspending lambda 라고 한다.

그리고 이렇게 suspend 키워드가 붙어 함수가 중지 되었다가 정지 될 수 있음을 명시해 주는데, 이를 suspension point라고 한다.

그래서 suspend를 어떻게 야무지게 사용할 수 있을까?

먼저 아래 코드를 봐보자. Main() 함수에서는 result1과 result2의 type이 Defereed 로 추론된다. 따라서 실제로 apiCall의 결과만 궁금한 main함수에서, 어떤 비동기처리 라이브러리에 의존했는지에 대한 정보까지 알아야 하는 셈이 된다.

적절한 비유일지는 모르겠지만..! Android의 viewmodel에서 coroutine을 활용하고자 할 때, 그 사실을 UI(Activity, Fragment)에서 알게하는 느낌일수 있을듯하다. 아래와 같은 구조로 바꿔서 api를 호출하는 곳에서는 어떤 비동기 처리를 사용할지 모르게 만들어 의존성을 낮출 수 있다.

이를 suspend키워드를 활용해 비교적 라이브러리에 대한 의존성이 낮은 코드로 변경할 수 있다.

fun main(): Unit = runBlocking {
  val result1 = async {
    apiCall1()
  }
  val result2 = async {
    apiCall2(result1.await())
    }
  printWithThread(result2.await())
}

fun apiCall1(): Int {
  Thread.sleep(1_000L)
  return 100
}

fun apiCall2(num: Int): Int {
  Thread.sleep(1_000L)
  return num * 2
}
fun main(): Unit = runBlocking {
  val result1 : Int = apiCall1() // 순수한 코틀린 type에만 의존할 수 있게 된다(<-> Deferred 객체)
  val result2 = apiCall2(result1)
  printWithThread(result2)
}
suspend fun apiCall1(): Int {
  return CoroutineScope(Dispatchers.Default).async {
    Thread.sleep(1_000L)
    100
  }.await()
}

// cf. CompletableFuture는 Java에서 제공하는 비동기 프로그래밍 지원 클래스이다.
suspend fun apiCall2(num: Int): Int {
  return CompletableFuture.supplyAsync {
    Thread.sleep(1_000L)
    num * 2
  }.await()
}

suspend 키워드를 활용해 apiCall1()apiCall2() 내부에서 어떤 비동기 라이브러리 구현체를 사용했는지에 대한 정보를 몰라도 main() 함수를 작성할 수 있는 구조가 되었다.

참고 by GPT

apiCall1의 경우 coroutine 라이브러리를 활용한 함수고, apiCall2의 경우 CompletableFuture를 활용한 함수인데 두 함수가 공통적으로 await라는 함수를 활용할수 있는 이유는 무엇일까?

Kotlin은 다양한 비동기 라이브러리와의 통합을 지원하는 어댑터를 활발하게 제공한다. 예를 들어, Deferred는 Kotlin의 async와 함께 사용되는 코루틴 라이브러리에서 제공하는 인터페이스이며, CompletableFutureJava에서 제공하는 비동기 라이브러리이지만, await 함수를 통해 이러한 라이브러리 간의 상호 운용성을 쉽게 달성할 수 있다고 한다

[suspend function 1] coroutineScope

  • <-> launch와 async와 비교
    • 공통점 : 새로운 코루틴을 만듦
    • 차이점 : 주어진 함수 블록이 바로 실행됨
  • 새로 생긴 코루틴과 자식 코루틴이 모두 완료된 이후 반환된다

[suspend function 2] withContext

  • 새로운 코루틴 만들기 + 코드 블록이 즉시 호출 && 코루틴이 완전히 종료되어야 반환
  • coroutineScope와 유사하나 context의 변화를 줄 수 있음
  • withContext(NonCancellable)을 이용해 취소 불가능한 블록을 만들 수 있다

[suspend function 3] withTimeout

fun main() = runBlocking {
  withTimeout(1_000L){
    delay(1_500L)
    30
  }
}

// TimeoutCancellationException 발생
// Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
  • Try- catch로 exception을 핸들링 하거나, 아래의 withTimeoutOrNull 사용하는 방법이 있음

[suspend function 4] withTimeoutOrNull

  • timeout 발생 시 null 반환 -> elvis 연산자로 (exception 보다 쉽게) 핸들링 가능

  • fun main() = runBlocking {
        val result = withTimeoutOrNull(500L) {
            doCount()
            true
        } ?: false ✅
        println(result)
    }

(부록) Kotlin Expression Body

Kotlin에서 함수나 프로퍼티 등을 정의할 때 중괄호 {} 블록 대신에 등호 = 뒤에 짧은 표현식을 사용하여 정의하는 문법을 "expression body"라고 한다. 이를 통해 간결하고 명료한 코드를 작성할 수 있습니다. Expression body는 간단한 함수나 프로퍼티 정의에서 특히 유용하다.

// 일반적인 코드
fun add(a: Int, b: Int): Int {
    return a + b
}
// expression body 코드
fun add(a: Int, b: Int): Int = a + b
// return type 생략 코드 (type 추론 가능)
fun add(a: Int, b: Int) = a + b

이렇게 나타내는 표현 방식을 익숙하게 쓰고 있었지만 'Expression Body'라는 표현이 낯설게 느껴져 한번 정리해보았다

// 아래 두 코드가 동일
suspend fun doSomething() = coroutineScope { ... }
suspend fun doSomething() {
   coroutineScope{...} 
}
Comments