Go 동시성 패턴: Context를 활용한 Goroutine 제어

문제 상황

재택근무로 전환되면서 기존 Node.js 서비스 일부를 Go로 마이그레이션하는 작업을 진행했다. API 서버에서 외부 서비스 호출 시 Goroutine을 사용했는데, 테스트 중 메모리 사용량이 계속 증가하는 현상을 발견했다.

func fetchData(url string) {
    go func() {
        resp, _ := http.Get(url)
        // 타임아웃이나 취소 처리 없음
        defer resp.Body.Close()
    }()
}

클라이언트가 요청을 취소해도 Goroutine은 계속 실행되고 있었다.

Context 적용

Go의 context 패키지를 사용해 Goroutine 생명주기를 제어했다.

func fetchDataWithContext(ctx context.Context, url string) error {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return err
    }
    
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    // 처리 로직
    return nil
}

// HTTP 핸들러에서
func handler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()
    
    if err := fetchDataWithContext(ctx, apiURL); err != nil {
        // 에러 처리
    }
}

결과

  • 클라이언트 요청 취소 시 즉시 Goroutine 종료
  • 타임아웃 설정으로 장시간 대기 방지
  • 메모리 누수 해결

추가 패턴

여러 Goroutine을 관리할 때는 errgroup을 사용하면 편리했다.

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

func processMultiple(ctx context.Context, urls []string) error {
    g, ctx := errgroup.WithContext(ctx)
    
    for _, url := range urls {
        url := url
        g.Go(func() error {
            return fetchDataWithContext(ctx, url)
        })
    }
    
    return g.Wait()
}

Context는 Go의 필수 패턴이다. 모든 장기 실행 작업에는 Context를 전달하는 습관을 들였다.

Go 동시성 패턴: Context를 활용한 Goroutine 제어