사전 채널
용어함정

채널

gabury1고친 사람 github-actions[bot]

채널은 한쪽이 넣은 값을 다른 쪽이 꺼내 가게 이어 줍니다. 그런데 어디서 쓰느냐에 따라 가리키는 물건이 전혀 다릅니다. 동시성 프로그래밍에서는 동시에 도는 두 작업이 값을 건네는 통로입니다. 원격 호출에서는 서버 하나로 가는 연결을 맡아 두는 객체입니다.

쉽고 빠른 이해

무슨 일을 하는 물건인가 — 한쪽이 넣은 값을 다른 쪽이 꺼내 가게 이어 줍니다. Go 에서는 두 작업이 계산 결과를 주고받을 때 채널을 씁니다. 원격 호출 라이브러리에서는 서버 하나로 가는 연결을 채널이라고 부릅니다. 두 뜻은 이름만 같고 서로 다른 물건입니다.

왜 이렇게 하나 — 동시에 도는 작업들이 변수 하나를 같이 고치면 값이 엉킵니다. 채널로 값을 건네면 한 번에 한쪽만 그 값을 쥡니다. 원격 호출 쪽에서는 연결을 맺고 다시 잇는 일을 채널이 맡습니다. 호출하는 코드는 그 일을 신경 쓰지 않습니다.

어떻게 도나

  1. 동시성의 채널은 한쪽이 값을 넣으면 다른 쪽이 꺼낼 때까지 기다립니다
  2. 칸을 몇 개 두면 그만큼은 기다리지 않고 먼저 넣어 둡니다
  3. 원격 호출의 채널은 서버 주소 하나를 맡아 연결을 들고 있습니다. 여러 호출이 그 연결을 함께 씁니다

대가 — 동시성의 채널은 넣는 쪽과 꺼내는 쪽이 서로를 기다리다 둘 다 멈출 수 있습니다. 그래서 채널은 작업을 나눠 주거나 결과를 모을 때 씁니다. 여러 작업이 한 값을 오래 같이 읽고 고칠 때는 잠금을 쓰는 코드가 더 짧습니다.

원격 호출의 채널은 만들 때 연결 비용이 듭니다. 호출마다 새로 만들면 호출마다 그 비용을 냅니다. 그래서 서버 주소마다 한 번 만들어 프로그램이 끝날 때까지 씁니다.

상세

이 절은 「채널」이 가리키는 물건들을 나란히 놓고 봅니다. 먼저 뜻들을 표 하나로 가릅니다. 그다음 백엔드 개발자가 가장 자주 만나는 두 뜻을 따라갑니다. 동시성의 채널은 Go 코드 몇 줄로, 원격 호출의 채널은 자바 클라이언트 코드 몇 줄로 봅니다.

끝에서는 나머지 뜻들을 짧게 짚습니다. 대화에서 어느 뜻인지 알아채는 단서도 모읍니다.

채널이 가리키는 물건들

뜻마다 쓰는 분야와 하는 일이 다릅니다. 공통점은 하나입니다. 무언가가 한쪽에서 다른 쪽으로 지나가는 통로라는 점입니다.

맥락 채널이 하는 일 예
동시성 같은 프로그램 안에서 동시에 도는 작업끼리 값을 건넨다 Go 의 chan · 코틀린 코루틴의 Channel
원격 호출 서버 주소 하나로 가는 연결을 맡아 호출을 실어 보낸다 gRPC 의 채널
자바 입출력 파일이나 소켓과 메모리 사이에서 바이트를 옮긴다 FileChannel · SocketChannel
발행·구독 메시징 메시지를 이름으로 갈라 구독자들에게 뿌린다 Redis 의 발행·구독 채널
통신 이론 신호가 지나가는 매체 구리선 · 전파

백엔드 개발자가 자주 만나는 것은 앞의 둘입니다. 둘은 이름만 같고 서로 이어지지 않습니다.

동시성의 채널

동시성 프로그래밍은 여러 작업을 한 프로그램 안에서 겹쳐 돌리는 일입니다. Go 에서는 이 작업 하나를 고루틴이라고 부릅니다. 고루틴은 Go 런타임이 관리하는 가벼운 실행 흐름입니다. 스레드보다 훨씬 싸게 만들 수 있어서 수천 개를 띄우기도 합니다.

고루틴들은 한 프로그램의 메모리를 같이 봅니다. 두 고루틴이 같은 변수를 동시에 고치면 한쪽이 쓴 값을 다른 쪽이 덮어씁니다. 이렇게 실행 순서에 따라 결과가 바뀌는 오류가 경쟁 상태입니다.

흔한 해법은 뮤텍스 같은 잠금입니다. 한 번에 한 고루틴만 그 변수를 만지게 막는 방식입니다.

채널은 다른 길을 갑니다. 변수를 같이 만지지 않고 값을 건넵니다. 넘긴 쪽이 그 값을 더 만지지 않으면, 그 값을 쥔 고루틴은 받은 쪽 하나뿐입니다.

두 방식은 맞는 경우가 다릅니다. 작업을 나눠 주거나 결과를 모으는 것처럼 값의 주인이 옮겨 가는 흐름에는 채널이 맞습니다. 캐시나 카운터처럼 여러 고루틴이 한 값을 오래 같이 읽고 고치는 경우에는 뮤텍스를 쓰는 코드가 더 짧습니다.

넣고 꺼내는 모양

채널은 make 로 만듭니다. 값을 넣을 때와 꺼낼 때 모두 화살표 연산자 <- 를 씁니다. 화살표가 채널 쪽을 가리키면 넣기입니다. 채널에서 나오면 꺼내기입니다.

go 키워드를 붙인 함수는 새 고루틴에서 돕니다. 아래 코드에서는 새 고루틴이 값을 넣고 원래 흐름이 그 값을 꺼냅니다. 원래 흐름도 고루틴 하나입니다.

Go
ch := make(chan int)
go func() {
    ch <- 42     // 채널에 넣는다
}()
v := <-ch        // v 는 42

chan int 는 정수만 나르는 채널이라는 뜻입니다. 채널마다 나르는 값의 타입이 정해져 있습니다. 다른 타입을 넣으려 하면 컴파일할 때 걸립니다.

버퍼가 없는 채널

위 코드처럼 make(chan int) 로 만든 채널은 값을 담아 둘 칸이 없습니다. 이런 채널에서는 넣는 쪽과 꺼내는 쪽이 만나야 값이 건너갑니다.

넣는 쪽이 먼저 오면 꺼내는 쪽이 올 때까지 멈춰 기다립니다. 꺼내는 쪽이 먼저 오면 넣는 쪽을 기다립니다. 값이 건너가는 순간 두 고루틴이 서로의 진행을 확인합니다. 그래서 버퍼 없는 채널은 값을 나르는 일과 순서를 맞추는 일을 한 번에 합니다.

sequenceDiagram
    participant S as 넣는 고루틴
    participant C as 채널
    participant R as 꺼내는 고루틴
    S->>C: 값을 넣으려 한다
    Note over S,C: 꺼내는 쪽이 없어 기다린다
    R->>C: 꺼내려 한다
    C-->>R: 값을 건넨다
    Note over S,R: 둘 다 다음 줄로 넘어간다

그림은 넣는 고루틴이 먼저 온 경우입니다. 꺼내는 고루틴이 나타날 때까지 넣는 고루틴은 한 줄도 더 나가지 않습니다.

버퍼가 있는 채널

make 에 두 번째 인자로 칸 수를 주면 버퍼가 있는 채널이 됩니다. 버퍼는 값을 잠깐 담아 두는 칸입니다. 칸이 남아 있는 동안은 넣는 쪽이 기다리지 않고 바로 다음 줄로 갑니다.

Go
ch := make(chan int, 2)
ch <- 1               // 바로 끝난다
ch <- 2               // 바로 끝난다
fmt.Println(len(ch))  // 2
fmt.Println(<-ch)     // 1

len 은 지금 칸에 들어 있는 값의 수를 돌려줍니다. 먼저 넣은 1이 먼저 나왔습니다. 채널은 들어온 순서대로 내보냅니다.

칸이 둘 다 찬 상태에서 셋째 값을 넣으면 누군가 하나를 꺼낼 때까지 기다립니다. 칸이 비었을 때 꺼내려 하면 누군가 넣을 때까지 기다립니다.

자바에서 가장 가까운 것은 BlockingQueue 입니다. 크기를 정한 BlockingQueue 도 가득 차면 넣는 쪽을, 비면 꺼내는 쪽을 기다리게 합니다. 버퍼 있는 채널이 이와 같습니다. 버퍼 없는 채널은 칸이 하나도 없는 SynchronousQueue 와 같습니다.

채널 닫기

보낼 값이 더는 없으면 close 로 채널을 닫습니다. 닫는 것은 「이제 끝」이라는 신호를 받는 쪽에 보내는 일입니다. 받는 쪽은 남은 값을 다 꺼낸 뒤에야 끝을 알아챕니다.

앞 코드에서 1을 꺼낸 뒤 채널에는 2가 남아 있습니다. 여기서 채널을 닫고 두 번 꺼내 봅니다. 꺼낼 때 받는 변수를 v, ok 처럼 둘 두면 꺼낸 값과 함께 참·거짓 하나가 더 옵니다. 둘째 변수 ok 에는 채널이 아직 값을 주는지가 담깁니다.

Go
close(ch)
v, ok := <-ch   // v 는 2, ok 는 true
v, ok = <-ch    // v 는 0, ok 는 false

남은 값이 없는 닫힌 채널은 그 타입의 기본값과 false 를 돌려줍니다. 그래서 for v := range ch 로 채널을 돌면, 닫히고 비는 순간 반복이 끝납니다.

닫는 일은 넣는 쪽이 맡습니다. 닫힌 채널에 값을 넣으면 panic 이 나기 때문입니다. panic 은 Go 프로그램을 멈추는 런타임 오류입니다. 꺼내는 쪽이 닫으면 아직 넣던 고루틴이 이 오류를 냅니다.

채널에서 멈추는 경우

채널은 기다림을 쉽게 만드는 만큼 영영 기다리는 경우도 쉽게 만듭니다. 모든 고루틴이 채널 앞에서 서로를 기다리면 아무도 움직이지 못합니다. 이것이 교착 상태입니다.

Go 런타임은 모든 고루틴이 멈춘 것을 알아채면 오류를 내고 프로그램을 끝냅니다. 일부 고루틴만 멈춘 경우는 알아채지 못합니다. 그 고루틴은 메모리를 쥔 채 남습니다. 이렇게 끝나지 못하고 남는 고루틴을 고루틴 누수라고 부릅니다.

한 채널만 보고 기다리면 이런 일이 잦아집니다. select 문은 여러 채널을 한꺼번에 보고 먼저 준비된 쪽 하나를 처리합니다.

select 에는 시간 제한 채널을 함께 넣을 수 있습니다. 시간 제한 채널은 정한 시간이 지나면 값 하나가 들어오는 채널입니다. 다른 채널이 끝내 값을 주지 않아도 그 시간이 지나면 select 가 빠져나옵니다. 그래서 영영 기다리는 일을 막습니다.

코틀린의 채널

코루틴은 실행 도중 멈췄다가 나중에 이어서 도는 함수입니다. 코틀린 코루틴 라이브러리에도 같은 생각의 채널이 있습니다. 채널에는 send 로 넣고 receive 로 꺼냅니다.

기다리는 방식은 Go 와 같습니다. 칸이 다 찼으면 넣는 쪽이 멈춥니다. 비어 있으면 꺼내는 쪽이 멈춥니다. 이때 멈추는 것은 코루틴뿐입니다. 그 밑의 스레드는 다른 코루틴을 돌리러 갑니다.

원격 호출의 채널

원격 프로시저 호출은 다른 서버에 있는 함수를 내 코드의 메서드처럼 부르는 방식입니다. gRPC(gRPC Remote Procedure Calls)는 이 방식을 구현한 프레임워크 가운데 하나입니다. 서비스끼리 서로 부르는 백엔드 내부 통신에서 흔히 씁니다.

gRPC 클라이언트 코드는 스텁을 부릅니다. 스텁은 서버의 메서드와 같은 이름의 메서드를 가진 로컬 객체입니다. 스텁의 메서드를 부르면 스텁이 인자를 메시지로 싸서 서버로 보냅니다.

스텁이 그 메시지를 실어 보내는 길이 채널입니다. 채널은 서버 주소 하나를 맡습니다. 스텁을 만들 때 채널을 넘겨야 하므로, 채널이 먼저 있어야 스텁도 생깁니다.

채널이 뒤에서 하는 일

채널은 가상의 연결입니다. 채널 하나 뒤에 연결이 아직 없을 수도, 하나일 수도, 여럿일 수도 있기 때문입니다.

채널은 주소를 받으면 연결에 필요한 일을 알아서 합니다. 먼저 서버 이름을 IP(Internet Protocol) 주소로 바꾸는 이름 해석을 합니다. 그다음 그 주소로 연결을 맺습니다. 연결이 끊기면 다시 잇습니다.

서버가 여러 대면 호출을 여러 서버에 나눠 보내는 부하 분산도 맡습니다.

아래 그림은 주문 서비스가 결제 서버를 부르는 경우입니다. 스텁 둘은 모두 주문 서비스 코드 안에 있습니다. 결제 스텁은 결제 서버의 결제 기능을 부릅니다. 환불 스텁은 같은 서버의 환불 기능을 부릅니다.

두 스텁은 결제 서버 주소를 맡은 채널 하나를 함께 씁니다. 결제 서버는 두 대입니다.

flowchart TD
    subgraph O["주문 서비스"]
        A["결제 스텁"]
        B["환불 스텁"]
    end
    A --> CH["채널 · 결제 서버 주소 하나"]
    B --> CH
    subgraph L["채널이 뒤에서 들고 있는 연결"]
        C1["연결 1 · 결제 서버 1"]
        C2["연결 2 · 결제 서버 2"]
    end
    CH --> C1
    CH --> C2

그림에서 두 스텁은 채널 하나만 봅니다. 연결이 몇 개인지, 어느 서버로 가는지는 채널이 정합니다. 스텁 쪽 코드는 서버가 한 대에서 두 대로 늘어도 바뀌지 않습니다.

채널이 맺는 연결은 HTTP/2(HyperText Transfer Protocol version 2) 연결입니다. HTTP/2 는 연결 하나 위에 여러 요청을 섞어 동시에 보냅니다. 그래서 스텁 여럿이 채널 하나를 동시에 써도 호출끼리 서로를 기다리지 않습니다.

채널을 만들고 쓰는 코드

아래는 자바 클라이언트가 채널을 만들고 그 위에 스텁을 세우는 코드입니다. Greeter 는 서버가 내놓은 서비스 이름입니다. request 는 미리 만들어 둔 요청 메시지입니다.

Java
ManagedChannel channel = ManagedChannelBuilder
    .forAddress("localhost", 50051)
    .usePlaintext()   // 암호화 없이 잇는다
    .build();

var stub = GreeterGrpc.newBlockingStub(channel);
var reply = stub.sayHello(request);

channel.shutdown();

build() 가 끝나도 연결은 아직 없습니다. 연결은 첫 호출인 sayHello 를 할 때 맺어집니다. 다 쓴 채널은 shutdown() 으로 닫아 연결을 돌려줍니다.

채널은 오래 들고 나눠 쓴다

채널을 만들면 연결을 맺는 비용이 듭니다. 이름 해석을 하고 연결을 새로 엽니다. 암호화를 켰다면 그 위에 암호화 협상도 합니다.

그래서 채널은 서버 주소마다 한 번 만들어 애플리케이션이 끝날 때까지 씁니다. 스텁 여럿과 호출 수천 번이 그 채널 하나를 나눠 씁니다. 호출마다 채널을 새로 만들면 호출마다 연결 비용을 다시 냅니다.

데이터베이스 커넥션 풀도 연결을 오래 들고 나눠 씁니다. 차이는 연결 하나를 쓰는 방식에 있습니다. 데이터베이스 연결 하나는 한 번에 쿼리 하나만 나릅니다. 채널이 쓰는 HTTP/2 연결 하나는 여러 호출을 동시에 나릅니다.

그 밖의 채널

자바 입출력의 채널 — 자바의 NIO(New Input/Output) 패키지에도 Channel 이 있습니다. 파일이나 소켓 같은 입출력 대상과 이어진 통로입니다. 쓸 때는 ByteBuffer 에 담은 바이트를 채널로 보냅니다. 읽을 때는 채널에서 받은 바이트를 ByteBuffer 에 담습니다.

NIO 의 소켓 채널은 논블로킹 방식으로도 쓸 수 있습니다. 논블로킹은 읽을 데이터가 없어도 기다리지 않고 바로 돌아오는 방식입니다.

자바 네트워크 프레임워크 Netty 의 Channel 도 소켓 연결 하나를 가리키는 객체입니다.

발행·구독의 채널 — Redis 의 발행·구독 기능은 메시지를 채널 이름으로 가릅니다. 한 클라이언트가 채널에 메시지를 올리면 그 채널을 구독하던 클라이언트가 모두 받습니다. 그 순간 구독하지 않던 클라이언트는 그 메시지를 받지 못합니다.

통신 이론의 채널 — 신호가 한쪽에서 다른 쪽으로 지나가는 매체입니다. 구리선이나 전파가 채널입니다. 한 채널로 오류 없이 보낼 수 있는 최대 속도를 채널 용량이라고 합니다.

어느 뜻인지 가르는 단서

대화나 문서에서 「채널」이 나오면 함께 붙은 낱말을 봅니다. 대개 그것만으로 어느 뜻인지 갈립니다.

함께 나오는 말 뜻
고루틴 · <- · 버퍼 크기 · 닫는다 · select 동시성
스텁 · 서버 주소 · ManagedChannel · 다시 연결 원격 호출
ByteBuffer · FileChannel · 논블로킹 자바 입출력
발행 · 구독 · 구독자 발행·구독 메시징
대역폭 · 잡음 · 채널 용량 통신 이론

「채널 바인딩」은 이름에 같은 낱말이 들어 있을 뿐 여기 어느 뜻과도 다릅니다. 채널 바인딩은 보안 기법입니다. 로그인 같은 인증을 그 인증이 오간 암호화 연결에 묶습니다. 그러면 그 연결 밖에서는 그 인증을 못 씁니다. 누군가 중간에서 인증을 가로채 다른 연결에서 다시 쓰는 일을 막으려는 것입니다.

관련 항목

채널로 값을 주고받는 동시성 실행 단위

고루틴 · 코루틴 · Kotlin 코루틴 · 스레드 · Go · Kotlin

채널이 대신하는 공유 메모리 동기화 수단

뮤텍스 · 잠금 · 공유 메모리 · 원자적 연산 · 임계 구역

채널 방식이 속하는 동시성 모델

동시성 · CSP · 메시지 전달 · 액터 모델 · 생산자-소비자 패턴

채널과 같은 방식으로 기다리는 자바 큐

BlockingQueue · SynchronousQueue · 큐 · 작업 큐 · 버퍼

Go 채널과 함께 쓰는 문법과 도구

select 문 · WaitGroup · context 패키지 · 타임아웃 · 취소

Go 채널에서 자주 나는 오류

데드락 · 고루틴 누수 · 경쟁 상태 · panic

gRPC 채널 위에서 호출을 보내는 부품

gRPC · 스텁 · RPC · 클라이언트 · 서버 · 프로토콜 버퍼

gRPC 채널이 뒤에서 맡는 연결 기능

HTTP/2 · 다중화 · 이름 해석 · DNS · 부하 분산 · TCP · TLS · 커넥션 풀

채널과 이름이 겹치는 다른 개념

자바 NIO · 소켓 · Netty · 발행-구독 · Redis · 채널 용량 · 채널 바인딩

다른 이름: channel · chan