관리 메뉴

나만을 위한 블로그

[코틀린 코루틴] 3. 중단은 어떻게 작동하는가 본문

책/코틀린 코루틴

[코틀린 코루틴] 3. 중단은 어떻게 작동하는가

참깨빵위에참깨빵 2024. 2. 14. 20:23
728x90
반응형

중단 함수(suspend fun)는 코루틴의 핵심으로, 중단이 가능하다는 건 코루틴의 다른 모든 개념의 기본이 된다.

코루틴은 중단되면 Continuation 객체를 리턴하는데 이 객체를 쓰면 멈췄던 곳에서 코루틴을 재시작할 수 있다.

또한 코루틴을 중단할 때 어떤 자원도 쓰지 않으며 다른 쓰레드에서 시작하거나 (역)직렬화할 수 있다.

 

재개

 

재개에는 당연히 코루틴이 필요하다. 이것은 안드로이드 디벨로퍼에도 작성되어 있으며 해당 링크는 하단의 재개 부분에 첨부했다. 중단 함수는 반드시 코루틴 or 다른 중단 함수에 의해 호출돼야 한다.

코루틴은 runBlocking, launch 같은 코루틴 빌더를 써서 만들 수 있다.

 

suspend fun main() {
    println("before")

    println("after")
}

// before
// after

 

두 println 사이에서 중지하면 after는 출력되지 않은 채 프로그램이 종료되지 않는다.

 

import kotlin.coroutines.suspendCoroutine

suspend fun main() {
    println("before")

    suspendCoroutine<Unit> {  }

    println("after")
}

// before

 

 

suspendCoroutine을 보면 후행 람다를 받고 있다. 인자로 들어간 람다 함수는 중단되기 전에 실행되며 이 함수는 Continuation 객체를 인자로 받는다.

 

import kotlin.coroutines.suspendCoroutine

suspend fun main() {
    println("before")

    suspendCoroutine<Unit> { continuation ->
        println("before too")
    }

    println("after")
}

// before
// before too (프로그램 계속 실행됨)

 

suspendCoroutine 함수의 후행 람다에서 쓸 수 있는 Continuation 객체를 써서 프로그램을 종료할 수 있다.

after가 호출되는 이유는 resume 메서드의 영향이다.

 

import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

suspend fun main() {
    println("before")

    suspendCoroutine<Unit> { continuation ->
        continuation.resume(Unit)
    }

    println("after")
}

// before
// after (이후 프로그램 종료)

 

resume()이 무엇인지는 아래 문서들을 참고한다.

 

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellable-continuation/resume.html

 

resume

Resumes this continuation with the specified value and calls the specified onCancellation handler when either resumed too late (when continuation was already cancelled) or, although resumed successfully (before cancellation), the coroutine's job was cancel

kotlinlang.org

Continuation을 지정된 값으로 재개(resume)하고 너무 늦게 재개됐거나 이미 Continuation이 취소된 경우, 재개에 성공했지만 코루틴의 작업이 디스패처에서 실행되기 전에 취소되서 일시 중단된 함수가 이 값을 리턴하는 대신 예외를 던질 경우 지정된 onCancellation 핸들러를 호출한다. onCancellation 핸들러는 예외를 던지지 않아야 한다...(중략)

 

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/resume.html

 

resume - Kotlin Programming Language

 

kotlinlang.org

마지막 일시 중단 지점의 리턴값으로 값을 전달하는 해당 코루틴의 실행을 재개한다

 

https://developer.android.com/kotlin/coroutines-adv?hl=ko

 

Kotlin 코루틴으로 앱 성능 향상  |  Android 개발자  |  Android Developers

Kotlin 코루틴으로 앱 성능 향상 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Kotlin 코루틴을 사용하면 네트워크 호출이나 디스크 작업과 같은 장기 실행 작

developer.android.com

(중략)...resume은 정지된 위치부터 정지된 코루틴을 계속 실행한다

 

또한 suspendCoroutine 안에서 잠깐 정지(sleep)한 뒤 재개되는 다른 쓰레드를 호출할 수도 있다.

 

import kotlin.concurrent.thread
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

suspend fun main() {
    println("before")

    suspendCoroutine<Unit> { continuation ->
        thread { 
            println("suspended")
            Thread.sleep(1000)
            continuation.resume(Unit)
            println("resumed")
        }
    }

    println("after")
}

// before
// suspended
// (1초 후)
// after
// resumed

 

그러나 이 글을 찾아본 사람이라면 알다시피 쓰레드는 생성 비용이 높은 요소다. 코루틴을 사용하는 이유 중 하나가 고비용인 쓰레드를 사용하지 않기 위함도 있는데, 굳이 써야 할까?

쓰레드보다 더 좋은 방법은 ScheduleExecutorService를 사용해서, 일정 시간이 지나면 continuation.resume()을 호출하도록 알람처럼 설정하는 것이다.

 

import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

private val executor = Executors.newSingleThreadScheduledExecutor {
    Thread(it, "scheduler").apply {
        isDaemon = true
    }
}

suspend fun main() {
    println("before")

    suspendCoroutine<Unit> { continuation ->
        executor.schedule({
            continuation.resume(Unit)
        }, 1000, TimeUnit.MILLISECONDS)
    }

    println("after")
}

// before
// (1초 후)
// after

 

이것 또한 쓰레드를 사용하지만 코루틴의 전용 쓰레드기 때문에, 대기할 때마다 하나의 쓰레드를 블로킹하는 것보다는 나은 방법이다.

그리고 위의 코드는 코루틴 라이브러리가 제공하는 delay()의 핵심 코드와 일치한다.

 

값으로 재개하기

 

코드를 잘 보면 suspendCoroutine의 제네릭 타입과 resume()의 매개변수로 Unit을 넘긴다.

왜냐면 suspendCoroutine 안에서 resume()으로 리턴되는 값은 반드시 suspendCoroutine의 제네릭 타입과 일치하는 타입이어야 하기 때문이다.

 

import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

private val executor = Executors.newSingleThreadScheduledExecutor {
    Thread(it, "scheduler").apply {
        isDaemon = true
    }
}

suspend fun main() {
    val i: Int = suspendCoroutine<Int> { cont ->
        cont.resume(42)
    }
    println(i)

    val str: String = suspendCoroutine<String> { cont ->
        cont.resume("어떤 문자열")
    }
    println(str)

    val b: Boolean = suspendCoroutine<Boolean> { cont ->
        cont.resume(true)
    }
    println(b)
}

// 42
// 어떤 문자열
// true

 

중단 함수는 레트로핏, Room DB 등 여러 라이브러리가 이미 지원하고 있다. 그래서 중단 함수 안에서 콜백 형태의 코드를 쓸 일은 별로 없다.

만약 콜백이 필요하다면 suspendCancellableCoroutine을 쓰는 게 좋다.

 

suspend fun requestUser(): User = suspendCancellableCoroutine<User> { cont ->
    requestUser { user ->
        cont.resume(user)
    }
}

 

그러나 API를 쓰다 보면 데이터 대신 예외를 받을 수도 있다. 그럼 필연적으로 앱이 죽을 수도 있는데 이것은 사용자 경험 측면에서 아주 좋지 않은 현상이다.

이 때 데이터가 없기 때문에 리턴할 수 없으므로 코루틴이 중단된 곳에서 예외로 재개하는 방법을 고려할 수 있다.

 

예외로 재개하기

 

resumeWithException을 쓰면 중단된 지점에서 인자로 넣은 예외를 던진다.

 

import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

suspend fun main() {
    try {
        suspendCoroutine<Unit> { cont ->
            cont.resumeWithException(MyException())
        }
    } catch (e: MyException) {
        println("예외 잡음!")
    }
}

class MyException: Throwable("예외")

// 예외 잡음!

 

이 방법은 네트워크 통신 도중 에러가 발생한 경우 알릴 때 쓸 수 있다.

 

suspend fun requestUser(): User = suspendCancellableCoroutine<User> { cont ->
    requestUser { resp ->
        if (resp.isSuccessful) {
            cont.resume(resp.data)
        } else {
            val e = Exception(/* 커스텀 예외를 쓴다고 가정 */)
            cont.resumeWithException(e)
        }
    }
}

suspend fun requestNews(): News = suspendCancellableCoroutine<News> { cont ->
    requestNews(
        onSuccess = { news ->
            cont.resume(news)
        },
        onError = { e ->
            cont.resumeWithException(e)
        }
    )
}

fun requestNews(onSuccess: (News) -> Unit, onError: (Exception) -> Unit) {
    // ...
}

data class News(val news: String)

 

반응형
Comments