Go에서 여러 고루틴 기다리기: 4가지 필수적 방법
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Go에서 메인 고루틴은 종종 다른 고루틴이 작업을 완료할 때까지 실행을 계속하거나 프로그램을 종료하기 전에 기다려야 합니다. 이는 동시성 동기화에 대한 일반적인 요구 사항입니다. Go는 시나리오 및 요구 사항에 따라 이를 달성하기 위한 여러 메커니즘을 제공합니다.
방법 1: sync.WaitGroup 사용
sync.WaitGroup
은 Go에서 가장 일반적으로 사용되는 동기화 도구로, 고루틴 그룹이 작업을 완료할 때까지 기다리도록 설계되었습니다. 카운터 메커니즘을 통해 작동하며, 메인 고루틴이 여러 하위 고루틴을 기다려야 할 때 특히 적합합니다.
예제 코드
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup // 3개의 고루틴 시작 for i := 1; i <= 3; i++ { wg.Add(1) // 카운터를 1씩 증가 go func(id int) { defer wg.Done() // 작업이 완료되면 카운터를 1씩 감소 fmt.Printf("고루틴 %d이(가) 실행 중\n", id) }(i) } wg.Wait() // 메인 고루틴은 모든 고루틴이 완료될 때까지 기다립니다. fmt.Println("모든 고루틴이 완료되었습니다.") }
출력 (순서는 다를 수 있음):
고루틴 1이(가) 실행 중 고루틴 2이(가) 실행 중 고루틴 3이(가) 실행 중 모든 고루틴이 완료되었습니다.
작동 방식:
wg.Add(n)
: 기다려야 할 고루틴 수를 나타내기 위해 카운터를 증가시킵니다.wg.Done()
: 각 고루틴이 완료되면 호출되며, 카운터를 1씩 감소시킵니다.wg.Wait()
: 카운터가 0에 도달할 때까지 메인 고루틴을 차단합니다.
장점:
- 간단하고 사용하기 쉬우며, 고정된 수의 고루틴에 적합합니다.
- 추가 채널이 필요 없으며, 성능 오버헤드가 낮습니다.
방법 2: 채널 사용
채널을 통해 신호를 전달함으로써 메인 고루틴은 다른 모든 고루틴이 완료 신호를 보낼 때까지 기다릴 수 있습니다. 이 방법은 더 유연하지만, 일반적으로 WaitGroup보다 약간 더 복잡합니다.
예제 코드
package main import "fmt" func main() { done := make(chan struct{}) // 완료를 알리는 신호 채널 numGoroutines := 3 for i := 1; i <= numGoroutines; i++ { go func(id int) { fmt.Printf("고루틴 %d이(가) 실행 중\n", id) done <- struct{}{} // 작업이 완료되면 신호를 보냅니다. }(i) } // 모든 고루틴이 완료될 때까지 기다립니다. for i := 0; i < numGoroutines; i++ { <-done // 신호를 받습니다. } fmt.Println("모든 고루틴이 완료되었습니다.") }
출력 (순서는 다를 수 있음):
고루틴 1이(가) 실행 중 고루틴 2이(가) 실행 중 고루틴 3이(가) 실행 중 모든 고루틴이 완료되었습니다.
작동 방식:
- 각 고루틴은 완료 시
done
채널로 신호를 보냅니다. - 메인 고루틴은 지정된 수의 신호를 수신하여 모든 작업이 완료되었음을 확인합니다.
장점:
- 높은 유연성, 데이터(예: 작업 결과)를 전달할 수 있습니다.
- 동적인 수의 고루틴에 적합합니다.
단점:
- 수신 횟수를 수동으로 관리해야 하므로 코드가 약간 번거로울 수 있습니다.
방법 3: 컨텍스트로 종료 제어
context.Context
를 사용하면 고루틴 종료를 정상적으로 제어하고 메인 고루틴이 모든 작업이 완료될 때까지 기다릴 수 있습니다. 이 방법은 취소 또는 시간 초과가 필요한 시나리오에서 특히 유용합니다.
예제 코드
package main import ( "context" "fmt" "sync" ) func main() { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) go func(id int) { defer wg.Done() select { case <-ctx.Done(): fmt.Printf("고루틴 %d이(가) 취소되었습니다.\n", id) return default: fmt.Printf("고루틴 %d이(가) 실행 중\n", id) } }(i) } // 작업 완료 시뮬레이션 cancel() // 취소 신호 보내기 wg.Wait() // 모든 고루틴이 종료될 때까지 기다립니다. fmt.Println("모든 고루틴이 완료되었습니다.") }
출력 (취소 시점에 따라 다를 수 있음):
고루틴 1이(가) 실행 중 고루틴 2이(가) 실행 중 고루틴 3이(가) 실행 중 모든 고루틴이 완료되었습니다.
작동 방식:
- 컨텍스트는 고루틴에 종료를 알리는 데 사용됩니다.
- WaitGroup은 메인 고루틴이 모든 고루틴이 완료될 때까지 기다리도록 보장합니다.
장점:
- 취소 및 시간 초과를 지원하며, 복잡한 동시성 시나리오에 적합합니다.
단점:
- 코드가 약간 더 복잡합니다.
방법 4: errgroup 사용 (권장)
golang.org/x/sync/errgroup
은 WaitGroup의 대기 기능과 오류 처리를 결합한 고급 도구로, 작업 그룹을 기다리고 오류를 처리하는 데 특히 적합합니다.
예제 코드
package main import ( "fmt" "golang.org/x/sync/errgroup" ) func main() { var g errgroup.Group for i := 1; i <= 3; i++ { id := i g.Go(func() error { fmt.Printf("고루틴 %d이(가) 실행 중\n", id) return nil // 오류 없음 }) } if err := g.Wait(); err != nil { fmt.Println("오류:", err) } else { fmt.Println("모든 고루틴이 완료되었습니다.") } }
출력:
고루틴 1이(가) 실행 중 고루틴 2이(가) 실행 중 고루틴 3이(가) 실행 중 모든 고루틴이 완료되었습니다.
작동 방식:
g.Go()
는 고루틴을 시작하고 그룹에 추가합니다.g.Wait()
는 모든 고루틴이 완료될 때까지 기다리고 첫 번째 nil이 아닌 오류(있는 경우)를 반환합니다.
장점:
- 간단하고 우아하며, 오류 전파를 지원합니다.
- 내장된 컨텍스트 지원 (
errgroup.WithContext
사용 가능).
설치:
go get golang.org/x/sync/errgroup
필요.
어떤 방법을 선택해야 할까요?
sync.WaitGroup
- 적용 가능한 시나리오: 고정된 수의 간단한 작업.
- 장점: 간단하고 효율적입니다.
- 단점: 오류 처리 또는 취소를 지원하지 않습니다.
채널
- 적용 가능한 시나리오: 동적 작업 또는 결과를 전달해야 하는 경우.
- 장점: 매우 유연합니다.
- 단점: 수동 관리가 더 복잡합니다.
컨텍스트
- 적용 가능한 시나리오: 취소 또는 시간 초과가 필요한 복잡한 상황.
- 장점: 취소 및 시간 초과를 지원합니다.
- 단점: 코드가 약간 더 복잡합니다.
errgroup
- 적용 가능한 시나리오: 오류 처리 및 대기가 필요한 최신 애플리케이션.
- 장점: 우아하고 강력합니다.
- 단점: 추가 종속성이 필요합니다.
기타: 메인 고루틴이 그냥 잠들지 않는 이유는 무엇일까요?
time.Sleep
은 고정된 지연만 발생시키고 작업이 완료될 때까지 정확하게 기다릴 수 없습니다. 이로 인해 프로그램이 조기에 종료되거나 불필요한 대기가 발생할 수 있습니다. 동기화 도구가 더 안정적입니다.
요약
메인 고루틴이 다른 고루틴을 기다리는 데 가장 일반적으로 사용되는 방법은 간단하고 효율적인 sync.WaitGroup
입니다. 오류 처리 또는 취소 기능이 필요한 경우 errgroup
또는 context
와의 조합을 권장합니다. 명확한 프로그램 논리를 보장하고 리소스 누수를 방지하려면 특정 요구 사항에 따라 적절한 도구를 선택하십시오.
Leapcell은 Go 프로젝트 호스팅을 위한 최고의 선택입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하십시오.
무료로 무제한 프로젝트 배포
- 사용량에 대해서만 지불하십시오. 요청도 없고, 요금도 없습니다.
최고의 비용 효율성
- 유휴 요금 없이 사용한 만큼만 지불하십시오.
- 예: $25로 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
손쉬운 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장됩니다.
- 운영 오버헤드가 없으므로 구축에만 집중하십시오.
설명서에서 자세히 알아보십시오!
X에서 팔로우하세요: @LeapcellHQ