🔥 버퍼가 있는 채널

633자
8분

Go 언어에서는 채널에 버퍼를 설정할 수 있습니다. 버퍼가 있는 채널을 생성하려면 make 함수의 두 번째 인자로 버퍼 크기를 지정하면 됩니다.

ch := make(chan int, 100)
 
go

위 코드는 정수형 값을 저장할 수 있는 버퍼 크기가 100인 채널을 생성합니다.

버퍼가 있는 채널은 버퍼가 가득 찰 때까지는 값을 보내는 쪽에서 블로킹이 발생하지 않습니다. 반대로 버퍼가 비어있을 때는 값을 받는 쪽에서 블로킹이 발생합니다.

다음은 버퍼가 있는 채널을 사용하는 예제 코드입니다.

package main
 
import "fmt"
 
func main() {
	ch := make(chan int, 2) // 버퍼 크기가 2인 정수형 채널 생성
	ch <- 1                 // 채널에 1을 보냄
	ch <- 2                 // 채널에 2를 보냄
	fmt.Println(<-ch)       // 채널에서 값을 받아서 출력
	fmt.Println(<-ch)       // 채널에서 값을 받아서 출력
}
 
go

위 코드를 실행하면 다음과 같은 출력 결과를 얻을 수 있습니다.

1
2
text

코드를 단계별로 살펴보겠습니다.

  1. ch := make(chan int, 2)
    • 버퍼 크기가 2인 정수형 채널 ch를 생성합니다.
  2. ch <- 1
    • 채널 ch에 정수 1을 보냅니다.
    • 버퍼에 여유 공간이 있으므로 블로킹 없이 바로 값이 전달됩니다.
  3. ch <- 2
    • 채널 ch에 정수 2를 보냅니다.
    • 버퍼에 여유 공간이 있으므로 블로킹 없이 바로 값이 전달됩니다.
  4. fmt.Println(<-ch)
    • 채널 ch에서 값을 받아서 출력합니다.
    • 버퍼에 값이 있으므로 블로킹 없이 바로 값을 받을 수 있습니다.
  5. fmt.Println(<-ch)
    • 채널 ch에서 값을 받아서 출력합니다.
    • 버퍼에 값이 있으므로 블로킹 없이 바로 값을 받을 수 있습니다.

만약 버퍼 크기를 초과하여 값을 보내려고 하면 어떻게 될까요? 다음 예제 코드를 살펴봅시다.

package main
 
import "fmt"
 
func main() {
	ch := make(chan int, 2) // 버퍼 크기가 2인 정수형 채널 생성
	ch <- 1                 // 채널에 1을 보냄
	ch <- 2                 // 채널에 2를 보냄
	ch <- 3                 // 채널에 3을 보냄 (버퍼 크기 초과)
	fmt.Println(<-ch)       // 채널에서 값을 받아서 출력
	fmt.Println(<-ch)       // 채널에서 값을 받아서 출력
}
 
go

위 코드를 실행하면 다음과 같은 런타임 에러가 발생합니다.

fatal error: all goroutines are asleep - deadlock!
text

버퍼 크기가 2인 채널에 이미 두 개의 값을 보냈는데, 세 번째 값을 보내려고 시도했기 때문에 데드락(deadlock)이 발생한 것입니다.

이러한 데드락을 해결하는 방법으로는 다음과 같은 것들이 있습니다.

  1. 버퍼 크기 늘리기
    • 버퍼 크기를 늘려서 더 많은 값을 보낼 수 있도록 할 수 있습니다.
    • 예를 들어, 버퍼 크기를 3으로 늘리면 아래와 같이 코드를 수정할 수 있습니다.
package main
 
import "fmt"
 
func main() {
	ch := make(chan int, 3) // 버퍼 크기를 3으로 늘림
	ch <- 1
	ch <- 2
	ch <- 3
	fmt.Println(<-ch)
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}
 
go
  1. select 문 사용하기
    • select 문을 사용하여 값을 보내거나 받을 수 있는 채널을 선택할 수 있습니다.
    • 값을 보내는 부분에서 블로킹이 발생하면 다른 케이스로 분기하여 처리할 수 있습니다.
package main
 
import "fmt"
 
func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
 
	select {
	case ch <- 3:
		fmt.Println("값을 보냈습니다.")
	default:
		fmt.Println("버퍼가 가득 찼습니다.")
	}
 
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}
 
go

위의 예제에서는 버퍼 크기가 2인 채널에 이미 두 개의 값이 있는 상태에서 세 번째 값을 보내려고 시도합니다. 이때 select 문을 사용하여 값을 보내는 케이스와 기본 케이스를 처리합니다. 버퍼가 가득 차면 기본 케이스로 분기하여 "버퍼가 가득 찼습니다."라는 메시지를 출력합니다.

버퍼가 있는 채널은 값을 보내는 쪽과 받는 쪽 사이의 속도 차이를 어느 정도 완화해주는 역할을 합니다. 값을 보내는 쪽이 일시적으로 더 빠르게 값을 보내더라도 버퍼에 값을 저장해두고, 값을 받는 쪽은 필요할 때 버퍼에서 값을 꺼내올 수 있습니다.

하지만 버퍼 크기를 무한정 크게 설정하는 것은 바람직하지 않습니다. 버퍼 크기가 너무 크면 불필요한 메모리 낭비가 발생할 수 있고, 프로그램의 응답성이 떨어질 수 있습니다.

따라서 버퍼 크기는 상황에 맞게 적절히 설정해야 합니다. 값을 보내는 쪽과 받는 쪽의 속도 차이, 처리할 데이터의 양, 시스템 리소스 등을 고려하여 최적의 버퍼 크기를 결정해야 합니다.

이상으로 Go 언어에서 버퍼가 있는 채널에 대해 알아보았습니다. 버퍼가 있는 채널은 값을 보내는 쪽과 받는 쪽 사이의 속도 차이를 완화해주고, 일시적인 부하를 견딜 수 있게 해줍니다. 하지만 버퍼 크기를 적절히 설정하고, 데드락을 해결하기 위한 방법을 사용하는 것이 중요합니다.