본문 바로가기
개발/go

Go 채널(Channel) : 고루틴 간 안전한 통신하기

by xwing 2025. 5. 28.

Go 채널(Channel) : 고루틴 간 안전한 통신하기

Go 언어의 채널(Channel)은 고루틴 간의 안전한 데이터 교환을 위한 핵심 도구입니다. "Don't communicate by sharing memory; share memory by communicating"이라는 Go의 철학을 구현하는 대표적인 기능이죠. 이번 글에서는 채널의 기본 개념부터 고급 사용법까지 실용적인 예제와 함께 살펴보겠습니다.

채널이란 무엇인가?

채널은 고루틴 간에 데이터를 주고받을 수 있는 파이프라인입니다. 한 고루틴에서 채널에 값을 보내면, 다른 고루틴에서 그 값을 받을 수 있습니다. 이를 통해 메모리 공유 없이도 안전하게 데이터를 교환할 수 있습니다.

기본 채널 사용법

채널 생성과 기본 송수신

package main

import "fmt"

func main() {
    // int 타입 채널 생성
    ch := make(chan int)
    
    // 고루틴에서 채널에 값 전송
    go func() {
        ch <- 42  // 채널에 42 전송
    }()
    
    // 메인 고루틴에서 값 수신
    value := <-ch  // 채널에서 값 수신
    fmt.Println("받은 값:", value)  // 출력: 받은 값: 42
}

채널을 이용한 고루틴 동기화

package main

import (
    "fmt"
    "time"
)

func worker(id int, done chan bool) {
    fmt.Printf("작업자 %d 시작\n", id)
    time.Sleep(time.Second)
    fmt.Printf("작업자 %d 완료\n", id)
    done <- true  // 작업 완료 신호
}

func main() {
    done := make(chan bool, 1)
    
    go worker(1, done)
    
    <-done  // 작업 완료까지 대기
    fmt.Println("모든 작업 완료")
}

버퍼드 채널 vs 언버퍼드 채널

언버퍼드 채널 (Synchronous)

package main

import "fmt"

func main() {
    ch := make(chan string)  // 버퍼 크기 0 (기본값)
    
    go func() {
        ch <- "동기화된 메시지"
    }()
    
    msg := <-ch
    fmt.Println(msg)
}

버퍼드 채널 (Asynchronous)

package main

import "fmt"

func main() {
    ch := make(chan string, 2)  // 버퍼 크기 2
    
    // 버퍼가 가득 찰 때까지 블로킹되지 않음
    ch <- "첫 번째 메시지"
    ch <- "두 번째 메시지"
    
    fmt.Println(<-ch)  // 첫 번째 메시지
    fmt.Println(<-ch)  // 두 번째 메시지
}

채널 방향성 제어

함수 매개변수에서 채널의 방향을 제한할 수 있습니다.

package main

import "fmt"

// 송신 전용 채널
func sender(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}

// 수신 전용 채널
func receiver(ch <-chan int) {
    for value := range ch {
        fmt.Printf("받은 값: %d\n", value)
    }
}

func main() {
    ch := make(chan int)
    
    go sender(ch)
    receiver(ch)
}

Select 문을 이용한 다중 채널 처리

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "채널 1에서 메시지"
    }()
    
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "채널 2에서 메시지"
    }()
    
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("ch1:", msg1)
        case msg2 := <-ch2:
            fmt.Println("ch2:", msg2)
        case <-time.After(3 * time.Second):
            fmt.Println("타임아웃!")
            return
        }
    }
}

실용적인 예제: Worker Pool 패턴

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    
    for job := range jobs {
        fmt.Printf("작업자 %d가 작업 %d 시작\n", id, job)
        
        // 랜덤한 작업 시간 시뮬레이션
        time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
        
        fmt.Printf("작업자 %d가 작업 %d 완료\n", id, job)
        results <- job * 2  // 작업 결과 전송
    }
}

func main() {
    const numJobs = 5
    const numWorkers = 3
    
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)
    
    var wg sync.WaitGroup
    
    // 워커 고루틴 시작
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }
    
    // 작업 전송
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)
    
    // 모든 워커가 완료될 때까지 대기
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // 결과 수집
    for result := range results {
        fmt.Printf("결과: %d\n", result)
    }
}

채널을 이용한 팬아웃/팬인 패턴

package main

import (
    "fmt"
    "sync"
)

// 팬아웃: 하나의 입력을 여러 고루틴으로 분산
func fanOut(input <-chan int) (<-chan int, <-chan int, <-chan int) {
    out1 := make(chan int)
    out2 := make(chan int)
    out3 := make(chan int)
    
    go func() {
        defer close(out1)
        defer close(out2)
        defer close(out3)
        
        for value := range input {
            // 모든 출력 채널에 동일한 값 전송
            out1 <- value
            out2 <- value
            out3 <- value
        }
    }()
    
    return out1, out2, out3
}

// 팬인: 여러 입력을 하나의 출력으로 합병
func fanIn(inputs ...<-chan int) <-chan int {
    output := make(chan int)
    var wg sync.WaitGroup
    
    // 각 입력 채널에 대해 고루틴 생성
    for _, input := range inputs {
        wg.Add(1)
        go func(ch <-chan int) {
            defer wg.Done()
            for value := range ch {
                output <- value
            }
        }(input)
    }
    
    // 모든 입력이 완료되면 출력 채널 닫기
    go func() {
        wg.Wait()
        close(output)
    }()
    
    return output
}

func main() {
    input := make(chan int)
    
    // 팬아웃
    out1, out2, out3 := fanOut(input)
    
    // 팬인
    result := fanIn(out1, out2, out3)
    
    // 입력 데이터 전송
    go func() {
        defer close(input)
        for i := 1; i <= 3; i++ {
            input <- i
        }
    }()
    
    // 결과 출력
    for value := range result {
        fmt.Printf("결과: %d\n", value)
    }
}

채널 사용 시 주의사항

1. 데드락 방지

// 잘못된 예제 - 데드락 발생
func badExample() {
    ch := make(chan int)
    ch <- 42  // 수신자가 없어서 영원히 블로킹됨
    fmt.Println(<-ch)
}

// 올바른 예제
func goodExample() {
    ch := make(chan int, 1)  // 버퍼드 채널 사용
    ch <- 42
    fmt.Println(<-ch)
}

2. 채널 닫기

func channelClosing() {
    ch := make(chan int, 3)
    
    // 데이터 전송
    ch <- 1
    ch <- 2
    ch <- 3
    
    close(ch)  // 더 이상 전송하지 않음을 알림
    
    // 채널이 닫혀도 남은 데이터는 수신 가능
    for value := range ch {
        fmt.Println(value)
    }
    
    // 닫힌 채널에서 수신 시 zero value와 false 반환
    value, ok := <-ch
    fmt.Printf("값: %d, 열려있음: %t\n", value, ok)
}

성능 고려사항

채널을 사용할 때는 다음과 같은 성능 특성을 고려해야 합니다:

  • 언버퍼드 채널은 동기화 오버헤드가 있지만 메모리 사용량이 적습니다
  • 버퍼드 채널은 비동기 처리가 가능하지만 메모리를 더 사용합니다
  • 채널 연산은 뮤텍스보다 약간 느리지만 더 안전하고 읽기 쉽습니다

결론

Go의 채널은 동시성 프로그래밍을 위한 강력하고 우아한 도구입니다. 적절히 사용하면 복잡한 동시성 로직을 명확하고 안전하게 구현할 수 있습니다. 채널의 다양한 패턴을 익히고 적재적소에 활용한다면, 더욱 견고하고 효율적인 Go 프로그램을 작성할 수 있을 것입니다.

핵심은 "메모리를 공유해서 통신하지 말고, 통신해서 메모리를 공유하라"는 Go의 철학을 이해하고 실천하는 것입니다. 채널을 통한 명시적인 통신은 코드의 의도를 명확하게 만들고, 경쟁 상태와 같은 동시성 버그를 예방하는 데 큰 도움이 됩니다.

'개발 > go' 카테고리의 다른 글

Go로 크로스 플랫폼 WiFi 스캐너  (0) 2025.05.28
Go(golang) 테트리스  (0) 2025.04.07
Golang으로 화면보호기와 절전 모드 방지하기  (0) 2025.03.24