코루틴 공식 가이드 자세히 읽기 — Part 7

Myungpyo Shim
6 min readJan 24, 2019

--

공식 가이드 읽기 (7 / 9)

Select 표현식

Korean [English]

(목차로 돌아가기)

Select 표현식은 다수의 중단 함수들을 호출 후 그 결과를 동시에 대기 할 수 있도록 해주며, 그 중 가장 먼저 완료된 결과를 바로 이용 할 수 있도록 해줍니다.

Select 표현식은 kotlinx.coroutines 의 실험적인 기능으로 kotlinx.coroutines 라이브러리 업데이트에 따라서 동작이 변경되거나 하위 호환을 제공하지 못할 수 있습니다.

채널로부터 선택 (Selecting from channels)

우리에게 문자열 “fizz” 와 “buzz” 를 생성하는 두개의 채널이 있다고 가정 해 봅시다. fizz 채널은 “fizz” 문자열을 300ms 마다 생성하고, buzz 채널은 “Buzz!” 문자열을 500ms 마다 생성합니다.

우리는 receive() 중단 함수를 호출하여 각 채널의 데이터를 수신 받을 수 있습니다. 하지만 다음과 같이 select { } 표현식과 onReceive() 문을 이용하면 두 개의 채널에서 동시에 수신 받도록 구현할 수 있습니다.

위 예제는 두개의 채널에서 수신받는 selectFizzBuss() 중단 함수를 정의하고 메인에서 7번 호출 하여 아래와 같은 결과를 얻습니다.

<출력 결과>

fizz -> ‘Fizz’
buzz -> ‘Buzz!’
fizz -> ‘Fizz’
fizz -> ‘Fizz’
buzz -> ‘Buzz!’
fizz -> ‘Fizz’
buzz -> ‘Buzz!’

닫힌 채널에서의 선택 (Selecting on close)

select { } 블록 안의 onReceive 구문은 채널이 닫히게 되면 연관 된 select { }블록으로 예외를 전달하고 실패하게 됩니다. 우리는 채널이 닫힐 때 수행할 액션을 정의하기 위해서 onReceiveOrNull() 을 이용할 수 있습니다. 다음 예제는 select { } 구문도 역시 최종적으로 선택된 블록의 결과를 반환하는 하나의 표현식임을 나타내고 있습니다.

위 예제에서 채널 a 는 “Hello” 문자열을 4번 전달하고, b 채널은 “World” 문자열을 4번 전달 합니다.

onReceiveOrNull 은 각 요소들이 non-nullable 인 채널에 대해서만 정의 된 확장함수 라는 점에 주의를 기울입니다. 이것은 채널이 닫힌 것과 채널의 값이 null 인 경우에 대한 혼동을 방지하기 위함입니다.

<출력 결과>

a -> ‘Hello 0’
a -> ‘Hello 1’
b -> ‘World 0’
a -> ‘Hello 2’
a -> ‘Hello 3’
b -> ‘World 1’
Channel ‘a’ is closed
Channel ‘a’ is closed

위 출력 결과에는 몇가지 흥미로운 점들이 있기 때문에 좀 더 살펴봅시다.

첫째로 선택 문이 첫번째 구문에 편향(biased)되어 있다는 것입니다. 여러 개의 구문들이 동시에 선택가능한 경우 그 구문들 중 첫번째 것이 선택됩니다. 이 예제에서 두 개의 채널은 계속해서 문자열을 생성해 냅니다. 그러므로 select { } 절의 첫번째 구문인 a 채널이 우선순위를 갖게 됩니다. 하지만 우리는 버퍼가 없는 채널을 사용하고 있기 때문에 a 는 때때로 send() 호출이 중단 되고 b 에게 send() 할 기회를 주게됩니다.

두번째는 이미 닫힌 채널에 대해서는 onReceiveOrNull() 은 대기 없이 곧장 선택된다는 점 입니다.

전송을 위한 선택 (Selecting to send)

선택문은 그 자체적인 편향 특성을 장점으로 이용할 수 있는 onSend() 문을 가지고 있습니다.

정수를 생성한 뒤 주 소비자(main channel)가 소비하지 못하는 상황에는 부 소비자(side channel)로 값을 전달하는 특성이 있는 프로듀서를 생각해 봅시다. 이러한 상황은 다음과 같은 예제로 재현해 볼 수 있습니다. 다음 예제는 주 소비자가 꽤 느려서 각 정수를 처리하는데 250ms 가 걸리는 상황을 가정하고 작성되어 있습니다.

<출력 결과>

Consuming 1
Side channel has 2
Side channel has 3
Consuming 4
Side channel has 5
Side channel has 6
Consuming 7
Side channel has 8
Side channel has 9
Consuming 10
Done consuming

지연 값 선택 (Selecting deferred values)

지연 값들(Deferred values)은 onAwait() 문을 이용해 선택될 수 있습니다. 이를 확인해보기 위해서 파라미터로 전달 받은 시간동안 대기 후 특정 문자열을 반환하는 비동기 함수 asyncString() 과 이 함수에 임의의 지연시간을 전달하며 12번 호출하는 asyncStringList() 함수를 다음 예제와 같이 작성 해 봅시다.

이제 메인 함수는 asyncStringList() 를 호출하여 지연값들 중 하나가 끝날때까지 기다린 후 여전히 진행 중인 지연 값 들의 개수를 셉니다. 코드를 보면 선택문도 Kotlin DSL 이기 때문에 임의의 코드를 사용하여 선택문에 구문들을 제공하였습니다. 이번 경우에 우리는 지연 값 리스트의 각 지연 값에 onAwait() 구문을 제공하기 위해서 지연 값 리스트를 순회하였습니다.

<실행 결과>

Deferred 6 produced answer ‘Waited for 43 ms’
11 coroutines are still active

지연 값 채널 간 전환 (Switch over a channel of deferred values)

문자열 타입 지연값(Deferred<String>)을 수신한 후 각 지연되는 실제 값을 기다리는 채널 프로듀서 함수를 작성해 봅시다. 이 함수는 다음 지연 값이 도착하거나 채널이 닫히기 전까지만 현재 지연 값의 실제 값을 수신하기 위해 대기합니다. 이 예제는 동일한 select 문에서 onReceiveOrNull() 과 onAwait() 문을 함께 사용합니다.

테스트를 위해서 우리는 지정된 시간뒤에 전달 된 문자열을 반환하는 다음과 같은 async 함수를 사용할 것입니다.

메인 함수는 switchMapDeferreds() 의 결과를 출력할 코루틴을 실행하고 몇몇 테스트 데이터를 전송할 뿐입니다.

<실행 결과>

BEGIN
Replace
END
Channel was closed

위 구현은 ReactiveStream 에서 볼 수 있는 switchMap 과 비슷합니다. switchMap 을 사용할 만한 예제로 자주 등장하는 것은 퀄리티가 다른 이미지 프로세싱 파이프라인을 들 수 있습니다.

저화질 프로세서 P1은 화질이 좋지 않지만 빠르고, 중간 화질 프로세서 P2는 화질도 중간이고 속도도 중간이며, 고화질 프로세서 P3 은 속도는 느리지만 고화질 이미지를 출력한다고 가정해 봅시다.

우리는 사용자에게 최대한 빠르게 이미지를 보여주기 위해서 P1 파이프라인으로부터 이미지들을 수신받을 것입니다. 그러다가 P2 파이프라인이 준비가 되면 P1 의 수신은 중지하고 P2 로 전환하여 수신을 하다가 P3 파이프라인이 준비가 되면 P2 역시 중지하고 P3 파이프라인 수신을 시작합니다.

이러한 방식으로 스트림의 전환을 구현할 때 switchMap 을 사용하여 우리가 이번에 구현한 switchMapDeferreds 함수를 사용하면 동일한 효과를 낼 수 있습니다.

--

--