시배's Android

Kotlin | The Big Difference Between Flows and Channels in Kotlin 본문

Kotlin/Kotlin

Kotlin | The Big Difference Between Flows and Channels in Kotlin

si8ae 2023. 9. 20. 14:16
 

The Big Difference Between Flows and Channels in Kotlin

Stop worrying if flows are hot or cold, and focus on good old-fashioned encapsulation instead

betterprogramming.pub

"Channel은 Hot이고 Flow은 Cold"는 Kotlin 프로그래머의 말을 들어보셨을 것입니다.

 

이는 비동기 데이터 스트림으로 작업하는 두 가지 방법을 유용하게 구분하는 말입니다. Flow와 Channel은 함수와 개체만큼이나 서로 다릅니다. 하지만 이것이 전부는 아닙니다. Flow 자체는 적어도 두 가지 매우 다른 형태로 제공되기 때문입니다. 바로 이 지점에서 제한적인 Hot, Cold 비유가 무너지기 시작합니다.

 

이 모호한 비유를 어떻게 하면 좀 더 구체적이고 실행 가능한 것으로 개선할 수 있을까요?

 

Channel을 "Hot"이라고 부르는 이유는 Channel이 상태 저장 객체이기 때문입니다. Channel은 다른 계산에서 값을 수신할 수 있는 통신 메커니즘입니다. 소비자는 Channel과의 상호 작용을 통해 해당 계산이 언제 시작되고 중지되는지 제어할 수 없습니다.

 

지하철의 움직이는 에스컬레이터라고 생각하면 됩니다. 에스컬레이터는 사용자가 사용하기 전부터 작동하고 있으며, 사용자가 내린 후에도 계속 작동할 가능성이 높습니다.

 

Flow는 상태를 유지하지 않기 때문에 "Cold"라고 합니다. Kotlin 코드에서 Flow를 전달할 때 Flow는 데이터를 보유하거나 생성하지 않습니다. 이는 Flow 개체가 데이터 스트림의 활성 인스턴스가 아니기 때문입니다. 대신 수집을 호출할 때마다 해당 함수 호출 내부에만 존재하는 Flow 계산의 새 임시 인스턴스가 생성됩니다.

 

Channel이 지하철 역에서 움직이는 에스컬레이터와 같다면, Flow는 엘리베이터와 비슷합니다. 사용자가 상호작용을 시작할 때만 작동을 시작하고 사용자가 나가자마자 다시 멈춥니다.

 

가장 기본적인 Channel과 Flow를 다룰 때는 이러한 구분이 괜찮습니다. 하지만 Flow를 만드는 방법은 여러 가지가 있으며, Flow를 사용해 본 경험이 많으시다면 저의 지나치게 단순화된 설명에 구멍이 있다는 것을 이미 알고 계실 것입니다.

"Hot Flow는 어떻게 하나요?"라고 질문하실 수도 있습니다.

Flows : Hot or Not?

Flow은 Cold라는 간단한 설명으로 시작했습니다. "Hot Flow"는 개별 소비자보다 오래 지속되는 일부 활성 계산에 의해 뒷받침되는 Flow를 설명하는 데 사용되는 새로운 용어입니다. 예를 들어, Kotlin의 기본 제공 SharedFlow에 대한 문서의 첫 번째 줄은 "Hot Flow"라고 선언합니다. 그리고 계속해서 설명합니다:

 

"SharedFlow은 활성 인스턴스가 수집기의 존재 여부와 독립적으로 존재하기 때문에 Hot Flow라고 합니다."

 

저는 이 용어가 마음에 들지 않습니다. Channel을 Hot, Flow를 Cold라고 부르는 것은 유용하지만, Hot Flow라는 형태로 구분을 추가하는 것은 혼란스럽고 불필요한 것일 수도 있습니다. 그 이유를 설명하겠습니다.

 

Flow를 Hot Flow와 Cold Flow로 구분하는 것을 중단해야 하는 이유는 두 가지가 있습니다. 첫 번째는 그 구분이 정확하지 않다는 것입니다. Flow를 Cold Flow에서 Hot Flow로 올리는 것은 정확히 무엇일까요?

 

SharedFlow 문서에서 Flow가 Hot이라고 설명되는 것을 보았습니다. Flow를 "Hot"이라고 부르는 또 다른 예는 Channel의 consumeAsFlow 함수입니다. 이 함수는 SharedFlow는 아니지만 Channel이 수집 여부에 관계없이 계속 값을 생성하는 활성 상태 저장 데이터 소스이기 때문에 'Hot'이라는 레이블을 붙이는 것이 합당한 것 같습니다.

val flow = channel.consumeAsFlow()

Hot Channel을 Flow로 전환하는 유일한 방법은 consumeAsFlow 함수가 아닙니다. 몇 줄의 직접 작성한 Flow 코드만으로 동일한 값 스트림을 소비할 수 있습니다.

val flow = flow {
    emitAll(channel)
}

이 두 코드는 완전히 상호 교환이 가능합니다. 동일하지는 않더라도 동등한 동작과 기능을 제공합니다. 하지만 flow { ... } 빌더의 문서에는 "Cold Flow를 생성한다"고 매우 명확하게 명시되어 있습니다.

 

이 두 가지 Flow 예제가 동일한 작업을 수행한다면 어떻게 하나는 Cold이고 하나는 Flow일 수 있을까요? 다시 설명하겠지만, 짧은 대답은 Hot Flow와 Cold Flow를 구분하는 것은 그 의미에 대한 명확한 정의가 없기 때문에 그다지 유용하지 않다는 것입니다.

 

제가 Hot Flow와 Cold Flow를 구분하는 것이 마음에 들지 않는 두 번째 이유는 실행 가능하지 않기 때문입니다.

 

누군가 반짝이는 Flow 건네주며 "조심하세요, 뜨거워요!"라고 말한다고 가정해 봅시다.

 

경고에 감사하지만, 잠시 후 그 정보로 무엇을 할 수 있을지 궁금해지기 시작합니다.

Flow를 두 번 이상 수집하지 않아야 할까요? consumeAsFlow에 의해 생성된 'Hot' Flow에는 이러한 제한이 있지만, 유사한 receiveAsFlow를 포함하여 스스로를 Hot이라고 부르는 다른 많은 Flow는 원하는 만큼 수집할 수 있습니다.

리소스 관리는 어떤가요? Hot Flow 수집을 완료한 후 정리해야 하는 기본 Hot Channel이나 기타 데이터 소스가 있나요? 그럴 수도 있지만, 세부 사항을 파악하는 것은 쉽지 않습니다. consumeAsFlow를 사용한 경우, Flow 수집을 중단하자마자 Channel이 닫혔을 것입니다. 하지만 receiveAsFlow를 사용하여 자동 정리를 선택 해제했다면 누가 Channel을 취소했는지는 누구나 추측할 수 있습니다.

심지어 여러 소비자가 동시에 Flow를 수집하고 있을 수도 있습니다. 한 소비자가 Channel을 취소하면 다른 소비자에게 문제가 발생할 수 있지만, 아무도 Channel을 취소하지 않으면 리소스 누수가 발생하여 영원히 열려 있게 되는 캐치-22처럼 보입니다.

Hot Flow와 관련된 리소스를 닫는 방법을 아는 것도 까다롭습니다. Channel은 자체적인 정리 메커니즘을 제공합니다. 취소 메서드를 호출하여 값 수신이 끝났음을 알리고, 생산자가 적절하게 반응하여 어떤 것을 정리해야 하는지 결정할 것이라고 믿으면 됩니다. 그러나 Flow를 사용하면 수집을 시작하고 중지하는 것만이 유일한 상호 작용입니다. Flow에는 아무리 "Hot"하다고 주장하더라도 리소스를 닫거나 종료하는 함수가 없습니다.

그렇다면 Hot Flow가 우리의 삶을 어렵게 만든다는 뜻일까요? Hot Flow에는 이러한 모든 문제를 해결할 수 있는 전용 닫기 또는 취소 메서드가 있어야 하지 않을까요?

이 문제도 다시 다룰 것이지만, 간단히 답하자면 Flow는 소비자가 리소스 관리에 대해 걱정할 필요가 없도록 설계되었습니다. 대신 구조화된 동시성이 개별 Flow 수집기보다 오래 지속되는 리소스를 처리합니다. 올바르게 수행하면 완전히 자동으로 이루어지므로 Flow에 "Hot"이라는 라벨을 붙인다고 해서 사용 방식이 달라지지도 않고, 달라져서도 안 됩니다.

One of These Things Is Not Like the Other

Flow 문서에서 'Hot'과 'Cold'에 대한 언급이 자주 나오는 것을 보면 이 둘을 구분할 필요가 있다는 것을 알 수 있습니다. 문제는 그 차이를 더 정확하고 실행 가능하도록 명확히 할 수 있을까요?

 

우선 Hot Channel과 Cold Flow를 원래의 구분으로 돌아가 보겠습니다. Hot Flow가 실제 존재하는지 여부는 차치하고, Cold Flow와 Hot Flow를 근본적으로 다르게 만드는 것은 무엇일까요?

 

Flow와 Channel은 비동기 스트림이라는 동일한 기본 개념의 두 가지 변형일 뿐이라고 생각하기 쉽습니다. 'Hot'과 'Cold'를 구분하면 Channel은 항상 실행 중이고, Flow는 수집할 때만 실행된다는 점이 주요 차이점인 것처럼 들릴 수 있습니다.

 

Channel과 Flow을 서로 연관된 개념으로 생각하지 말아 달라는 요청을 드리고 싶습니다.

  • Flow는 제어 구조입니다. 일시 중단 함수처럼 실행 코드를 포함합니다. Flow에서 값을 수집할 때 함수를 호출하여 함수의 코드를 실행하는 것처럼 Flow 내부의 코드를 호출하는 것입니다.
  • Channel은 통신 메커니즘입니다. 메시지나 값을 처리하고 한 곳에서 다른 곳으로 전달할 수 있습니다. 여기에는 어떤 코드도 포함되지 않습니다. Channel에서 수신하는 것은 다른 코드가 남긴 메시지를 수집하는 것일 뿐입니다.
Kotlin에서 Flow와 Channel의 차이는 함수와 객체의 근본적인 차이와 같습니다. 객체를 Hot이고 함수를 Cold하다고 설명할 수도 있습니다. 개체는 사용자가 상호 작용하지 않는 동안에도 계속 상태를 유지하는 존재입니다. 반면 함수는 해당 함수가 호출되는 동안에만 상태를 유지합니다. 함수는 호출할 때 인스턴스화되고 호출이 끝나면 다시 사라집니다.
Kotlin에서 Flow와 Channel의 차이는 함수와 객체의 차이만큼이나 기본적입니다.
이것이 Channel은 Hot이고 Flow는 Cold하다고 말할 때 포착하는 기본 개념입니다. 데이터 스트림의 속성이나 수명 주기를 지적하는 것이 아니라 완전히 별개의 두 프로그래밍 개념 간의 본질적인 차이점을 설명하는 것입니다.

Managing Resources

Flow와 Channel은 근본적으로 다르기 때문에 기능과 한계가 상당히 다릅니다. 이 논의와 가장 관련성이 높은 것 중 하나는 스스로 정리하는 방식입니다.

Flow는 함수처럼 작동하기 때문에 각 호출에는 명확하게 정의된 입구와 출구가 있습니다. 이는 구조화된 프로그래밍의 원칙이며, 그 장점 중 하나는 자동 리소스 관리를 위해 Flow 내부에서 시도/종료를 사용할 수 있다는 것입니다. 소비자가 사라지든, 생산자의 값이 부족하든, 전체 Flow에 오류가 발생하든, Flow이 종료될 때마다 Flow는 함수 호출처럼 스택을 풀고 종료 경로에 있는 모든 최종 블록을 호출합니다. 함수 호출을 종료하는 것을 잊어서는 안 되는 것과 마찬가지로 Flow를 종료하는 것을 잊어서는 안 됩니다.

그런데 이를 가능하게 하는 특별한 마법은 없습니다. Flow는 함수 호출이기 때문에 함수 호출처럼 작동합니다. Flow를 사용하는 것은 항상 수집 함수를 한 번 호출하는 방식으로 이루어집니다. 어떤 이유로든 해당 함수가 종료되면 흐름이 종료됩니다.

좋은 리소스 관리는 캡슐화의 한 형태입니다. 함수가 열려 있는 리소스를 노출하는 경우(예: 닫을 수 있는 결과를 반환하는 경우) 호출자는 이를 알고 있어야 함수가 종료된 후 정리할 수 있습니다. 그러나 함수가 시도/마지막으로 자체 리소스를 관리하거나 소비 또는 사용과 같은 관련 메커니즘을 사용하는 경우 호출자는 상자 내부에서 일어나는 일에 대해 아무것도 알 필요가 없습니다.

겉으로 보기에 자동 리소스 정리를 포함하는 함수는 리소스를 전혀 사용하지 않는 함수와 동일하게 보입니다. 프로그래밍 언어는 어떤 이유로든 함수가 종료될 때마다 정리 코드를 실행하도록 보장하므로 프로그래머는 이에 대해 걱정할 필요가 없습니다.

Flow는 본질적으로 이러한 종류의 리소스 관리 및 캡슐화에 탁월합니다. Flow는 닫을 수 있거나 다른 어떤 결과도 반환하지 않습니다. 대신 Flow를 수집할 때 수집 함수에 전달한 코드를 실행하여 값을 제공합니다. Flow 구현은 수집기 코드를 실행하기 전에 시도/최종 블록으로 감싸서 어떤 이유로든 코드가 종료되는 경우 Flow의 리소스가 항상 닫히도록 할 수 있습니다.

flow {
    try {
        emit("Hello, World!")
    } finally {
        println("Goodbye!")
    }
}

수집기 코드는 Flow 호출이 발생할 때 호출됩니다. 이는 일반적인 Closeable.use부터 보다 전문적인 Reader.forEachLine에 이르기까지 모든 종류의 리소스 관리 함수에서 볼 수 있는 "우리를 호출하지 말고 우리가 호출하겠다"는 패턴과 정확히 동일합니다.

Channel은 리소스 관리와 관련하여 동일한 이점을 제공하지 않습니다. Flow는 수집 함수에 대한 단일 호출로 완전히 소비되는 반면, Channel에서 값을 가져오는 것은 여러 개의 개별 호출로 구성됩니다. 즉, Channel에는 시작점과 종료점이 명확하게 정의되어 있지 않으며 소비자가 이탈했을 때 어떤 일이 일어날지 보장할 수 없습니다.

val messages: ReceiveChannel<String> = GlobalScope.produce {
    try {
        send("Hello, World!")
        send("How are things?")
    } finally {
        println("Producer is going away") // ❌ this won't work!
    }
}
println(messages.receive())
error("That's all, folks.")

이 코드를 실행할 때 마지막 블록이 실행되지 않는 것을 주목하세요. 메시지 취소()에 누락된 호출을 추가하여 이 문제를 해결해 보면 다음 예제에서 코드를 변경하는 내용을 이해하는 데 도움이 될 것입니다.

consume과 같은 함수는 Channel에서 값 수신을 중단할 때 취소를 호출하는 것을 잊지 않도록 이 문제를 완화하는 데 도움이 되도록 설계되었습니다. 하지만 Channel의 경우 소비 또는 취소를 호출하는 것을 기억하는 것은 항상 생산자가 아닌 소비자의 책임입니다.

Channel의 리소스 관리는 완전히 캡슐화되어 있지 않습니다. Channel의 소비자가 생산자가 깔끔하게 종료되도록 하려면 Channel의 소비자는 행동이 바른 참여자여야 합니다. Channel을 반환하는 함수를 호출한 후 채널을 소비하거나 취소하는 것을 잊어버리면 관련 리소스가 무기한 열려 있는 상태로 유지될 수 있습니다.

Structured Concurrency

유출된 리소스가 폐쇄될 수 있는 다른 방법이 있기 때문에 "있을 수 있다"고 말씀드렸습니다. Channel은 일반적으로 코루틴 간의 통신에 사용됩니다. 코루틴을 처리할 때 리소스 누수를 방지하기 위해 구조화된 동시성을 사용할 수 있습니다.

아주 작은 변경으로 이전 예제에서 구조적 동시성을 제대로 사용하도록 만들 수 있습니다. GlobalScope 사용을 중단하고 대신 공유 코루틴 스코프 내에서 코드를 그룹화하기만 하면 됩니다.

coroutineScope {
    val messages: ReceiveChannel<String> = produce {
        try {
            send("Hello, World!")
            send("How are things?")
        } finally {
            println("Producer is going away") // ✅ now it works!
        }
    }
    println(messages.receive())
    error("That's all, folks.")

이제 모든 코드가 동일한 구조화된 코루틴 범위에 속하기 때문에 한 곳에서 오류가 발생하면 다른 코루틴이 취소되고 정리됩니다. 즉, Channel이 명시적으로 취소되지 않았더라도 프로듀서 코루틴의 최종 블록은 계속 실행됩니다.

이는 유용한 안전 조치입니다. 하지만 지저분하게 느껴진다면 그럴 만한 이유가 있습니다. Channel 취소 메커니즘과 그로 인해 발생하는 취소 예외는 구조화된 동시성이 도입되기 이전부터 사용되어 왔으며, 구조화된 동시성과 항상 잘 어울리지는 않습니다.

이제 구조적 동시성이 도입되었으므로 이 Channel의 리소스를 관리할 수 있는 방법은 두 가지가 있습니다. Channel 자체를 취소하는 것이 한 가지 방법이고, Channel의 값을 생성하는 코루틴을 취소하는 것이 또 다른 방법입니다. 어떤 것이 올바른 선택일까요? 결과는 동일할까요, 아니면 동작에 미묘한 차이가 있을까요? 안전을 위해 항상 두 가지를 모두 수행해야 할까요?

"The Silent Killer That's Crashing Your Coroutines"라는 글에서 취소된 Channel은 취소 예외를 발생시켜 예기치 않은 문제를 일으킬 수 있다고 설명한 바 있습니다. 취소된 코루틴과 취소된 Channel은 같은 유형의 예외를 재사용하기 때문에 취소된 Channel과 취소된 코루틴 사이의 경계가 모호해질 수 있습니다. 최악의 경우 Channel을 취소하면 원하지 않는데도 전체 코루틴이 자동으로 취소될 수 있습니다.

 

The Silent Killer That’s Crashing Your Coroutines

There’s only one safe way to deal with cancellation exceptions in Kotlin, and it’s not to re-throw them

betterprogramming.pub

이 시점에서 Channel은 자체적으로 어떤 리소스도 보유하지 않는다는 점을 분명히 할 필요가 있습니다. 사용 후 Channel을 비우거나 닫아야 한다는 규정은 없습니다. 더 이상 참조되지 않는 Channel은 다른 객체와 마찬가지로 자유롭게 가비지 컬렉션할 수 있습니다.

Channel을 취소하는 것은 생산자와 소비자 사이에 신호를 보내는 것 외에는 아무것도 하지 않습니다. 이렇게 하면 Channel의 다른 쪽 끝에 있는 코드가 값 전송을 중단하고 사용 중이던 리소스를 정리해야 한다는 것을 알 수 있습니다. 즉, Channel을 닫거나 취소해야 하는 유일한 이유는 Channel이 다른 코루틴 및 리소스와 함께 작업하는 데 활발히 사용되고 있을 때입니다. 이러한 코루틴과 리소스를 관리하기 위해 동시성을 구조화하면 Channel을 취소해야 할 이유가 전혀 없습니다.

Channel을 취소하거나 닫을 필요가 없는 세상에서 ReceiveChannel API가 어떤 모습일지 잠시 생각해 봅시다.

Channel은 영원히 열려 있으므로 receive를 호출하면 일시 중단되거나 값을 반환하며 예외가 발생하지 않습니다.
Channel에 실패하거나 닫힌 상태가 없기 때문에 receiveCatching 함수는 필요하지 않습니다. Channel 생산자에게 오류가 발생하면 소비자에게 전달되지 않고 실행 중인 코루틴 스코프에서 문제를 처리할 수 있습니다.
하지만 이 이론적인 Channel을 사용한 후에는 어떻게 해야 할까요? 생산자가 더 많은 값을 생성할 의도가 없을 때 소비자가 무한정 중단되는 것을 어떻게 막을 수 있을까요? 간단합니다. 소비자 코루틴을 취소하면 됩니다. 이는 구조화된 동시성으로 인해 많은 경우 자동으로 수행될 수 있습니다.

Shared Flows

"SharedFlow에서 Flow.collect에 대한 호출은 정상적으로 완료되지 않습니다."

SharedFlow는 Flow의 실제 값을 전달하는 것 외에는 업스트림 흐름과 다운스트림 수집기 간의 모든 통신을 없애기 때문입니다. 가상의 닫을 수 없는 Channel과 마찬가지로, SharedFlow는 소비자에게 오류나 종료를 전파할 수 없습니다.

val sharedFlow = flowOf("Hello, World!")
    .onCompletion { println("Upstream flow has no more values") }
    .shareIn(this, SharingStarted.Lazily) // 💬 try removing this line!

val collector = sharedFlow
    .onEach { println("Flow collector received '$it'") }
    .onCompletion { println("Flow collection stopped, error was $it") }
    .launchIn(this)

launch {
    while (collector.isActive) {
        println("Flow collector is waiting for more values...")
        delay(50)
    }
}
delay(250)
collector.cancel("Giving up waiting")

업스트림 Flow가 종료되면 SharedFlow의 소비자는 영원히 일시 중단되어 결코 오지 않는 값을 기다리게 됩니다. 이를 일반 Flow의 동작과 비교하려면 예제를 편집하여 shareIn을 호출하는 줄을 제거해 보세요.

업스트림 Flow이 가장 끔찍한 방식으로 충돌하더라도 SharedFlow는 계속 열려 있습니다. 오히려 구조화된 동시성이 아니었다면 그럴 수도 있습니다. 실제로는 업스트림 Flow에 오류가 발생하면 오류가 포함된 나머지 범위로 전파됩니다. 거기서 애플리케이션은 소비자 코루틴을 취소할지 여부를 결정할 수 있습니다.

val sharedFlow = flow<Any> { throw Exception("This is a disaster!") }
    .onCompletion { println("Upstream flow has no more values") }
    .shareIn(this, SharingStarted.Lazily) // 💬 try removing this line!

val collector = sharedFlow
    .onEach { println("Flow collector received '$it'") }
    .onCompletion { println("Flow collection stopped, error was $it") }
    .launchIn(this)

SharedFlow는 Channel의 더 나은 캡슐화된 버전과 같습니다. 소비자보다 오래 지속되는 활성 생산자 코루틴이 있을 수 있지만 소비자에게는 모든 오류, 리소스 및 취소를 숨깁니다.

결론은 SharedFlow는 일반 Flow와 똑같은 방식으로 소비자에게 취급될 수 있다는 것입니다. 수집을 호출하여 값을 하나씩 처리하기만 하면 됩니다. SharedFlow와 일반 Flow는 결국 동일한 인터페이스를 구현하기 때문입니다. 이것이 바로 우리가 Flow을 "Hot" 또는 "Cold"라고 부르는 데 신경 쓸 필요가 없는 근본적인 이유입니다. 리소스가 첨부되어 있을 수 있지만, 리소스를 처리하는 책임은 생산자와 코루틴 범위에 있는 것이지 소비자에게 있는 것이 아닙니다.

캡슐화와 대체 가능성은 Kotlin의 Flow를 매우 강력하고 다양하게 만드는 요소입니다. SharedFlow를 가져와서 일련의 연산자와 변환을 적용하면 결과 Flow이 더 뜨겁거나 차가워질까요? Cold Flow를 가져와서 코루틴을 실행하여 값을 모니터링하고 보강하는 것은 어떨까요? 대답은 간단합니다. 상관없습니다. 구조화된 동시성을 고수하고 리소스를 올바르게 관리하고 캡슐화하기만 하면 모든 Flow는 상호 교환할 수 있습니다.

함수가 Flow를 반환하면, 그 Flow의 출처를 알 필요 없이 바로 사용할 수 있습니다. 함수가 Flow를 받아들이면 원하는 대로 생성할 수 있고 올바르게 작동할 것이라는 것을 알 수 있습니다. Flow가 뜨겁다고 설명되었는지 차갑다고 설명되었는지 걱정할 필요가 없으며, 여러분도 마찬가지입니다.

The Right Tool For the Job

Channel과 Flow에 대해 이렇게 많은 이야기를 했는데도 어떤 것을 사용해야 할지 여전히 고민 중이신가요? 답은 간단합니다. 둘 다 사용하세요! Channel과 Flow을 같은 작업을 수행하는 두 가지 다른 방법이라고 생각하지 말고, 두 가지 작업을 위한 완전히 다른 두 가지 도구라고 생각하세요. Channel은 커뮤니케이션을 위한 도구이고, Flow는 캡슐화와 코드 재사용을 위한 도구입니다.

한 코루틴에서 다른 코루틴으로 값을 전달하고 싶을 때는 Channel을 사용합니다.
소비자가 언제 시작, 중지 또는 실패할지 걱정할 필요가 없도록 가치를 창출하는 코드를 캡슐화하려면 Flow를 사용하세요.
이 두 가지 도구는 함께 사용할 수도 있고 함께 사용해야 합니다. Channel 생산자와 Flow 소비자를 결합하는 기본 제공 API인 Channel Flow에 대한 전체 글도 작성했습니다. 동시성과 캡슐화의 이점을 누리고 싶다면 자유롭게 Channel을 만들어 Flow로 묶어보세요! Channel에서 몇 가지 값을 읽은 다음 Flow로 묶어 나머지 값과 정리 절차를 다른 코드에 위임하는 등 믹스 앤 매치를 사용할 수 있습니다.

Channel을 Flow로 래핑하면 애플리케이션을 더 안전하고 예측 가능하게 만들 수 있습니다. Flow가 종료될 때 어떤 일이 일어날지 결정할 수 있습니다. 소비자는 리소스를 정리하고 오류를 처리하는 것을 기억할 필요가 없으며, 잘못된 시간에 취소를 호출하는 소비자로 인해 코루틴이 종료되는 것에 대해 걱정할 필요가 없습니다.

하지만 캡슐화에는 한계가 있습니다. 외부에서 보기에 아무리 깔끔하고 정돈된 벽처럼 보이더라도 벽 뒤에는 항상 코드가 존재합니다. Flow가 제공하는 캡슐화는 주로 단일 함수 호출로 표현할 수 있다는 사실에서 비롯되며, 이를 통해 모든 구조화된 프로그래밍이 보장됩니다. 하지만 단일 함수 호출에는 단일 제어 흐름이 필요합니다. 동일한 함수나 동일한 코루틴 내에 있지 않은 생산자와 소비자 간에 값을 전달해야 하는 경우, Flow가 제공하는 캡슐화 경계를 넘어야 합니다.
// You can't do this with a flow!
val job1 = launch {
    channel.send("Hello from job 1!")
    println("Job 1 received a message: ${channel.receive()}")
}
val job2 = launch {
    println("Job 2 received a message: ${channel.receive()}")
    channel.send("Hello from job 2!")
}
여러 코루틴이 동시에 소비하거나 생성하는 경우, Channel은 작업을 배포하고 조정하는 데 사용하는 통신 도구입니다. 하지만 Flow와 구조화된 동시성을 적절히 사용하면 Channel과 모든 코루틴을 래핑하고 캡슐화하여 나머지 애플리케이션이 걱정할 필요가 없도록 할 수 있습니다.

'Kotlin > Kotlin' 카테고리의 다른 글

Kotlin | Backing Property & Backing Field  (0) 2024.01.19
Kotlin | 왜 코틀린에서 "by" 키워드를 사용할까??  (0) 2023.11.01
Kotlin | KSP vs KAPS  (0) 2023.09.20
Kotlin | Deep Dive into Sealed Interfalces  (0) 2023.09.03
Kotlin | Generic  (0) 2023.08.27