
ErrGroup (에러그룹): Go (고) 동시성 프로그래밍의 숨겨진 보석
Go (고) 언어 errgroup (에러그룹) 라이브러리: 강력한 동시성 제어 도구
errgroup (에러그룹)은 공식 Go (고) 라이브러리 x에 있는 유틸리티로, 여러 고루틴(goroutine)을 동시에 실행하고 오류를 처리하는 데 사용됩니다.
이는 sync.WaitGroup (씽크 점 웨이트그룹)을 기반으로 errgroup.Group (에러그룹 점 그룹)을 구현하여 동시성 프로그래밍을 위한 더욱 강력한 기능을 제공합니다.
errgroup (에러그룹)의 장점
sync.WaitGroup (씽크 점 웨이트그룹)과 비교하여 errgroup.Group (에러그룹 점 그룹)은 다음과 같은 장점이 있습니다.
오류 처리: sync.WaitGroup (씽크 점 웨이트그룹)은 고루틴(goroutine)이 완료될 때까지 기다리는 역할만 하며 반환 값이나 오류를 처리하지 않습니다.
반면 errgroup.Group (에러그룹 점 그룹)은 반환 값을 직접 처리할 수는 없지만, 고루틴(goroutine)에서 오류가 발생하면 실행 중인 다른 고루틴(goroutine)을 즉시 취소하고 Wait (웨이트) 메서드에서 첫 번째 nil (닐)이 아닌 오류를 반환할 수 있습니다.
컨텍스트 취소: errgroup (에러그룹)은 context.Context (컨텍스트 점 컨텍스트)와 함께 사용할 수 있습니다.
고루틴(goroutine)에서 오류가 발생하면 다른 고루틴(goroutine)을 자동으로 취소하여 리소스를 효과적으로 제어하고 불필요한 작업을 방지할 수 있습니다.
동시성 프로그래밍 단순화: errgroup (에러그룹)을 사용하면 오류 처리를 위한 상용구 코드를 줄일 수 있습니다.
개발자는 오류 상태와 동기화 로직을 수동으로 관리할 필요가 없어 동시성 프로그래밍이 더 간단하고 유지 관리가 용이해집니다.
동시성 수 제한: errgroup (에러그룹)은 과부하를 피하기 위해 동시 고루틴(goroutine) 수를 제한하는 인터페이스를 제공하는데, 이는 sync.WaitGroup (씽크 점 웨이트그룹)에는 없는 기능입니다.
sync.WaitGroup (씽크 점 웨이트그룹) 사용 예시
errgroup.Group (에러그룹 점 그룹)을 소개하기 전에 먼저 sync.WaitGroup (씽크 점 웨이트그룹)의 사용법을 복습해 보겠습니다.
package main
import (
"fmt"
"net/http"
"sync"
)
func main() {
var urls = []string{ // HTTP GET 요청을 보낼 URL 목록입니다.
"http://www.golang.org/",
"http://www.google.com/",
"http://www.somestupidname.com/", // 존재하지 않는 호스트입니다.
}
var err error // 오류를 저장할 변수입니다.
var wg sync.WaitGroup // WaitGroup을 선언합니다.
for _, url := range urls { // 각 URL에 대해 반복합니다.
wg.Add(1) // WaitGroup 카운터를 1 증가시킵니다.
// 고루틴을 시작합니다.
go func() {
defer wg.Done() // 고루틴 종료 시 WaitGroup 카운터를 1 감소시킵니다.
resp, e := http.Get(url) // HTTP GET 요청을 보냅니다. (주의: 클로저 내 url 변수 사용)
if e != nil { // 오류가 발생하면
err = e // 오류를 저장하고 (주의: 동시성 문제 발생 가능)
return // 고루틴을 종료합니다.
}
defer resp.Body.Close() // 응답 본문을 닫습니다.
fmt.Printf("fetch url %s status %s\n", url, resp.Status) // URL과 상태 코드를 출력합니다.
}()
}
wg.Wait() // 모든 고루틴이 완료될 때까지 기다립니다.
if err != nil { // 저장된 오류가 있으면
fmt.Printf("Error: %s\n", err) // 오류를 출력합니다.
}
}
실행 결과:
$ go run waitgroup/main.go
fetch url http://www.google.com/ status 200 OK
fetch url http://www.golang.org/ status 200 OK
Error: Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host
sync.WaitGroup (씽크 점 웨이트그룹)의 일반적인 사용 패턴:
var wg sync.WaitGroup // WaitGroup 선언
for ... { // 반복문
wg.Add(1) // 카운터 증가
go func() { // 고루틴 시작
defer wg.Done() // 고루틴 종료 시 카운터 감소
// 작업 수행
}()
}
wg.Wait() // 모든 고루틴 완료 대기
errgroup.Group (에러그룹 점 그룹) 사용 예시
기본 사용법
errgroup.Group (에러그룹 점 그룹)의 사용 패턴은 sync.WaitGroup (씽크 점 웨이트그룹)과 유사합니다.
package main
import (
"fmt"
"net/http"
"golang.org/x/sync/errgroup" // errgroup 패키지를 가져옵니다.
)
func main() {
var urls = []string{ // HTTP GET 요청을 보낼 URL 목록입니다.
"http://www.golang.org/",
"http://www.google.com/",
"http://www.somestupidname.com/", // 존재하지 않는 호스트입니다.
}
var g errgroup.Group // errgroup.Group을 선언합니다.
for _, url := range urls { // 각 URL에 대해 반복합니다.
// g.Go를 사용하여 고루틴을 시작합니다.
// 클로저가 올바른 url 값을 사용하도록 url을 매개변수로 전달하는 것이 좋습니다.
// 여기서는 예제 간결성을 위해 외부 스코프의 url을 직접 사용합니다.
// 실제 사용 시에는 currentURL := url과 같이 복사하여 사용하거나,
// g.Go(func(u string) func() error { ... }(url)) 형태로 전달하는 것이 안전합니다.
// 아래 코드에서는 외부 url 변수를 직접 참조하고 있습니다.
// 이는 동시 실행 시 예기치 않은 동작을 유발할 수 있습니다.
// 하지만 이 예제에서는 각 고루틴이 시작될 때의 url 값을 사용하게 됩니다.
// 더 안전한 방법은 아래와 같습니다:
// currentURL := url
// g.Go(func() error {
// resp, err := http.Get(currentURL)
// ...
// })
// 또는
// g.Go(func(u string) func() error {
// return func() error {
// resp, err := http.Get(u)
// ...
// }
// }(url))
// 현재 예제 코드 (주의 필요)
// url 변수를 각 고루틴에 복사하여 전달하는 것이 좋습니다.
// 아래와 같이 수정하면 각 고루틴이 올바른 url 값을 사용하게 됩니다.
currentURL := url
g.Go(func() error {
resp, err := http.Get(currentURL) // HTTP GET 요청을 보냅니다.
if err != nil {
return err // 오류가 발생하면 오류를 반환합니다.
}
defer resp.Body.Close() // 응답 본문을 닫습니다.
fmt.Printf("fetch url %s status %s\n", currentURL, resp.Status) // URL과 상태 코드를 출력합니다.
return nil // 오류가 없으면 nil을 반환합니다.
})
}
// g.Wait()는 모든 고루틴이 완료될 때까지 기다리고, 첫 번째 발생한 오류를 반환합니다.
if err := g.Wait(); err != nil {
fmt.Printf("Error: %s\n", err) // 오류를 출력합니다.
}
}
실행 결과:
$ go run examples/main.go
fetch url http://www.google.com/ status 200 OK
fetch url http://www.golang.org/ status 200 OK
Error: Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host
컨텍스트 취소
errgroup (에러그룹)은 취소 기능을 추가하기 위해 errgroup.WithContext (에러그룹 점 위드컨텍스트)를 제공합니다.
package main
import (
"context" // context 패키지를 가져옵니다.
"fmt"
"net/http"
"sync"
"golang.org/x/sync/errgroup"
)
func main() {
var urls = []string{ // HTTP GET 요청을 보낼 URL 목록입니다.
"http://www.golang.org/",
"http://www.google.com/",
"http://www.somestupidname.com/", // 존재하지 않는 호스트입니다.
}
// errgroup.WithContext를 사용하여 Group과 함께 취소 가능한 Context를 생성합니다.
g, ctx := errgroup.WithContext(context.Background())
var result sync.Map // 결과를 저장할 동기화된 맵입니다.
for _, url := range urls { // 각 URL에 대해 반복합니다.
// url 변수를 각 고루틴에 복사하여 전달합니다.
currentURL := url
g.Go(func() error {
// Context를 사용하여 HTTP 요청을 생성합니다.
req, err := http.NewRequestWithContext(ctx, "GET", currentURL, nil)
if err != nil {
return err // 오류가 발생하면 오류를 반환합니다.
}
resp, err := http.DefaultClient.Do(req) // HTTP 요청을 실행합니다.
if err != nil {
return err // 오류가 발생하면 오류를 반환합니다.
}
defer resp.Body.Close() // 응답 본문을 닫습니다.
result.Store(currentURL, resp.Status) // 결과를 맵에 저장합니다.
return nil // 오류가 없으면 nil을 반환합니다.
})
}
// 모든 고루틴이 완료될 때까지 기다리고, 오류가 발생하면 해당 오류를 출력합니다.
if err := g.Wait(); err != nil {
fmt.Println("Error: ", err)
}
// 저장된 결과를 출력합니다.
result.Range(func(key, value any) bool {
fmt.Printf("fetch url %s status %s\n", key, value)
return true
})
}
실행 결과:
$ go run examples/withcontext/main.go
Error: Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host
fetch url http://www.google.com/ status 200 OK
http://www.somestupidname.com/ (에이치티티피 콜론 슬래시슬래시 더블유더블유더블유 쩜 썸스투피드네임 쩜 컴 슬래시) 요청에서 오류가 발생했기 때문에 프로그램은 http://www.golang.org/ (에이치티티피 콜론 슬래시슬래시 더블유더블유더블유 쩜 고랭 쩜 오알지 슬래시) 요청을 취소했습니다.
동시성 수 제한
errgroup (에러그룹)은 동시에 실행되는 고루틴(goroutine) 수를 제한하기 위해 errgroup.SetLimit (에러그룹 점 셋리미트)를 제공합니다.
package main
import (
"fmt"
"time"
"golang.org/x/sync/errgroup"
)
func main() {
var g errgroup.Group
g.SetLimit(3) // 동시에 실행될 고루틴 수를 3으로 제한합니다.
for i := 1; i <= 10; i++ {
// 클로저가 올바른 i 값을 사용하도록 i를 매개변수로 전달합니다.
// currentI := i 와 같이 복사하여 사용하는 것이 더 명확할 수 있습니다.
// 여기서는 예제의 간결성을 위해 외부 스코프의 i를 직접 사용하고 있습니다.
// 주의: 아래 코드에서 고루틴 내 i는 반복문 변수를 직접 참조하므로,
// 실제 실행 시점의 i 값을 사용하게 되어 의도와 다를 수 있습니다.
// 안전한 방법은 아래와 같습니다:
// currentI := i
// g.Go(func() error {
// fmt.Printf("Goroutine %d is starting\n", currentI)
// ...
// fmt.Printf("Goroutine %d is done\n", currentI)
// return nil
// })
// 또는
// g.Go(func(val int) func() error {
// return func() error {
// fmt.Printf("Goroutine %d is starting\n", val)
// ...
// fmt.Printf("Goroutine %d is done\n", val)
// return nil
// }
// }(i))
// 현재 예제 코드 (주의 필요)
// 각 고루틴에 i 값을 복사하여 전달해야 합니다.
// 아래와 같이 수정하면 각 고루틴이 시작 시점의 올바른 i 값을 사용하게 됩니다.
currentI := i
g.Go(func() error {
fmt.Printf("Goroutine %d is starting\n", currentI) // 고루틴 시작을 알립니다.
time.Sleep(2 * time.Second) // 2초 동안 대기합니다.
fmt.Printf("Goroutine %d is done\n", currentI) // 고루틴 완료를 알립니다.
return nil
})
}
// 모든 고루틴이 완료될 때까지 기다립니다.
if err := g.Wait(); err != nil {
fmt.Printf("Encountered an error: %v\n", err) // 오류가 발생하면 출력합니다.
}
fmt.Println("All goroutines complete.") // 모든 고루틴 완료를 알립니다.
}
실행 결과:
$ go run examples/setlimit/main.go
Goroutine 3 is starting
Goroutine 1 is starting
Goroutine 2 is starting
Goroutine 2 is done
Goroutine 1 is done
Goroutine 5 is starting
Goroutine 3 is done
Goroutine 6 is starting
Goroutine 4 is starting
Goroutine 6 is done
Goroutine 5 is done
Goroutine 8 is starting
Goroutine 4 is done
Goroutine 7 is starting
Goroutine 9 is starting
Goroutine 9 is done
Goroutine 8 is done
Goroutine 10 is starting
Goroutine 7 is done
Goroutine 10 is done
All goroutines complete.
작업 시작 시도
errgroup (에러그룹)은 작업을 시작하려고 시도하는 errgroup.TryGo (에러그룹 점 트라이고)를 제공하며, 이는 errgroup.SetLimit (에러그룹 점 셋리미트)와 함께 사용해야 합니다.
package main
import (
"fmt"
"time"
"golang.org/x/sync/errgroup"
)
func main() {
var g errgroup.Group
g.SetLimit(3) // 동시에 실행될 고루틴 수를 3으로 제한합니다.
for i := 1; i <= 10; i++ {
// 클로저가 올바른 i 값을 사용하도록 i를 매개변수로 전달합니다.
// 이전 예제와 동일한 주의사항이 적용됩니다.
// currentI := i 와 같이 복사하여 사용하는 것이 좋습니다.
// 현재 예제 코드 (주의 필요)
// 각 고루틴에 i 값을 복사하여 전달해야 합니다.
// 아래와 같이 수정하면 각 고루틴이 시작 시점의 올바른 i 값을 사용하게 됩니다.
currentI := i
// TryGo는 현재 실행 중인 고루틴 수가 제한에 도달하지 않았으면 true를 반환하고 작업을 시작합니다.
if g.TryGo(func() error {
fmt.Printf("Goroutine %d is starting\n", currentI) // 고루틴 시작을 알립니다.
time.Sleep(2 * time.Second) // 2초 동안 대기합니다.
fmt.Printf("Goroutine %d is done\n", currentI) // 고루틴 완료를 알립니다.
return nil
}) {
fmt.Printf("Goroutine %d started successfully\n", currentI) // 고루틴 시작 성공을 알립니다.
} else {
fmt.Printf("Goroutine %d could not start (limit reached)\n", currentI) // 고루틴 시작 실패(제한 도달)를 알립니다.
}
}
// 모든 고루틴이 완료될 때까지 기다립니다.
if err := g.Wait(); err != nil {
fmt.Printf("Encountered an error: %v\n", err) // 오류가 발생하면 출력합니다.
}
fmt.Println("All goroutines complete.") // 모든 고루틴 완료를 알립니다.
}
실행 결과:
$ go run examples/trygo/main.go
Goroutine 1 started successfully
Goroutine 1 is starting
Goroutine 2 is starting
Goroutine 2 started successfully
Goroutine 3 started successfully
Goroutine 4 could not start (limit reached)
Goroutine 5 could not start (limit reached)
Goroutine 6 could not start (limit reached)
Goroutine 7 could not start (limit reached)
Goroutine 8 could not start (limit reached)
Goroutine 9 could not start (limit reached)
Goroutine 10 could not start (limit reached)
Goroutine 3 is starting
Goroutine 2 is done
Goroutine 3 is done
Goroutine 1 is done
All goroutines complete.
소스 코드 해석
errgroup (에러그룹)의 소스 코드는 주로 3개의 파일로 구성됩니다.
핵심 구조체
// token은 빈 구조체로, 동시성 수를 제어하기 위한 신호를 전달하는 데 사용됩니다.
type token struct{}
// Group은 여러 고루틴을 관리하고 오류를 처리하는 구조체입니다.
type Group struct {
cancel func(error) // 컨텍스트가 취소될 때 호출되는 함수입니다.
wg sync.WaitGroup // 내부적으로 사용되는 sync.WaitGroup입니다.
sem chan token // 동시 실행 고루틴 수를 제어하는 신호 채널입니다.
errOnce sync.Once // 오류가 한 번만 처리되도록 보장합니다.
err error // 첫 번째 발생한 오류를 기록합니다.
}
token (토큰): 동시성 수를 제어하기 위해 신호를 전달하는 데 사용되는 빈 구조체입니다.Group (그룹):cancel (캔슬): 컨텍스트가 취소될 때 호출되는 함수입니다.wg (더블유지): 내부적으로 사용되는 sync.WaitGroup (씽크 점 웨이트그룹)입니다.sem (셈): 동시 실행 코루틴 수를 제어하는 신호 채널입니다.errOnce (에러원스): 오류가 한 번만 처리되도록 보장합니다.err (에러): 첫 번째 오류를 기록합니다.
주요 메서드
SetLimit (셋리미트): 동시성 수를 제한합니다.
// SetLimit은 동시에 실행될 수 있는 고루틴의 수를 제한합니다.
// n < 0 이면 제한이 없습니다.
func (g *Group) SetLimit(n int) {
if n < 0 {
g.sem = nil // 제한 없음을 나타냅니다.
return
}
if len(g.sem) != 0 { // 이미 활성 고루틴이 있는 상태에서 제한을 변경하려고 하면 패닉을 발생시킵니다.
panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", len(g.sem)))
}
g.sem = make(chan token, n) // 제한된 크기의 채널을 생성합니다.
}
Go (고): 작업을 실행하기 위해 새 코루틴을 시작합니다.
// Go는 새로운 고루틴에서 주어진 함수를 호출합니다.
// 첫 번째 nil이 아닌 오류가 반환되면 Wait는 해당 오류를 반환하고
// 그룹의 컨텍스트가 있는 경우 해당 컨텍스트를 취소합니다.
func (g *Group) Go(f func() error) {
if g.sem != nil { // 동시성 제한이 설정된 경우
g.sem <- token{} // 채널에 토큰을 보내 실행 권한을 얻습니다. (꽉 차면 대기)
}
g.wg.Add(1) // WaitGroup 카운터를 증가시킵니다.
go func() { // 새로운 고루틴을 시작합니다.
defer g.done() // 고루틴 종료 시 done 메서드를 호출합니다. (sem에서 토큰을 반환하고 wg.Done() 호출)
if err := f(); err != nil { // 함수를 실행하고 오류가 발생하면
g.errOnce.Do(func() { // 한 번만 오류를 기록하고 처리합니다.
g.err = err
if g.cancel != nil { // 취소 함수가 설정되어 있으면
g.cancel(g.err) // 컨텍스트를 취소합니다.
}
})
}
}()
}
Wait (웨이트): 모든 작업이 완료될 때까지 기다리고 첫 번째 오류를 반환합니다.
// Wait는 g.Go로 시작된 모든 함수 호출이 반환될 때까지 차단한 다음,
// 해당 호출에서 반환된 첫 번째 nil이 아닌 오류(있는 경우)를 반환합니다.
func (g *Group) Wait() error {
g.wg.Wait() // 모든 고루틴이 완료될 때까지 기다립니다.
if g.cancel != nil { // 취소 함수가 설정되어 있으면
g.cancel(g.err) // (이미 오류로 인해 취소되었을 수 있지만) 컨텍스트를 취소합니다.
}
return g.err // 기록된 첫 번째 오류를 반환합니다.
}
TryGo (트라이고): 작업 시작을 시도합니다.
// TryGo는 현재 실행 중인 고루틴 수가 제한에 도달하지 않은 경우에만 함수를 실행합니다.
// 함수가 실행되면 true를 반환하고, 그렇지 않으면 false를 반환합니다.
func (g *Group) TryGo(f func() error) bool {
if g.sem != nil { // 동시성 제한이 설정된 경우
select {
case g.sem <- token{}: // 채널에 토큰을 즉시 보낼 수 있으면 (실행 가능)
// 토큰을 획득했습니다.
default: // 채널이 꽉 차서 즉시 보낼 수 없으면 (실행 불가)
return false // false를 반환합니다.
}
}
g.wg.Add(1) // WaitGroup 카운터를 증가시킵니다.
go func() { // 새로운 고루틴을 시작합니다.
defer g.done() // 고루틴 종료 시 done 메서드를 호출합니다.
if err := f(); err != nil { // 함수를 실행하고 오류가 발생하면
g.errOnce.Do(func() { // 한 번만 오류를 기록하고 처리합니다.
g.err = err
if g.cancel != nil { // 취소 함수가 설정되어 있으면
g.cancel(g.err) // 컨텍스트를 취소합니다.
}
})
}
}()
return true // 함수 실행이 시작되었으므로 true를 반환합니다.
}
결론
errgroup (에러그룹)은 sync.WaitGroup (씽크 점 웨이트그룹)을 기반으로 오류 처리 기능을 추가한 공식 확장 라이브러리로, 동기화, 오류 전파, 컨텍스트 취소와 같은 기능을 제공합니다.WithContext (위드컨텍스트) 메서드는 취소 기능을 추가할 수 있고, SetLimit (셋리미트)는 동시성 수를 제한할 수 있으며, TryGo (트라이고)는 작업 시작을 시도할 수 있습니다.
소스 코드는 독창적으로 설계되었으며 참고할 가치가 있습니다.
'Go' 카테고리의 다른 글
| Go (고) 컴파일러 성능 최적화 팁과 트릭 (0) | 2025.05.17 |
|---|---|
| Go (고) 언어 panic (패닉) 및 recover (리커버) 심층 해부: 알아야 할 모든 것! (0) | 2025.05.17 |
| Go (고) 언어에서 고루틴 풀(Goroutine Pool)을 구현하는 방법은? (0) | 2025.05.17 |
| Go 언어 완벽 마스터: 함수형 프로그래밍이 최고의 선택인 이유 (0) | 2025.05.17 |
| Go 언어로 JWT 완벽 마스터! 안전한 로그인과 권한 부여, A부터 Z까지 파헤치기 (JWT 초보 탈출 가이드) (0) | 2025.05.07 |