
Go 언어에서 가장 대표적인 특징은 바로 고루틴(Goroutine) 이라고 할 수 있습니다.
이 고루틴과 고루틴을 안전하게 연결하는 혈관같은 존재가 채널(Channel)인데요.
이 채널에 대해서 한 번 정리해보겠습니다.
1. 채널은 왜 만들어졌나
전통적인 멀티스레드 환경에서는 여러 스레드가 하나의 메모리(공유 자원)에 접근 할 때 동시성 문제를 해결하기 위해서 뮤텍스나 세마포어 기법을 활용했습니다.
하지만 코드는 복잡해지고, 잠금을 해제하는 것을 잘 제어하지 못하면 데드락이 발생하여 시스템이 멈추는 위험이 컸습니다.
그래서 Go에서는 이러한 문제를 해결하기 위해서 새로운 패러다임을 제시했습니다
"공유 메모리로 통신하지 말고, 통신을 통해 메모리를 공유하자"
그래서 채널이라는 것은 위 패러다임을 기준으로 만들어졌습니다.
데이터를 주고 받는 통로, 데이터를 주고 받는 그 행위 자체가 동기화를 포함함으로써,
별도의 잠금 장치 없이도 안전한 병행 프로그래밍이 가능해졌습니다.
2. 방식
채널은 크게 두 가지 방식으로 나누어지게 됩니다.
- 동기 (Unbuffered Channel)
데이터를 담아둘 공간이 없는 채널로, 송신자와 수신자가 동시에 준비가 되어야만 데이터가 이동하게 됩니다.
예를 들면 당근 직거래로 생각할 수 있습니다. 둘 중 한명이라도 약속 장소에 나오지 않으면 상대방이 올 때까지 기다려야 하죠. - 비동기 (Buffered Channel)
내부에 데이터를 보관할 수 있는 저장소(버퍼)가 있는 채널로, 버퍼가 가득 차지 않았다면 송신자는 수신자를 기다리지 않고,
데이터를 밀어넣은 뒤에 기다리지 않고 자신에게 주어진 다음일을 진행합니다.
3. 예제 코드
위 두 가지 방식을 함께 예제 코드로 보겠습니다.
package main
import (
"fmt"
)
func main() {
// Unbuffered 방식
ch := make(chan int)
go func() {
defer close(ch)
for i := 1; i <= 5; i++ {
ch <- i // 데이터를 꺼낼때 까지 대기
}
}()
// 수신
sum := 0
for v := range ch {
sum += v
}
fmt.Printf("sum=%d\n", sum)
// Buffered 방식
buf := make(chan string, 2)
buf <- "a"
buf <- "b"
close(buf)
for s := range buf {
fmt.Println("buf:", s)
}
}
- 핵심 규칙
- 송신자 : 수신 측에서 채널을 닫으면 송신 측에서 Panic이 발생 할 수 있습니다.
데이터를 보내는 쪽에서 더 보낼 것이 없다면 채널을 닫는 것이 원칙 - 닫힌 채널 : 닫힌 채널에 데이터를 밀어 넣으려고 한다면 Painc이 발생하지만,
채널 버퍼에 데이터가 남아 있을 경우, 남아 있는 데이터를 꺼내 읽는 것은 가능합니다.
- 송신자 : 수신 측에서 채널을 닫으면 송신 측에서 Panic이 발생 할 수 있습니다.
4. 전체 코드
https://github.com/reochoi109/go-handbook/blob/main/channel/basic/main.go
'프로그래밍 > golang' 카테고리의 다른 글
| [Golang] flag로 서브커맨드 만들기 (0) | 2026.03.21 |
|---|---|
| [Golang] Context란? (0) | 2026.03.21 |
| [Golang] errgroup 이란? (0) | 2026.03.18 |
| [Golang] Close() 에러를 무시하면 안 되는 이유 (0) | 2026.03.18 |
| [Golang] 에러 분류를 통한 Retry 구현 (0) | 2026.03.18 |