채널 타입

채널 타입 (Channel types)

여러 고루틴이 데이터를 주고받으려면 어떻게 해야 할까요? Go에서는 채널(channel) 이라는 타입이 이 역할을 맡아요. 채널은 정해진 요소 타입(element type)의 값을 주고받으면서, 동시에 실행되는 함수들이 서로 통신할 수 있게 해주는 장치예요. 다시 말해, 동시에 실행되는 함수들보내고(send) 받으며(receive) 대화하는 창구라고 생각하시면 돼요.

채널이 초기화되지 않은 상태의 nil이에요. 이건 지금 기억해 두실 필요까지는 없고, "초기화 안 된 채널은 nil이다" 정도로만 알아두시면 돼요.

출처: Go Specification

본문

채널 타입의 문법은 다음과 같아요. EBNF로 표현하면 이렇게 되죠.

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

선택적인 <- 연산자가 채널의 방향(direction) 을 정해요. 방향은 송신(send) 이거나 수신(receive) 이죠. 방향이 주어지면 채널은 방향성(directional) 채널이 되고, 방향이 없으면 양방향(bidirectional) 채널이 돼요.

여기서 중요한 점이 하나 있어요. 채널은 대입(assignment)이나 명시적인 변환(conversion)을 통해 송신 전용 또는 수신 전용으로 제한될 수 있어요. 처음부터 "이 채널은 보내기만 가능한 채널이야"라고 정해두고 싶을 때 쓰는 거예요.

구체적인 예를 볼게요.

chan T          // can be used to send and receive values of type T
chan<- float64  // can only be used to send float64s
<-chan int      // can only be used to receive ints

첫 줄의 chan T는 타입 T의 값을 보내고 받는 데 모두 쓸 수 있어요. 둘째 줄 chan<- float64float64 값을 보내기만 할 수 있고, 셋째 줄 <-chan intint 값을 받기만 할 수 있어요. 화살표의 방향이 곧 데이터 흐름의 방향이라고 보시면 자연스러워요.

<- 연산자는 가능한 가장 왼쪽의 chan 과 결합해요. 이 결합 규칙 때문에 괄호를 어디에 두느냐가 중요한데, 아래 코드가 그 규칙을 보여줘요.

chan<- chan int    // same as chan<- (chan int)
chan<- <-chan int  // same as chan<- (<-chan int)
<-chan <-chan int  // same as <-chan (<-chan int)
chan (<-chan int)

한 줄씩 보면 이렇게 이해하면 돼요. chan<- chan intchan<- (chan int)와 같아서, "채널을 보내는 채널"이 되는 거고요. <-chan <-chan int<-chan (<-chan int)와 같아서 "채널을 받는 채널"을 받는 형태예요. 혼란스러우면 괄호로 명확히 묶어주면 되는데, 마지막 줄 chan (<-chan int)처럼 직접 괄호를 써서 의도를 드러내는 것도 좋아요.

새로 초기화된 채널 값은 내장 함수 make로 만들 수 있어요. make는 채널 타입과 함께 선택적인 용량(capacity) 을 인자로 받아요.

make(chan int, 100)

이 괄호 안의 숫자가 바로 채널의 용량이에요. 용량은 "요소 개수 기준"으로 채널 안의 버퍼(buffer) 크기를 정해요. 여기서부터가 채널을 이해하는 핵심이에요.

  • 용량이 0이거나 생략되면, 채널은 버퍼가 없는(unbuffered) 채널이 돼요. 이 경우 통신은 송신자와 수신자가 모두 준비된 상태에서만 성공해요.
  • 그 외의 경우에는 버퍼가 있는(buffered) 채널이라서, 버퍼가 가득 차지 않았으면(보내기) 또는 비어 있지 않았으면(받기) 블로킹 없이 통신이 성공해요.

그리고 nil 채널은 통신에 절대 준비되지 않아요. 이건 채널의 조용한 함정이니 꼭 기억해 두시면 좋아요.

채널은 내장 함수 close로 닫을 수 있어요. 수신 연산자(receive operator)의 다중 값 대입 형태는 받은 값이 채널이 닫히기 전에 보내진 값인지를 알려줘요. 즉 채널을 닫고 나서도 수신 측에서 "이 값은 정상적으로 보내진 거였는지, 아니면 닫힌 뒤의 잔여물인지"를 구분할 수 있게 해주는 거죠.

마지막으로, 하나의 채널은 추가 동기화 없이도 얼마든지 많은 고루틴이 함께 쓸 수 있어요. 채널은 send statements, receive operations, 그리고 내장 함수 caplen 호출에 안전하게 사용할 수 있어요. 그리고 채널은 선입선출(first-in-first-out) 큐처럼 동작해요. 예를 들어 한 고루틴이 채널에 값을 보내고 다른 고루틴이 그 값을 받는다면, 값은 보낸 순서대로 받게 돼요. 그래서 메시지의 순서가 중요한 작업에서도 채널을 안심하고 쓸 수 있어요.

더 알아보기

채널은 goroutine과 함께 Go의 동시성(concurrency)을 이루는 두 축이에요. 더 깊이 알고 싶다면 Go Specification 원문을 참고하시고, 관련해서 알아두면 좋은 개념은 이쪽이에요.