본문 바로가기

프로그래밍/golang

[Golang] errgroup 이란?

 

Go에서 고루틴을 병렬로 구현하고 실행할 때, 가장 먼저 배우는 도구는 sync.WaitGroup 일 것입니다.
하지만 Go에서는 병렬 구현이 쉬운만큼 반대로 어떻게 관리할 것인가를 많이 고민하게 되는 것 같습니다.
이번에는 이러한 관리 문제를 해결하기 위해 만들어진 errgroup에 대해서 함께 알아 봅시다.


1. errorgroup

sync.WaitGroup은 고루틴을 관리하는 좋은 도구였지만, 아쉬운 점이 있었습니다.

  • 에러 전파 불가능 : 고루틴 내부에서 에러가 발생해도 상위 부모 루틴은 이를 알 수 있는 방법이 없습니다.
    별도의 에러 채널을 직접 만들고 관리해야 합니다.
  • 리소스 낭비 : 예를 들어 10개의 고루틴을 병렬로 실행하고 있는데, 어떤 고루틴이 에러가 발생했더라도, 나머지 고루틴들은
    그 사실을 알 수가 없음으로, 계속 자신의 동작을 수행하게 됩니다.

그래서 errgroup은 에러 핸들링과 작업 취소를 자동화하기 위해서 등장했습니다.

 

2. 핵심 매커니즘

errgroup 패키지의 핵심은 Go()와 Wait() 메서드에 있습니다.

  • Go(f func() error) : 고루틴을 실행하며, 에러를 리턴하도록 되어 있어 상위로 에러를 전달할 수 있습니다.
  • Wait() error :  모든 고루틴이 끝날 때까지 대기하다가, 실행되는 고루틴 중 가장 먼저 발생하는 에러를 리턴하며,
    모두 성공하면 nil을 리턴합니다

 

3. 예제 코드

 

3-1 : 에러 발생 시 즉시 중단

여러 외부 서비스의 상태를 체크하는 시나리오이며 해당 예제 코드는
하나라도 실패하면 나머지 체크는 의미 없는 것을 가정한 코드입니다.

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"golang.org/x/sync/errgroup"
)

type ServiceHealth struct {
	Name string
	URL  string
}

func main() {
	services := []ServiceHealth{
		{"Google", "https://www.google.com"},
		{"BrokenAPI", "https://this.url.does.not.exist.reo"},
		{"Naver", "https://www.naver.com"},
		{"GitHubAPI", "https://api.github.com"},
	}

	client := &http.Client{
		Timeout: 4 * time.Second,
	}

	g, ctx := errgroup.WithContext(context.Background())
	for _, svc := range services {
		svc := svc

		g.Go(func() error {
			reqCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
			defer cancel()

			req, err := http.NewRequestWithContext(reqCtx, "GET", svc.URL, nil)
			if err != nil {
				return err
			}

			req.Header.Set("User-Agent", "Go-Backend-Handbook-Example")

			resp, err := client.Do(req)
			if err != nil {
				return fmt.Errorf("%s 접속 실패: %w", svc.Name, err)
			}
			defer resp.Body.Close()

			if resp.StatusCode < 200 || resp.StatusCode >= 300 {
				return fmt.Errorf("%s bad status: %s", svc.Name, resp.Status)
			}

			fmt.Printf("[%s] 상태: %s\n", svc.Name, resp.Status)
			return nil
		})
	}

	if err := g.Wait(); err != nil {
		fmt.Printf("system check error : %v\n", err)
		return
	}
	fmt.Println("all service is good")
}

 

 

3-2 : 고루틴 실행 개수 제한

대량 작업을 처리할 때 시스템의 동시성을 직접 제어하는 방법에 대한 예제 입니다.

package main

import (
	"context"
	"fmt"
	"time"

	"golang.org/x/sync/errgroup"
)

func main() {
	g, ctx := errgroup.WithContext(context.Background())
	g.SetLimit(2)

	for i := 1; i <= 5; i++ {
		workerID := i
		g.Go(func() error {
			fmt.Printf("worker %d starting...\n", workerID)

			select {
			case <-time.After(1 * time.Second):
			case <-ctx.Done():
				fmt.Printf("worker %d canceled: %v\n", workerID, ctx.Err())
				return nil
			}

			// force an error
			if workerID == 3 {
				return fmt.Errorf("worker %d failed", workerID)
			}

			fmt.Printf("worker %d finishing\n", workerID)
			return nil
		})
	}
	if err := g.Wait(); err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}
	fmt.Println("All tasks complete")
}

 

SetLimit(2)를 설정했는데, 5개의 작업을 넘기게 되더라도, 한 번에 2개씩만 실행이 되며,

한 작업이 끝나야 다음 작업이 큐에서 빠져나와 실행이 됩니다. 

 

4. 전체 코드

https://github.com/reochoi109/go-handbook/blob/main/errGroup/basic/f1/main.go

 

go-handbook/errGroup/basic/f1/main.go at main · reochoi109/go-handbook

A personal handbook of Go patterns and best practices. Lightweight, practical code snippets for real-world backend development. - reochoi109/go-handbook

github.com