코루틴은 가장 먼저 완료되는 코루틴의 결과를 기다리는 select 함수를 제공한다. 또한 여러 개의 채널 중 버퍼에 남은 공간이 있는 채널을 먼저 확인하여 데이터를 보내거나, 이용 가능한 원소가 있는 채널로부터 데이터를 받을 수 있는지 여부도 확인할 수 있다. 코루틴 사이에 경합을 일으키거나, 여러 개의 데이터 소스로부터 나오는 결괏값을 합칠 수도 있다.

Select 함수는 실험용

select를 필요로 하는 경우가 적기 때문에 안정화되는 건 어려워 보인다. 실제로 사용되는 경우는 드물기 때문에 간단하게 설명한다.

지연되는 값 선택하기

여러 개의 소스에 데이터를 요청한 뒤, 가장 빠른 응답만 얻는 경우를 생각해 보자. 가장 쉬운 방법은 요청을 여러 개의 비동기 프로세스로 시작한 뒤, select 함수를 표현식으로 사용하고 표현식 내부에서 값을 기다리는 것이다. select 내부에서는 셀렉트 표현식에서 나올 수 있는 결괏값을 명시하는 Deferred 값의 onAwait 함수를 호출한다. 람다식 내부에서 값을 변환할 수도 있다.

다음 예제에서 비동기 결괏값 하나만 반환하는 걸 볼 수 있는데, select 표현식이 하나의 비동기 작업이 완료됨과 동시에 끝나게 되어 결괏값을 반환한다는 것을 알 수 있다.

suspend fun requestData1(): String {
    delay(100_000)
    return "Data1"
}

suspend fun requestData2(): String {
    delay(1000)
    return "Data2"
}

val scope = CoroutineScope(SupervisorJob())

suspend fun askMultipleForData(): String {
    val defData1 = scope.async { requestData1() }
    val defData2 = scope.async { requestData2() }
    return select {
        defData1.onAwait { it }
        defData2.onAwait { it }
    }
}

suspend fun main(): Unit = coroutineScope {
    println(askMultipleForData())
}

(1초 후)
Data2

위 예제를 보면 외부의 스코프로부터 async가 시작된다. 따라서 askMultipleForData를 시작하는 코루틴을 취소하면, 외부의 스코프인 비동기 태스크는 취소가 되지 않는다. 구현 방식에 문제가 있지만, 이것보다 나은 방식을 찾지는 못했다.

coroutineScope를 사용하면 자식 코루틴도 기다리게 되며, 다음 예제에서 확인할 수 있듯이 1초가 아닌 100초 후에서야 Data2를 결과로 받는다.

suspend fun requestData1(): String {
    delay(100_000)
    return "Data1"
}

suspend fun requestData2(): String {
    delay(1000)
    return "Data2"
}

suspend fun askMultipleForData(): String = coroutineScope {
    select<String> {
        async { requestData1() }.onAwait { it }
        async { requestData2() }.onAwait { it }
    }
}

suspend fun main(): Unit = coroutineScope {
    println(askMultipleForData())
}

(100초 후)
Data2

async와 select를 사용하면 코루틴끼리 경합하는 상황을 쉽게 구현할 수 있지만, 스코프를 명시적으로 취소해야 한다. select가 값을 생성하고 나서 also를 호출한 뒤 다른 코루틴을 취소할 수 있다.

suspend fun askMultipleForData(): String = coroutineScope {
    select<String> {
        async { requestData1() }.onAwait { it }
        async { requestData2() }.onAwait { it }
    }.also { coroutineContext.cancelChildren() }
}

suspend fun main(): Unit = coroutineScope {
    println(askMultipleForData())
}

(1초 후)
Data2

위 해결책은 약간 복잡하기 때문에, 많은 개발자는 헬퍼 함수를 정의하거나 raceOf 함수를 지원하는 (루이 캐드가 개발한 Splitties 같은) 외부 라이브러리를 사용한다. 27장 ‘코루틴 활용 비법’에서 raceOf 함수를 코드 몇 줄로 구현해 본다.

https://github.com/LouisCAD/Splitties

suspend fun askMultipleForData(): String = raceOf({
    requestData1()
}, {
    requestData2()
})

suspend fun main(): Unit = coroutineScope {
    println(askMultipleForData())
}

채널에서 값 선택하기

select 함수는 채널에서도 사용할 수 있다. 셀렉트 표현식에서 사용하는 주요 함수는 다음과 같다.