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를 전달하는 습관을 들였다.