📡

Channel — goroutine 사이의 파이프라인

Ruby에는 없는 개념. goroutine 간 데이터를 안전하게 주고받는 통로.

Ruby에서 스레드 간 데이터를 주고받으려면 Queue를 쓴다: queue = Queue.new; Thread.new { queue.push(42) }; val = queue.pop.

Go의 channel도 비슷하다. 하지만 언어 문법에 내장되어 있다:

ch := make(chan int)
go func() { ch <- 42 }()  // 보내기
val := <-ch                // 받기

<- 연산자가 핵심이다. ch <- 42는 채널에 값을 보내고, <-ch는 채널에서 값을 받는다. Ruby의 Queue#push/pop과 같은 역할인데, 문법이 간결하다.

버퍼 채널 vs 언버퍼 채널

make(chan int) — 언버퍼. 보내는 쪽이 받는 쪽이 준비될 때까지 블로킹된다. 동기적 핸드오프.
make(chan int, 10) — 버퍼 10. 버퍼가 가득 찰 때까지 보내는 쪽이 블로킹되지 않는다. Ruby의 SizedQueue.new(10)과 비슷.

select — 여러 채널을 동시에 대기

select {
case msg := <-ch1:
    fmt.Println(msg)
case msg := <-ch2:
    fmt.Println(msg)
case <-time.After(5 * time.Second):
    fmt.Println("timeout")
}

여러 채널 중 먼저 값이 오는 쪽을 처리한다. Ruby에는 이런 게 없다. IO.select가 소켓에서 비슷한 역할을 하지만, 범용 채널 멀티플렉싱은 Go만의 영역이다.

파이프라인 패턴

channel을 연결하면 Unix 파이프라인처럼 데이터 처리 파이프라인을 만들 수 있다:

생성 → 필터 → 변환 → 출력

각 단계가 goroutine으로 동시 실행된다. Ruby에서 array.select.map.each 체이닝을 하듯이, Go에서는 channel을 연결해서 스트림 처리를 한다.

Ruby에서 Go로

1

Ruby: Queue.new / push / pop → Go: make(chan T) / ch <- val / <-ch

2

Ruby: SizedQueue.new(n) → Go: make(chan T, n) (버퍼 채널)

3

Ruby: 해당 없음 → Go: select { case } (여러 채널 동시 대기)

4

Ruby: .select.map.each 체이닝 → Go: channel 연결 파이프라인

장점

  • 언어 레벨에서 지원 — 별도 라이브러리 없이 동시성 패턴 구현 가능
  • select로 타임아웃, 취소, 멀티플렉싱을 간결하게 표현

단점

  • 채널 방향을 잘못 쓰면 데드락 — 디버깅이 까다롭다
  • Ruby의 Enumerable 체이닝에 비해 파이프라인 코드가 장황하다

사용 사례

동시 HTTP 요청의 결과를 모아서 처리해야 할 때 (fan-out/fan-in 패턴) 작업 큐를 goroutine + channel로 구현하여 Sidekiq 없이 백그라운드 처리