Menu
Coddy logo textTech

Buffered vs Unbuffered 채널

Coddy GO 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 107개 중 61번째.

지금까지 우리가 사용한 채널은 버퍼링되지 않았습니다(unbuffered). 즉, 값을 보유할 공간이 없습니다. 송신 작업은 다른 고루틴이 수신할 때까지 차단되며, 그 반대의 경우도 마찬가지입니다. 버퍼링된 채널(Buffered channels)은 내부 저장 공간을 추가하여, 즉각적인 수신자가 없어도 송신이 완료될 수 있도록 합니다.

make에 용량을 전달하여 버퍼드 채널을 생성하세요:

// 버퍼가 없는 채널 - 용량 0
unbuffered := make(chan int)

// 버퍼가 있는 채널 - 용량 3
buffered := make(chan int, 3)

버퍼가 있는 채널(buffered channel)에서는 버퍼가 가득 찼을 때만 송신이 차단(block)되고, 버퍼가 비어 있을 때만 수신이 차단됩니다:

ch := make(chan string, 2)

ch <- "first"   // 차단되지 않음 - 버퍼에 공간이 있음
ch <- "second"  // 차단되지 않음 - 버퍼에 공간이 있음
// ch <- "third"는 차단됨 - 버퍼가 가득 참

fmt.Println(<-ch)  // "first"
fmt.Println(<-ch)  // "second"

len(ch)를 사용하여 현재 버퍼에 있는 항목 수를 확인하고, 전체 용량을 확인하려면 cap(ch)를 사용하세요.

각각을 사용하는 시기: 버퍼링되지 않은 채널(Unbuffered channels)은 엄격한 동기화를 제공합니다. 즉, 송신자는 수신자가 값을 받았음을 알 수 있습니다. 버퍼링된 채널(Buffered channels)은 송신자와 수신자의 타이밍을 분리하므로, 생산자와 소비자가 서로 다른 속도로 작업할 때 유용합니다. 하지만 버퍼링된 채널은 동기화 버그를 숨길 수 있으므로, 버퍼링을 사용해야 할 구체적인 이유가 없다면 버퍼링되지 않은 채널을 사용하는 것이 좋습니다.

challenge icon

챌린지

쉬움

버퍼드 채널(buffered channels)과 언버퍼드 채널(unbuffered channels)의 차이점을 보여주는 배치 프로세서를 만들어 보겠습니다. 생산자(producer)가 프로세서로 아이템을 보내는 시스템을 만들게 되며, 버퍼드 채널을 사용하여 타이밍을 분리하고 생산자가 미리 작업을 수행할 수 있도록 합니다.

코드는 두 개의 파일로 구성됩니다:

  • processor.go: 채널을 사용하여 배치 처리 로직을 정의합니다.

    ID (int)와 Value (string) 필드를 가진 Item 구조체를 생성합니다.

    두 개의 함수를 구현합니다:

    • Producer(items []Item, out chan Item) - 각 아이템을 출력 채널로 보냅니다. 각 아이템을 보낸 후, Produced item [ID] 형식으로 출력합니다. 모든 아이템을 보낸 후, len(out)을 사용하여 버퍼에서 대기 중인 현재 아이템 수를 Buffer has [count] items 형식으로 출력합니다. 그런 다음 채널을 닫습니다.
    • Consumer(in chan Item) []string - 입력 채널로부터 아이템을 수신하여 포맷된 문자열 슬라이스에 수집합니다. 수신된 각 아이템에 대해 문자열은 Consumed: [ID] - [Value] 형식이어야 합니다. 채널이 닫히면 슬라이스를 반환합니다.
  • main.go: 버퍼드 채널을 설정하고 생산자와 소비자를 조정합니다.

    버퍼 용량, 아이템 수, 그리고 각 아이템의 ID와 값을 차례로 읽습니다. 지정된 용량으로 버퍼드 채널을 생성합니다. 먼저 Producer를 실행하여(고루틴이 아닌 일반 함수 호출로) 버퍼를 채운 다음, Consumer를 실행하여 모든 아이템을 처리합니다. 소비된 각 결과를 별도의 줄에 출력합니다.

다음과 같은 입력이 제공됩니다:

  • 1행: 버퍼 용량 (정수)
  • 2행: 아이템 수 (정수)
  • 다음 행들: 각 아이템에 대해 두 줄씩 - 아이템 ID (정수), 그 다음 값 (문자열)

예를 들어, 다음과 같이 입력되면:

3
3
1
apple
2
banana
3
cherry

출력은 다음과 같아야 합니다:

Produced item 1
Produced item 2
Produced item 3
Buffer has 3 items
Consumed: 1 - apple
Consumed: 2 - banana
Consumed: 3 - cherry

버퍼 용량이 3인 경우, 생산자가 차단(blocking) 없이 3개의 아이템을 모두 보낼 수 있으며, 소비자가 시작하기 전에 버퍼에 3개의 아이템이 대기 중임을 알 수 있습니다. 만약 버퍼가 아이템 수보다 작다면, 생산자는 공간이 생길 때까지 대기하며 차단될 것입니다.

치트 시트

Go의 채널은 버퍼가 없는(용량 0) 또는 버퍼가 있는(내부 저장 공간이 있는) 상태일 수 있습니다.

make를 사용하여 채널을 생성합니다:

// 버퍼 없음 - 용량 0
unbuffered := make(chan int)

// 버퍼 있음 - 용량 3
buffered := make(chan int, 3)

버퍼가 있는 채널의 동작:

  • 전송은 버퍼가 가득 찼을 때만 차단(block)됩니다
  • 수신은 버퍼가 비어 있을 때만 차단(block)됩니다
ch := make(chan string, 2)

ch <- "first"   // 차단되지 않음 - 버퍼에 공간이 있음
ch <- "second"  // 차단되지 않음 - 버퍼에 공간이 있음
// ch <- "third"는 차단됨 - 버퍼가 가득 참

fmt.Println(<-ch)  // "first"
fmt.Println(<-ch)  // "second"

버퍼 상태 확인:

  • len(ch) - 현재 버퍼에 있는 아이템 수
  • cap(ch) - 전체 버퍼 용량

사용 시기:

  • 버퍼 없음: 엄격한 동기화 — 송신자가 수신자가 값을 받았음을 알 수 있음
  • 버퍼 있음: 송신자와 수신자가 서로 다른 속도로 작동할 때 타이밍을 분리(Decouple)함

직접 해보기

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// 버퍼 용량 읽기
	line, _ := reader.ReadString('\n')
	bufferCapacity, _ := strconv.Atoi(strings.TrimSpace(line))

	// 아이템 개수 읽기
	line, _ = reader.ReadString('\n')
	numItems, _ := strconv.Atoi(strings.TrimSpace(line))

	// 각 아이템의 ID와 값 읽기
	items := make([]Item, numItems)
	for i := 0; i < numItems; i++ {
		line, _ = reader.ReadString('\n')
		id, _ := strconv.Atoi(strings.TrimSpace(line))
		line, _ = reader.ReadString('\n')
		value := strings.TrimSpace(line)
		items[i] = Item{ID: id, Value: value}
	}

	// TODO: 지정된 용량으로 버퍼링된 채널 생성

	// TODO: 버퍼를 채우기 위해 생산자(Producer) 실행 (고루틴이 아닌 방식으로)

	// TODO: 모든 아이템을 처리하기 위해 소비자(Consumer) 실행

	// TODO: 소비된 각 결과를 별도의 줄에 출력
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨