코딩

[코루틴 (5)] 코루틴 채널(Channel)과 select

Eastpark 2025. 1. 18. 19:04
728x90
반응형

지난 4편에서 코루틴 예외 처리와 취소(Cancellation)에 대해 살펴보았습니다. 이번 글에서는 채널(Channel)이라는 개념을 중심으로, 코루틴 간 메시지를 주고받는 방식과 여러 연산을 동시에 처리하기 위한 select 문법을 살펴보려고 합니다.

 

핵심 키워드

Channel

Producer-Consumer

BufferedChannel

RendezvousChannel

select


1. 채널(Channel)이란?

 

1.1 코루틴 간의 통신 도구

코루틴 채널(Channel)은 여러 코루틴이 데이터를 주고받는 파이프라인을 구성할 수 있게 해주는 도구입니다. 간단히 말해, “코루틴 버전의 큐(Queue)”라고 볼 수 있습니다.

하나의 코루틴이 채널에 send()로 메시지를 넣으면, 다른 코루틴에서 receive()로 메시지를 받을 수 있습니다.

채널은 동시성 제어를 자동으로 처리하므로, mutual exclusion(상호 배제) 등의 복잡한 로직을 직접 구현할 필요가 줄어듭니다.

 

1.2 Producer-Consumer 패턴 예시

채널을 활용하면, 한쪽 코루틴이 생산자(Producer) 역할을, 다른 쪽이 소비자(Consumer) 역할을 할 수 있습니다.

val channel = Channel<Int>()

launch {
    // Producer
    for (x in 1..5) {
        channel.send(x)
        println("Sent $x")
    }
    channel.close() // 더 이상 보낼 데이터 없음
}

launch {
    // Consumer
    for (y in channel) { 
        println("Received $y")
    }
    println("Channel closed, done receiving")
}

Producer는 1부터 5까지 채널에 차례대로 전송(send)하고, 전송이 끝나면 channel.close()로 채널을 닫습니다.

Consumer는 for (y in channel) 문법으로 메시지를 계속 받으며, 채널이 닫히면 반복문을 빠져나갑니다.


2. 다양한 채널 종류

 

2.1 기본 채널: RendezvousChannel

기본 설정(Channel())은 RendezvousChannel로 동작합니다.

송신자(Sender)가 send()를 호출하면, 수신자(Receiver)가 receive()를 하기 전까지는 일시 중단됩니다. 마찬가지로, 수신자가 receive()를 호출했을 때 아직 send()가 없으면 일시 중단됩니다.

즉, 둘 중 하나가 없으면 서로를 기다리는 형태입니다.

 

2.2 버퍼(Channel.BUFFERED)

val bufferedChannel = Channel<Int>(Channel.BUFFERED)

내부 버퍼를 사용하는 채널로, 송신자가 send()할 때 수신자가 당장 없더라도 버퍼에 메시지를 보관할 수 있습니다.

버퍼가 꽉 차면 send()가 일시 중단되고, 버퍼에 여유 공간이 생길 때까지 기다립니다.

 

2.3 무제한 버퍼(Channel.UNLIMITED) & 제한 크기 채널

Channel.UNLIMITED: 버퍼가 사실상 무제한이므로, send()가 일시 중단되지 않습니다. 다만 메모리 사용량이 커질 수 있어 주의가 필요합니다.

특정 용량을 정해 Channel(capacity = n)을 직접 설정해둘 수도 있습니다.

 

2.4 ConflatedChannel

가장 최근에 보낸 값만 유지하고, 이전 값은 덮어써 버리는 채널 방식입니다. 예: UI 상태 업데이트 시 이전 데이터는 크게 중요하지 않은 경우에 사용합니다.

반응형

3. 예제: Producer-Consumer 파이프라인

suspend fun producer(channel: SendChannel<Int>) {
    for (i in 1..5) {
        println("Producer sending $i")
        channel.send(i)
        delay(100)
    }
    channel.close()
}

suspend fun consumer(channel: ReceiveChannel<Int>) {
    for (value in channel) {
        println("Consumer received $value")
        delay(200)
    }
}

fun main() = runBlocking {
    val channel = Channel<Int>(Channel.BUFFERED)
    launch { producer(channel) }
    launch { consumer(channel) }
    // producer와 consumer 코루틴이 끝날 때까지 대기
}

1. producer는 1~5까지 생성해 channel.send(...)로 보냅니다.

2. consumerfor (value in channel)을 통해 메시지를 수신하고, 일부 처리를 진행합니다.

3. 채널을 close()하면 consumer는 더 이상 데이터를 기다리지 않고 루프를 종료합니다.


4. select를 사용한 다중 채널/연산 처리

 

4.1 select란 무엇인가

select는 여러 채널 또는 여러 비동기 연산 중 가장 먼저 준비된 작업을 선택해 실행하는 문법입니다. 일종의 “non-blocking select”라고 할 수 있습니다.

예: 두 개의 채널에서 동시에 데이터를 기다린다든지, 채널에서 메시지 수신 vs 타임아웃(Delay) 중 먼저 도착한 이벤트를 처리하고 싶을 때 유용합니다.

 

4.2 예제: 두 채널 중 먼저 도착한 데이터를 받기

suspend fun selectExample(channelA: ReceiveChannel<String>, channelB: ReceiveChannel<String>) {
    select<Unit> {
        channelA.onReceive { valueA ->
            println("Received from A: $valueA")
        }
        channelB.onReceive { valueB ->
            println("Received from B: $valueB")
        }
    }
}

1. channelA.onReceive { ... }channelB.onReceive { ... }를 동시에 감시합니다.

2. 실제로 먼저 데이터가 도착한 채널 쪽 분기(valueA 또는 valueB)가 실행됩니다.

3. 실행이 완료되면 select 블록은 종료됩니다.

 

4.3 타임아웃이나 default 처리

타임아웃: onTimeout(millis) { ... }를 사용해 일정 시간 안에 데이터가 오지 않으면 다른 로직을 실행할 수 있습니다.

기본 분기: select에서 아무 이벤트도 오지 않을 경우, onTimeout(0)과 같은 방식으로 “기본 분기”를 작성해둘 수도 있습니다.

728x90

5. 채널/select를 활용한 실무 사례

 

1. UI 이벤트 처리: UI에서 발생하는 클릭/스와이프 등의 이벤트를 ConflatedChannel로 보관한 뒤, 코루틴에서 이를 수신해 로직을 처리할 수 있습니다.

 

2. 멀티플 소스 데이터 병합: 네트워크, 로컬 캐싱, BLE 신호 등 서로 다른 채널에서 데이터를 받아, select를 통해 먼저 도착한 신호부터 처리할 수 있습니다.

 

3. Producer-Consumer 파이프라인: 여러 Producer가 하나의 Channel에 메시지를 넣고, 여러 Consumer가 병렬로 해당 메시지를 처리하게 만들어 동시성을 높일 수 있습니다.


마무리 및 다음 예고

 

이번 편에서는 코루틴 채널(Channel)select 문법을 통해 코루틴끼리 데이터를 주고받거나, 여러 이벤트 중 먼저 도착한 것을 처리하는 방법을 알아보았습니다.

Channel의 다양한 유형(Rendezvous, Buffered, Conflated)을 이해하고, Producer-Consumer 패턴을 쉽게 구현할 수 있다는 점

select를 통해 동시에 여러 채널을 감시하거나, 타임아웃 시나리오를 처리할 수 있다는 점

 

다음 6편에서는 Flow 심화를 다룰 예정입니다. 코틀린 Flow의 구조와 원리, StateFlow, SharedFlow 등의 개념을 포함해, RxJava나 LiveData와는 어떻게 다른지도 자세히 살펴보겠습니다.


시리즈 전체 목차

0. 코루틴 탄생 배경과 기본 개념

1. 코루틴 기초(suspend, CoroutineScope, Dispatcher)

2. Structured Concurrency와 코루틴 스코프 관리

3. 예외 처리와 취소(Cancellation) 기법

4. 코루틴 채널(Channel)과 select

5. Flow 심화(Flow, StateFlow, SharedFlow)

6. 안드로이드 실무 적용(Retrofit, Room, ViewModel)

7. 동시성 문제 해결(Mutex, Semaphore)

8. 코루틴 테스트 & 디버깅 전략

9. 고급 퍼포먼스 최적화

10. WorkManager + 코루틴 연동

11. LifecycleScope & 안드로이드 생명주기 대응

12. Compose + 코루틴 + Flow 시너지

13. Compose Navigation + 코루틴 취소/재시작 사례

728x90
반응형