1.1 채널 개요
채널은 Go 언어에서 매우 중요한 기능으로, 서로 다른 고루틴 간의 통신에 사용됩니다. Go 언어의 동시성 모델은 CSP(Communicating Sequential Processes)이며, 여기서 채널은 메시지 전달의 역할을 합니다. 채널을 사용하면 복잡한 메모리 공유를 피할 수 있어 동시성 프로그램 설계가 더 간단하고 안전해집니다.
1.2 채널 생성과 종료
Go 언어에서 채널은 make
함수를 사용하여 생성되며, 채널의 유형과 버퍼 크기를 지정할 수 있습니다. 버퍼 크기는 선택 사항이며, 크기를 지정하지 않으면 버퍼가 없는 채널이 생성됩니다.
ch := make(chan int) // int 유형의 버퍼가 없는 채널 생성
chBuffered := make(chan int, 10) // int 유형에 대한 용량이 10인 버퍼 채널 생성
채널을 올바르게 닫는 것도 중요합니다. 데이터를 더 이상 보낼 필요가 없을 때 채널을 닫아 데드락을 피하거나 다른 고루틴이 데이터를 무한정으로 기다리는 상황을 방지해야 합니다.
close(ch) // 채널 닫기
1.3 데이터 송수신
채널에서 데이터를 보내고 받는 것은 <-
기호를 사용하여 간단합니다. 보내는 작업은 왼쪽에, 받는 작업은 오른쪽에 위치합니다.
ch <- 3 // 채널로 데이터 보내기
value := <- ch // 채널에서 데이터 받기
그러나 보내는 작업은 데이터를 받을 때까지 블록되며, 받는 작업도 읽을 데이터가 있을 때까지 블록됩니다.
fmt.Println(<-ch) // ch로부터 데이터가 전송될 때까지 블록됩니다.
2. 채널의 고급 사용
2.1 채널의 용량과 버퍼링
채널은 버퍼가 있는지 없는지에 따라 다릅니다. 버퍼가 없는 채널은 수신자가 메시지를 받을 준비가 될 때까지 송신자를 블록합니다. 버퍼가 없는 채널은 보통 두 개의 고루틴을 특정 시점에서 동기화하기 위해 사용됩니다.
ch := make(chan int) // 버퍼가 없는 채널 생성
go func() {
ch <- 1 // 받을 고루틴이 없을 경우 블록됩니다.
}()
버퍼가 있는 채널은 용량 제한이 있으며, 채널로 데이터를 보낼 때 버퍼가 가득 차 있을 때만 블록됩니다. 마찬가지로 비어 있는 버퍼에서 받으려고 하면 블록됩니다. 버퍼가 있는 채널은 일반적으로 고트래픽 및 비동기 통신 시나리오를 처리하기 위해 사용되며, 대기로 인한 직접적인 성능 손실을 줄입니다.
ch := make(chan int, 10) // 용량이 10인 버퍼 채널 생성
go func() {
for i := 0; i < 10; i++ {
ch <- i // 채널이 이미 가득 차 있지 않은 경우 블록되지 않습니다.
}
close(ch) // 송신이 완료된 후 채널 닫기
채널 유형의 선택은 통신의 성격에 따라 결정됩니다: 동기화 보장이 필요한지, 버퍼링이 필요한지, 성능 요구 사항 등이 있습니다.
2.2 select
문 사용
여러 채널 사이에서 선택할 때 select
문은 매우 유용합니다. switch 문과 유사하지만 각 케이스는 채널 작업을 포함합니다. 채널에서 데이터 흐름을 감시하며, 동시에 여러 채널이 동시에 준비되면 select
는 무작위로 실행할 채널을 선택합니다.
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch1 <- i
}
}()
go func() {
for i := 0; i < 5; i++ {
ch2 <- i * 10
}
}()
for i := 0; i < 5; i++ {
select {
case v1 := <-ch1:
fmt.Println("ch1으로부터 받음:", v1)
case v2 := <-ch2:
fmt.Println("ch2으로부터 받음:", v2)
}
}
select
를 사용하면 여러 채널에서 동시에 데이터를 수신하거나 특정 조건에 따라 데이터를 보낼 수 있는 복잡한 통신 시나리오를 처리할 수 있습니다.
2.3 채널을 위한 Range 루프
range
키워드를 활용하면 채널이 닫힐 때까지 데이터를 계속해서 받아옵니다. 특히 생산자-소비자 모델에서 알려지지 않은 양의 데이터를 처리할 때 매우 유용합니다.
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // 채널을 닫는 것을 잊지 마세요
}()
for n := range ch {
fmt.Println("받은 값:", n)
}
채널이 닫히고 남은 데이터가 없을 때 루프가 종료됩니다. 채널을 닫는 것을 잊는다면 range
는 고루틴 누출을 발생시키고 프로그램은 데이터가 도착할 때까지 무기한 대기할 수 있습니다.
3 병행성에서의 복잡한 상황 다루기
3.1 Context의 역할
고 언어의 병행 프로그래밍에서 context
패키지는 중요한 역할을 합니다. Context는 단일 요청 도메인을 다루는 여러 고루틴 간의 데이터, 취소 신호, 기한 등을 간소화하는 데 사용됩니다.
만약 웹 서비스가 데이터베이스를 쿼리하고 데이터에 대한 연산을 수행하는 경우, 이를 여러 고루틴을 통해 처리해야 한다고 가정해봅시다. 사용자가 요청을 갑자기 취소하거나 서비스가 특정 시간 내에 요청을 완료해야 할 경우 모든 실행 중인 고루틴을 취소해야 하는 메커니즘이 필요합니다.
여기서 해당 요구 사항을 충족시키기 위해 context
를 사용합니다.
package main
import (
"context"
"fmt"
"time"
)
func operation1(ctx context.Context) {
time.Sleep(1 * time.Second)
select {
case <-ctx.Done():
fmt.Println("operation1이 취소되었습니다")
return
default:
fmt.Println("operation1이 완료되었습니다")
}
}
func operation2(ctx context.Context) {
time.Sleep(2 * time.Second)
select {
case <-ctx.Done():
fmt.Println("operation2가 취소되었습니다")
return
default:
fmt.Println("operation2가 완료되었습니다")
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go operation1(ctx)
go operation2(ctx)
<-ctx.Done()
fmt.Println("main: context가 완료되었습니다")
}
위 코드에서 context.WithTimeout
을 사용하여 특정 시간 후 자동으로 취소되는 Context를 생성합니다. operation1
및 operation2
함수는 ctx.Done()
을 감시하는 select
블록을 갖추어 있어서 Context가 취소 신호를 보낼 때 즉시 멈출 수 있습니다.
3.2 채널을 이용한 오류 처리
병행 프로그래밍에서 오류 처리는 중요한 요소입니다. Go에서는 고루틴과 함께 채널을 사용하여 오류를 비동기적으로 처리할 수 있습니다.
다음 코드 예제는 고루틴에서 오류를 전달하고 메인 고루틴에서 이를 처리하는 방법을 보여줍니다:
package main
import (
"errors"
"fmt"
"time"
)
func performTask(id int, errCh chan<- error) {
// 무작위로 성공하거나 실패할 수 있는 작업 시뮬레이션
if id%2 == 0 {
time.Sleep(2 * time.Second)
errCh <- errors.New("작업 실패")
} else {
fmt.Printf("작업 %d가 성공적으로 완료되었습니다\n", id)
errCh <- nil
}
}
func main() {
tasks := 5
errCh := make(chan error, tasks)
for i := 0; i < tasks; i++ {
go performTask(i, errCh)
}
for i := 0; i < tasks; i++ {
err := <-errCh
if err != nil {
fmt.Printf("오류를 받음: %s\n", err)
}
}
fmt.Println("모든 작업 처리 완료")
}
이 예제에서는 성공하거나 실패할 수 있는 작업을 시뮬레이션하는 performTask
함수를 정의합니다. 오류는 매개변수로 전달되는 errCh
채널을 통해 메인 고루틴으로 전송되며, 메인 고루틴은 모든 작업이 완료될 때까지 오류 메시지를 수신합니다. 버퍼가 있는 채널을 사용함으로써 고루틴이 수신되지 않은 오류로 인해 블록되지 않도록 보장합니다.
이러한 기술들은 병행 프로그래밍에서 복잡한 상황을 다루는 강력한 도구입니다. 적절하게 사용함으로써 코드를 보다 견고하고 이해하기 쉽고 유지보수하기 좋게 만들 수 있습니다.