🔥 채널: 값을 주고받는 통로
Go에서 채널은 값을 주고받을 수 있는 타입이 지정된 통로라 할 수 있어요. 채널 연산자 <-
를 사용하면 채널을 통해 값을 보내거나 받을 수 있답니다.
ch <- v // v를 채널 ch로 보냅니다. v := <-ch // ch에서 값을 받아 // v에 할당합니다.
go
(데이터는 화살표 방향으로 흐릅니다.)
맵이나 슬라이스와 마찬가지로, 채널도 사용하기 전에 생성해야 해요:
ch := make(chan int)
go
기본적으로 값을 보내거나 받을 때는 상대편이 준비될 때까지 블록됩니다. 이를 통해 고루틴들이 명시적인 잠금이나 조건 변수 없이도 동기화할 수 있어요.
아래 예제 코드는 슬라이스의 숫자들을 합산하되, 두 개의 고루틴 사이에 작업을 분배합니다. 두 고루틴이 모두 계산을 완료하면, 최종 결과를 계산하죠.
package main import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v // 슬라이스의 각 요소를 더합니다. } c <- sum // 합계를 c로 보냅니다. } func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) // 슬라이스 앞부분을 더하는 고루틴 go sum(s[len(s)/2:], c) // 슬라이스 뒷부분을 더하는 고루틴 x, y := <-c, <-c // c에서 결과를 받습니다. fmt.Println(x, y, x+y) // 부분합과 전체합을 출력합니다. }
go
위 코드를 단계별로 살펴보면:
sum
함수는 정수 슬라이스s
와 정수형 채널c
를 매개변수로 받아요.- 슬라이스의 모든 요소를 순회하며
sum
변수에 더합니다. - 계산된 부분합을 채널
c
로 보냅니다. main
함수에서는 정수 슬라이스s
를 준비하고,- 정수형 채널
c
를 생성합니다. go
키워드를 사용해 두 개의 고루틴을 시작하는데, 하나는 슬라이스의 앞부분을, 다른 하나는 뒷부분을 더하도록sum
함수를 호출해요.- 각 고루틴에서 계산한 부분합을 채널
c
를 통해 받아x
와y
에 할당합니다. - 마지막으로
x
,y
, 그리고 이들의 합을 출력합니다.
위 코드 중 go sum(s[:len(s)/2], c)
을 보면 채널 c
를 사용하는 방식이 콜백 함수 패턴과 유사하다고 볼 수 있어요.
콜백 함수 패턴에서는 어떤 함수를 호출할 때 그 함수가 완료된 후 실행될 함수(콜백)를 같이 전달하죠. 호출된 함수는 작업을 완료한 후 전달받은 콜백 함수를 실행함으로써 호출자에게 결과를 알려주는 식입니다.
Go 채널을 사용하면 이와 비슷한 패턴을 구현할 수 있어요. 고루틴을 시작할 때 채널을 함께 전달하고, 고루틴은 작업을 완료한 후 그 채널을 통해 결과를 보내는 거죠. 호출자는 채널에서 결과를 읽음으로써 고루틴의 완료를 알 수 있습니다.
예를 들어, go sum(s[:len(s)/2], c)
에서는 sum
함수를 고루틴으로 실행하면서 채널 c
를 전달하고 있어요. sum
함수는 계산을 완료한 후 그 결과를 채널 c
를 통해 보내고, 호출자는 x, y := <-c, <-c
에서 그 결과를 받아옵니다.
이렇게 채널을 사용하면 고루틴 간의 통신과 동기화를 간편하게 처리할 수 있어요. 콜백 함수를 사용할 때처럼 비동기적인 실행 흐름을 관리할 수 있으면서도, 고루틴 간의 데이터 전달이 명확해지는 장점이 있죠.
물론 콜백 함수와 채널이 완전히 같은 개념은 아니에요. 콜백은 함수를 값으로 다루는 데 초점이 맞춰져 있고, 채널은 통신과 동기화에 더 특화되어 있습니다. 하지만 비동기적인 실행 흐름을 관리한다는 점에서는 유사한 면이 있다고 할 수 있겠죠?
이처럼 채널을 사용하면 고루틴 간에 값을 안전하게 교환할 수 있답니다. 고루틴들이 서로 기다리며 동기화하는 것도 간단히 처리할 수 있죠.
Swift의 Actor를 흉내내는 채널 활용 예제
Go의 채널을 사용하면 Swift의 Actor와 유사한 동작을 구현할 수 있어요. 아래는 은행 계좌를 모델링하는 예제인데, 각 계좌를 고루틴으로 표현하고 채널을 통해 메시지를 주고받도록 해볼게요.
package main import ( "fmt" "sync" ) type BankAccount struct { balance int ch chan func() } func NewBankAccount(initialBalance int) *BankAccount { account := &BankAccount{ balance: initialBalance, ch: make(chan func()), } go func() { for message := range account.ch { message() // 받은 메시지(함수)를 실행합니다. } }() return account } func (a *BankAccount) Deposit(amount int) { a.ch <- func() { a.balance += amount // 잔액에 입금액을 더합니다. } } func (a *BankAccount) Withdraw(amount int) bool { result := make(chan bool) a.ch <- func() { if a.balance >= amount { a.balance -= amount // 잔액에서 출금액을 뺍니다. result <- true } else { result <- false } } return <-result } func (a *BankAccount) Balance() int { result := make(chan int) a.ch <- func() { result <- a.balance // 현재 잔액을 채널로 보냅니다. } return <-result } func main() { var wg sync.WaitGroup account := NewBankAccount(1000) // 여러 고루틴에서 계좌에 접근합니다. for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() account.Deposit(100) if account.Withdraw(50) { fmt.Println("출금 성공!") } else { fmt.Println("잔액 부족!") } }() } wg.Wait() fmt.Printf("최종 잔액: %d", account.Balance()) }
go
위 코드에서 BankAccount
구조체는 Swift의 Actor와 유사한 역할을 합니다. 각 계좌는 고유의 고루틴에서 실행되며, 상태를 변경하는 모든 연산은 채널을 통해 전달되는 메시지(함수)를 통해서만 가능해요.
특히 BankAccount
구조체의 ch
필드는 chan func()
타입인데요, 이는 "함수를 전달할 수 있는 채널"을 의미합니다. 즉, ch
채널을 통해 함수(메시지)를 전달하고, 계좌의 고루틴에서 이 함수를 실행함으로써 상태를 변경하는 거죠. 이렇게 함수를 메시지로 사용하면 액터가 수신한 메시지에 따라 내부 상태를 변경하는 것과 유사한 동작을 구현할 수 있습니다.
Deposit
, Withdraw
, Balance
메서드는 모두 채널을 통해 계좌의 고루틴에 메시지를 전송하고, 필요한 경우 결과를 다른 채널을 통해 받아옵니다.
main
함수에서는 여러 고루틴이 동시에 계좌에 접근하는 상황을 시뮬레이션합니다. 각 고루틴은 입금과 출금을 시도하고, 최종적으로 모든 고루틴이 완료된 후 계좌의 최종 잔액을 출력해요.
이렇게 Go의 채널과 고루틴을 활용하면 Swift의 Actor와 유사한 동시성 패턴을 구현할 수 있답니다. 물론 Go와 Swift는 언어적 특성이 다르므로 완전히 동일하지는 않지만, 메시지 전달을 통해 상태를 관리하는 기본 아이디어는 공유하고 있어요. 이런 방식으로 Go에서도 액터 모델의 장점을 활용할 수 있게 되는 거죠!
고루틴과 채널로 자바스크립트의 Promise와 async/await 흉내내기
Go의 고루틴과 채널을 사용하면 자바스크립트의 Promise와 async/await와 유사한 동작을 구현할 수 있어요. 아래는 그 예시 코드입니다.
package main import ( "fmt" "math/rand" "time" ) // Result는 비동기 작업의 결과를 나타냅니다. type Result struct { Value int Err error } // Promise는 비동기 작업을 나타냅니다. type Promise struct { ch chan Result } // NewPromise는 새로운 Promise를 생성합니다. func NewPromise(fn func() (int, error)) *Promise { p := &Promise{ ch: make(chan Result, 1), } go func() { value, err := fn() p.ch <- Result{Value: value, Err: err} }() return p } // Then은 Promise의 결과를 처리하는 메서드입니다. func (p *Promise) Then(fn func(int) (int, error)) *Promise { return NewPromise(func() (int, error) { result := <-p.ch if result.Err != nil { return 0, result.Err } return fn(result.Value) }) } // Await은 Promise의 결과를 기다리는 함수입니다. func Await(p *Promise) (int, error) { result := <-p.ch return result.Value, result.Err } // 비동기 작업을 모사하는 함수입니다. func asyncTask() (int, error) { time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) if rand.Intn(2) == 0 { return 0, fmt.Errorf("async task failed") } return rand.Intn(100), nil } func main() { start := time.Now() p := NewPromise(asyncTask).Then(func(value int) (int, error) { fmt.Printf("첫 번째 비동기 작업 결과: %d\n", value) return asyncTask() }).Then(func(value int) (int, error) { fmt.Printf("두 번째 비동기 작업 결과: %d\n", value) return value, nil }) result, err := Await(p) if err != nil { fmt.Printf("에러 발생: %v\n", err) } else { fmt.Printf("최종 결과: %d\n", result) } elapsed := time.Since(start) fmt.Printf("소요 시간: %s\n", elapsed) }
go
이 코드에서는 Promise
구조체와 NewPromise
, Then
, Await
함수를 통해 자바스크립트의 Promise와 유사한 기능을 구현하고 있어요.
NewPromise
함수는 비동기 작업을 수행하는 함수를 받아 새로운Promise
를 생성합니다. 이 함수는 고루틴 내에서 실행되며, 작업의 결과는 채널을 통해 전달됩니다.Then
메서드는Promise
의 결과를 받아 새로운 비동기 작업을 수행하고, 그 결과로 새로운Promise
를 반환합니다. 이를 통해 비동기 작업을 연쇄적으로 수행할 수 있죠.Await
함수는Promise
의 결과를 기다리고, 결과와 에러를 반환합니다. 이는 자바스크립트의await
키워드와 유사한 역할을 합니다.
main
함수에서는 asyncTask
함수를 사용하여 비동기 작업을 흉내 냅니다. 이 함수는 랜덤한 지연 시간 후에 결과 또는 에러를 반환합니다.
NewPromise
와 Then
을 사용하여 비동기 작업을 연쇄적으로 수행하고, 마지막에 Await
함수를 호출하여 최종 결과를 얻습니다.
첫 번째 비동기 작업 결과: 96 두 번째 비동기 작업 결과: 65 최종 결과: 65 소요 시간: 709.187291ms
text
에러가 발생한 경우에는 에러를 처리하고, 그렇지 않으면 최종 결과를 출력하죠.
첫 번째 비동기 작업 결과: 71 에러 발생: async task failed 소요 시간: 936.864007ms
text
이렇게 Go의 고루틴과 채널을 활용하면 자바스크립트의 Promise와 async/await와 유사한 비동기 프로그래밍 패턴을 구현할 수 있답니다. 물론 Go의 동시성 모델과 자바스크립트의 비동기 모델은 차이가 있지만, 비동기 작업을 다루는 기본적인 아이디어는 공유하고 있어요.
이처럼 Go의 동시성 프리미티브는 매우 강력하고 유연해서, 다양한 동시성/비동기 패턴을 구현할 수 있답니다. 채널과 고루틴을 적절히 활용하면 복잡한 비동기 작업도 깔끔하고 안전하게 처리할 수 있게 되죠.