You can make anything
by writing

C.S.Lewis

by 서준수 Mar 04. 2025

카지노 게임 플로우 (Flow)

코틀린 카지노 게임(16)

카지노 게임 플로우 (Flow)


코루틴에서 비동기적으로 처리되는 다수의 값을 다루기 위해 사용되는 대표적인 방법으로 플로우(Flow)가 있다. 카지노 게임는 코루틴 자체만큼 다양한 내용을 포함하고 있기 때문에 이번에는 간단하게 기본 개념을 알아본다.


연속적인 값 다루기

카지노 게임 채널에서 Deferred_ex01.kt예제를 보면 5개의 값을 다루고 있다. 이때 List에 모든 값을 담은 후에 다음 작업을 처리하고 있다. 즉 List는 5개의 값을 실시간으로 다루는 것이 아니라 처리된 값을 일시적으로 저장하는 역할을 한다. 저장된 값을 필요할 때 꺼내 사용하는 것이다.


이러한 한계를 극복할 수 있는 것이 채널이다. 채널은 소비(구독) 여부와 무관하게 지속적으로 값을 생성한다. 값을 버퍼에 저장하여 소비자가 생겼을 때 전달할 수 있다. 따라서 실시간 연속 값 처리에 적합하다.

카지노 게임

채널이 소비자의 값 소비 여부와 관계없이 값을 생성해 내는 것은 아래 예제를 통해서 살펴볼 수 있다.

실행 결과는 다음과 같다.

Channel Started
Calling Channel
1

receive()가 되기 전에 이미 produce를 통해 값이 생성되었다. 이러한 방식으로 연속적인 값을 다루는 방식을 핫 스트림(Hot Stream)이라고 한다. 핫 스트림이 있으니 콜드 스트림(Cold Stream)도 있지 않을까? 있다.


콜드 스트림은 소비가 필요할 때마다 값을 생성하고 처리한다. 소비자의 요청이 있을 때만 동작하기 때문에 리소스 관점에서 효율적이다. 따라서 실시간 값 처리보다는 비동기 이벤트 처리에 더 적합하다. 코틀린에서 기본적으로 제공되는 콜드 스트림으로 시퀀스(Sequence)가 있다.

카지노 게임

시퀀스는 소비자가 값을 소비하고자 할 때 sequence() 블록이 실행된다. 위 그림을 참고하여 흐름을 파악하고 다음 코드를 살펴보자.

실행 결과는 다음과 같다.

Calling Sequence
Sequence Started
Consumed: 1

소비자가 소비를 하는 시점은 iterator.next()가 호출되는 시점이다. 이때 sequence 블록이 실행되고 500ms 후 yield()에 의해 값이 생성된다. 생성된 값은 interator.next()의 반환값이 된다. 생산자인 시퀀스가 주체적으로 값을 생산하는 방식이 아니다. 소비자가 값을 요청하면 그때 값을 생산한다. 이것이 채널과 같은 핫 스트림과 가장 큰 차이이다.


동의되지 않는 권위

공식 문서에서는 시퀀스 예제에서 CPU 리소스를 사용하는 것을 나타내기 위한 용도로 선언된 Thread.sleep()을 두고 메인 스레드를 블로킹하는 것이라고 말한다. 그리고 비동기로 처리하기 위해서 simple()을 suspend 함수로 바꾸고 List를 반환하는 형태의 예시를 보여준다. 이렇게 되면 결국 (도입부에서 언급한 Deferred 예제처럼) 한 번에 모든 값을 반환해야 하는 문제가 있다고 지적한다. 아무래도 공신력 있는 공식 문서의 탓인지, 다른 사람들은 이상하게 느껴지는 부분이 없는 것인지 모르겠으나 이런 내용을 번역 혹은 요약해서 쓴 글이 많이 보인다. 개인적으로 이러한 흐름에 의문이 생긴다. 이것이 적절한 예시인가?

물론 나의 지식이 짧아 문서 작성자의 의도를 파악하지 못했을 수도 있다. 하지만 공식 문서는 초보자도 많이 본다. 시퀀스 자체가 동기적임을 말하려는 것 같은데 조금 더 매끄러운 예시라면 어떨까 하는 아쉬움이 있다. 어떤 면에서는 차라리 시퀀스 관련 부분을 모두 삭제하는 편이 나을지도 모르겠다.

메인 스레드를 블로킹하지 않으려면 그냥 다른 스레드를 쓰면 된다.

실행 결과는 다음과 같다.

Main thread is not blocked - main
Consumed: 1 - Thread-0
Consumed: 2 - Thread-0
Consumed: 3 - Thread-0

이렇게 하면 메인 스레드를 블로킹하지 않기 때문에 simple()을 suspend 함수로 변경할 필요가 없다. 그 결과 값을 List로 반환하는 케이스도 자연스럽게 사라진다. 값이 비동기적으로 계산되며 스트림의 형태를 띠고 있다.따라서 비동기적으로 계산되는 값의 스트림을 나타내기 위해서 카지노 게임(Flow)를사용할 수 있다는 공식 문서의 흐름은 다시 생각해 볼 문제다. 오히려 코루틴 기반으로 동작하는 것에서 오는 장점, 백프레셔(Back Pressure) 처리, 다양한 연산자 지원 등과 같은 장점을 언급하며 시작하는 편이 나을 것 같다.


그래서 카지노 게임가 무엇인가?

카지노 게임는 카지노 게임을 활용하여 비동기적으로 생성되는 값의 스트림을 나타내는 동시에, 이러한 데이터 스트림을 처리하기 위한 강력한 API이다. 플로우를 시퀀스와 비교할 때 흔히 비동기라는 점을 강조한다. 하지만 앞서 봤듯이 비동기 자체는 시퀀스도 스레드를 통해서 구현할 수 있다. 차이점이라면 카지노 게임는 코루틴을 통한 비동기이다. 시퀀스와 마찬가지로 콜드 스트림이기 때문에 소비자의 요청이 있을 때 값이 생성된다. 해당 과정은 아래 코드를 통해서 확인할 수 있다.

실행 결과는 다음과 같다.

Calling Flow
Flow Started
1
2
3

플로우가 생성되고 500ms 동안 flow 내부는 실행되지 않는다. 즉 값이 생성되지 않는다. 하지만 collect를 통해서 값을 소비할 때 비로소 flow 내부가 실행된다. emit()은 플로우 내부에서 값을 생성하고 소비자에게 전달하는 역할을 한다.


시퀀스의 비동기에 대한 오해

플로우가 카지노 게임 기반의 비동기를 지원한다는 것을 강조하기 위해 시퀀스와 비교된다. 그래서시퀀스는 비동기가 불가능하다는 오해가 있을 것 같다. 비동기가 가능하다는 것은 앞서 스레드에 의한 시퀀스 비동기 예제를 통해 확인했다. 해당 예제는 메인 스레드를 블로킹하지 않는다는 것을 보여주기 위한 예시다. 그러면 다음과 같은 예제는 어떤가?

실행 결과는 다음과 같다. (다양한 케이스 중 하나일 뿐이다.)

main: coroutine start
coroutine 1 start - thread: DefaultDispatcher-worker-2 @coroutine#1
coroutine 2 start - thread: DefaultDispatcher-worker-1 @coroutine#2
coroutine 2: running
coroutine 1: 1
coroutine 2: end
coroutine 1: 2
coroutine 1: 3

출력되는 결과를 보면 코루틴 간 작업이 비동기적으로 잘 수행하고 있다. 스레드 상태를 확인해 보면 코루틴이 서로 다른 스레드에서 동작하면서 이뤄낸 결과인 것을 알 수 있다.


그렇다면 문제가 없는 것일까?만약 같은 스레드에서 동작하는 코루틴이었다면 어떨까? 시퀀스를 사용하는 코루틴의 스레드 블로킹으로 인해 다른 코루틴까지 블로킹된다. 간단하게 다음과 같이 runBlocking을 사용하여 확인할 수 있다.

실행 결과는 다음과 같다.

main: coroutine start
coroutine 1 start - thread: main @coroutine#2
coroutine 1: 1
coroutine 1: 2
coroutine 1: 3
coroutine 2 start - thread: main @coroutine#3
coroutine 2: running
coroutine 2: end

두 코루틴이 같은 스레드(=메인 스레드)에서 동작한다. 따라서 코루틴 1에서 스레드 블로킹이 발행하면 해당 작업이 모두 완료된 후에 코루틴 2가 실행된다. 이때 플로우를 사용하면 이러한 블로킹을 피할 수 있다. 카지노 게임는 코루틴 기반이기 때문에Thread.sleep() 대신에 suspend 함수인 delay()를 사용할 수 있다. 따라서 스레드를 블로킹하지 않는다.

실행 결과는 다음과 같다.

main: coroutine start
coroutine 1 start - thread: DefaultDispatcher-worker-1 @coroutine#1
coroutine 2 start - thread: DefaultDispatcher-worker-2 @coroutine#2
coroutine 2: running
coroutine 1: 1
coroutine 2: end
coroutine 1: 2
coroutine 1: 3

서로 다른 스레드에서 동작한 코루틴,시퀀스 조합(Sequence_ex03.kt)과 큰 차이가 없다.하지만 이것이 시퀀스가 비동기에 적합하다는 의미는 아니다. 시퀀스 자체는 동기적으로 동작한다.다른 일반적인 동기적 코드를 스레드나 코루틴으로 비동기적으로 실행시키는 것과 마찬가지로 구현했을 뿐이다. 따라서 비동기 작업에는 비동기적으로 작동하는 플로우를 사용하는 것이 더욱 적합하다.


요약하자.

- 핫 스트림은 값의 소비 여부와 무관하게 지속적으로 값을 생성한다.
- 콜드 스트림은 소비가 필요할 때마다 값을 생성하고 처리한다.
- 채널은 핫 스트림의 특성을 가진다.
- 시퀀스와 카지노 게임는 콜드 스트림이다.
- 시퀀스로 비동기를 구현할 수는 있으나 시퀀스 자체는 동기적으로 동작한다.
- 카지노 게임는 비동기적으로 동작하기 때문에 비동기 구현에 적합하다.
브런치는 최신 브라우저에 최적화 되어있습니다.