본문 바로가기

프로그래밍/golang

[Golang] 채널(Channel)이란?

 

Go 언어에서 가장 대표적인 특징은 바로 고루틴(Goroutine) 이라고 할 수 있습니다.
이 고루틴과 고루틴을 안전하게 연결하는 혈관같은 존재가 채널(Channel)인데요.
이 채널에 대해서 한 번 정리해보겠습니다.


1. 채널은 왜 만들어졌나

전통적인 멀티스레드 환경에서는 여러 스레드가 하나의 메모리(공유 자원)에 접근 할 때 동시성 문제를 해결하기 위해서 뮤텍스나 세마포어 기법을 활용했습니다.

하지만 코드는 복잡해지고, 잠금을 해제하는 것을 잘 제어하지 못하면 데드락이 발생하여 시스템이 멈추는 위험이 컸습니다.
그래서 Go에서는 이러한 문제를 해결하기 위해서 새로운 패러다임을 제시했습니다

 

"공유 메모리로 통신하지 말고, 통신을 통해 메모리를 공유하자"

그래서 채널이라는 것은 위 패러다임을 기준으로 만들어졌습니다.
데이터를 주고 받는 통로, 데이터를 주고 받는 그 행위 자체가 동기화를 포함함으로써,
별도의 잠금 장치 없이도 안전한 병행 프로그래밍이 가능해졌습니다.

 

2. 방식

채널은 크게 두 가지 방식으로 나누어지게 됩니다.

  1. 동기 (Unbuffered Channel)
    데이터를 담아둘 공간이 없는 채널로, 송신자와 수신자가 동시에 준비가 되어야만 데이터가 이동하게 됩니다.
    예를 들면 당근 직거래로 생각할 수 있습니다. 둘 중 한명이라도 약속 장소에 나오지 않으면 상대방이 올 때까지 기다려야 하죠.

  2. 비동기 (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이 발생하지만, 
      채널 버퍼에 데이터가 남아 있을 경우, 남아 있는 데이터를 꺼내 읽는 것은 가능합니다.

4. 전체 코드

https://github.com/reochoi109/go-handbook/blob/main/channel/basic/main.go