Go 채널과 select를 이용한 동시성 패턴 정리

배경

팀에서 트래픽이 높은 API 서버를 Node.js에서 Go로 전환하는 작업을 진행 중이다. Go의 goroutine과 채널을 이용한 동시성 처리가 Node의 비동기 처리와는 상당히 달라서 적응하는데 시간이 필요했다.

타임아웃 처리

외부 API 호출 시 타임아웃을 구현할 때 select와 time.After를 조합했다.

func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
    result := make(chan string, 1)
    
    go func() {
        resp, _ := http.Get(url)
        defer resp.Body.Close()
        body, _ := ioutil.ReadAll(resp.Body)
        result <- string(body)
    }()
    
    select {
    case res := <-result:
        return res, nil
    case <-time.After(timeout):
        return "", errors.New("timeout")
    }
}

Worker Pool 패턴

대량의 작업을 처리할 때 worker pool을 구현했다. 고정된 수의 goroutine으로 작업 큐를 소비하는 방식이다.

func processJobs(jobs <-chan Job, results chan<- Result, numWorkers int) {
    var wg sync.WaitGroup
    
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                results <- process(job)
            }
        }()
    }
    
    wg.Wait()
    close(results)
}

Done 채널 패턴

여러 goroutine에 종료 신호를 브로드캐스트할 때는 done 채널을 닫는 방식을 사용했다.

func worker(done <-chan struct{}) {
    for {
        select {
        case <-done:
            return
        default:
            // 작업 수행
        }
    }
}

소감

Node.js의 Promise나 async/await에 익숙했던 터라 처음엔 낯설었지만, 채널 기반 동시성이 더 명시적이고 제어하기 쉽다는 걸 느꼈다. 특히 select문으로 여러 채널을 동시에 다루는 부분이 유용했다.

Go 채널과 select를 이용한 동시성 패턴 정리