일시 중단 함수 조합하기
일시 중단 함수 조합하기 (Composing suspending functions)
이 섹션에서는 일시 중단 함수(suspending function)를 조합하는 다양한 방법을 다룹니다.
본문
기본적으로 순차 실행 (Sequential by default)
다른 곳에서 정의된, 원격 서비스 호출이나 계산 같은 유용한 일을 하는 두 개의 일시 중단 함수가 있다고 가정해 볼게요. 우리는 그저 유용하다고 치자고요. 사실 이 예시를 위해 각 함수는 1초 동안 지연만 합니다.
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
이 두 함수를 순차적으로 호출해야 한다면 어떻게 해야 할까요? 먼저 doSomethingUsefulOne을 호출하고, 그 다음 doSomethingUsefulTwo를 호출해서 두 결과의 합을 계산하는 상황이요. 실무에서는 첫 번째 함수의 결과를 사용해 두 번째 함수를 호출할지, 어떻게 호출할지 결정해야 할 때 이렇게 합니다.
일반적인 순차 호출을 사용하면 돼요. 코루틴 안의 코드는 일반 코드와 마찬가지로 기본적으로 순차적이기 때문입니다. 다음 예시는 두 일시 중단 함수를 실행하는 총 시간을 측정해서 이 사실을 보여줍니다.
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
//sampleStart
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
//sampleEnd
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
전체 코드는 여기에서 볼 수 있습니다.
이 코드는 대략 이런 출력을 만들어냅니다.
The answer is 42
Completed in 2017 ms
async로 동시 실행 (Concurrent using async)
doSomethingUsefulOne과 doSomethingUsefulTwo의 호출 사이에 의존성이 없고, 둘을 동시에 실행해서 더 빨리 답을 얻고 싶다면 어떻게 할까요? 이때 async가 도움이 됩니다.
개념적으로 async는 launch와 똑같아요. 다른 모든 코루틴과 동시에 작동하는 가벼운 스레드인 별도의 코루틴을 시작하죠. 차이점은 launch는 Job을 반환하고 결과 값을 담지 않는 반면, async는 Deferred를 반환한다는 거예요. Deferred는 나중에 결과를 제공하겠다는 약속을 나타내는 가벼운 non-blocking future입니다. deferred 값에 .await()를 사용해서 최종 결과를 얻을 수 있고, Deferred는 Job이기도 하므로 필요하면 취소할 수도 있습니다.
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
//sampleStart
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
//sampleEnd
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
전체 코드는 여기에서 볼 수 있습니다.
이 코드는 대략 이런 출력을 만들어냅니다.
The answer is 42
Completed in 1017 ms
두 코루틴이 동시에 실행되므로 두 배 더 빠릅니다. 코루틴에서의 동시성은 항상 명시적이라는 점을 기억하세요.
지연 시작 async (Lazily started async)
선택적으로 async는 start 매개변수를 CoroutineStart.LAZY로 설정해 지연(lazy) 실행되게 할 수 있어요. 이 모드에서는 await가 결과를 요구할 때, 또는 그 Job의 start 함수가 호출될 때만 코루틴을 시작합니다. 다음 예시를 실행해 보세요.
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
//sampleStart
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
// some computation
one.start() // start the first one
two.start() // start the second one
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
//sampleEnd
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
전체 코드는 여기에서 볼 수 있습니다.
이 코드는 대략 이런 출력을 만들어냅니다.
The answer is 42
Completed in 1017 ms
여기서 두 코루틴은 이전 예시처럼 정의되지만 실행되지는 않고, start를 호출해서 언제 정확히 실행을 시작할지 프로그래머에게 제어권이 주어집니다. 먼저 one을 시작하고, 그다음 two를 시작한 뒤, 각 코루틴이 끝나기를 await 합니다.
만약 각 코루틴에 start를 먼저 호출하지 않고 println 안에서 await만 호출하면 순차 동작으로 이어진다는 점을 유의하세요. await가 코루틴 실행을 시작하고 끝나기를 기다리기 때문인데, 이는 지연 실행의 의도된 사용 사례가 아닙니다. async(start = CoroutineStart.LAZY)의 사용 사례는 값 계산에 일시 중단 함수가 관여할 때 표준 lazy 함수를 대체하는 것입니다.
Async 스타일 함수 (Async-style functions)
async 함수를 쓰는 이 프로그래밍 스타일은 다른 프로그래밍 언어에서 인기 있는 스타일이기 때문에 설명을 위해 여기서만 제공합니다. 코틀린 코루틴에서 이 스타일을 쓰는 것은 아래에서 설명할 이유로 강력히 권장되지 않습니다.
doSomethingUsefulOne과 doSomethingUsefulTwo를 비동기로 호출하는 async 스타일 함수를, 구조적 동시성(structured concurrency)을 벗어나기 위해 GlobalScope 참조를 사용하는 async 코루틴 빌더로 정의할 수 있어요. 이 함수들은 비동기 계산만 시작하고 결과를 얻으려면 결과 deferred 값을 사용해야 한다는 사실을 강조하기 위해 "...Async" 접미사로 이름을 붙입니다.
GlobalScope는 사소하지 않은 방식으로 역효과를 낼 수 있는 민감한 API이며, 그중 하나는 아래에서 설명합니다. 그래서 GlobalScope를 사용하려면 @OptIn(DelicateCoroutinesApi::class)로 명시적으로 옵트인해야 합니다.
// The result type of somethingUsefulOneAsync is Deferred<Int>
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()
}
// The result type of somethingUsefulTwoAsync is Deferred<Int>
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()
}
이 xxxAsync 함수들은 일시 중단 함수가 아니라는 점을 유의하세요. 어디에서나 사용할 수 있습니다. 하지만 이들을 사용한다는 것은 항상 호출 코드와 함께 자신의 동작이 비동기(여기서는 동시) 실행된다는 뜻이에요.
다음 예시는 코루틴 바깥에서 이들의 사용을 보여줍니다.
import kotlinx.coroutines.*
import kotlin.system.*
//sampleStart
// note that we don't have `runBlocking` to the right of `main` in this example
fun main() {
val time = measureTimeMillis {
// we can initiate async actions outside of a coroutine
val one = somethingUsefulOneAsync()
val two = somethingUsefulTwoAsync()
// but waiting for a result must involve either suspending or blocking.
// here we use `runBlocking { ... }` to block the main thread while waiting for the result
runBlocking {
println("The answer is ${one.await() + two.await()}")
}
}
println("Completed in $time ms")
}
//sampleEnd
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()
}
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
전체 코드는 여기에서 볼 수 있습니다.
만약 val one = somethingUsefulOneAsync() 줄과 one.await() 표현식 사이에 코드에 어떤 논리 오류가 생겨 프로그램이 예외를 던지고, 프로그램이 수행하던 작업이 중단되는 상황을 생각해 보세요. 보통 전역 오류 처리기가 이 예외를 잡아 개발자에게 로그를 남기고 보고할 수 있지만, 프로그램은 그 외의 다른 작업을 계속할 수 있어요. 하지만 여기서는 그것을 시작한 작업이 중단됐음에도 somethingUsefulOneAsync가 여전히 백그라운드에서 실행 중입니다. 이 문제는 아래 섹션에서 보여주듯 구조적 동시성에서는 발생하지 않습니다.
async와 함께하는 구조적 동시성 (Structured concurrency with async)
동시에 async 사용 예시를, doSomethingUsefulOne과 doSomethingUsefulTwo를 동시에 실행하고 결합된 결과를 반환하는 함수로 리팩터링해 볼게요. async는 CoroutineScope 확장이므로, 필요한 스코프를 제공하기 위해 coroutineScope 함수를 사용할 겁니다.
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
이렇게 하면 concurrentSum 함수의 코드 안에서 무언가 잘못되어 예외가 던져지면, 그 스코프에서 시작된 모든 코루틴이 취소됩니다.
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
//sampleStart
val time = measureTimeMillis {
println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")
//sampleEnd
}
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
전체 코드는 여기에서 볼 수 있습니다.
위 main 함수의 출력에서 알 수 있듯, 우리는 여전히 두 연산을 동시에 실행합니다.
The answer is 42
Completed in 1017 ms
취소는 항상 코루틴 계층을 통해 전파됩니다.
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
try {
failedConcurrentSum()
} catch(e: ArithmeticException) {
println("Computation failed with ArithmeticException")
}
}
suspend fun failedConcurrentSum(): Int = coroutineScope {
val one = async<Int> {
try {
delay(Long.MAX_VALUE) // Emulates very long computation
42
} finally {
println("First child was cancelled")
}
}
val two = async<Int> {
println("Second child throws an exception")
throw ArithmeticException()
}
one.await() + two.await()
}
전체 코드는 여기에서 볼 수 있습니다.
자식 중 하나(즉, two)가 실패하면 첫 번째 async와 await하고 있던 부모가 모두 취소되는 방식을 주목하세요.
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException