Go에서 context 취소 시 goroutine 정리 패턴
문제 상황
사용자 요청에 따라 여러 외부 API를 병렬로 호출하는 aggregation 엔드포인트를 만들었다. 클라이언트가 연결을 끊어도 goroutine들이 계속 실행되면서 메모리 사용량이 증가하는 문제가 발견됐다.
func fetchData(userID string) (*Result, error) {
resultCh := make(chan *APIResponse, 3)
go fetchUserInfo(userID, resultCh)
go fetchUserPosts(userID, resultCh)
go fetchUserStats(userID, resultCh)
// 클라이언트가 연결을 끊어도 goroutine은 계속 실행됨
return collectResults(resultCh)
}
해결 방법
context를 활용해 요청 취소 시그널을 전파하도록 수정했다.
func fetchData(ctx context.Context, userID string) (*Result, error) {
resultCh := make(chan *APIResponse, 3)
errCh := make(chan error, 3)
go fetchUserInfo(ctx, userID, resultCh, errCh)
go fetchUserPosts(ctx, userID, resultCh, errCh)
go fetchUserStats(ctx, userID, resultCh, errCh)
return collectResultsWithContext(ctx, resultCh, errCh)
}
func fetchUserInfo(ctx context.Context, userID string, resultCh chan *APIResponse, errCh chan error) {
req, _ := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
resp, err := client.Do(req)
if err != nil {
select {
case errCh <- err:
case <-ctx.Done():
return
}
return
}
// response 처리...
}
주의사항
- 채널 전송 시에도 context 확인: 채널이 블록되면 goroutine이 leak 될 수 있다
- http.Client timeout 설정: context만으로는 네트워크 타임아웃을 완전히 제어할 수 없다
- 버퍼 크기: 버퍼 없는 채널은 수신자가 없으면 영원히 블록된다
pprof로 확인해보니 동시 요청 1000개 기준 goroutine 수가 3000+에서 100 이하로 감소했다. 프로덕션 배포 후 메모리 사용량도 안정화됐다.