Go에서 context를 이용한 goroutine 종료 패턴
문제 상황
회사 결제 서비스의 일부를 Node.js에서 Go로 전환하는 작업을 진행 중이다. 외부 API 호출 시 타임아웃을 걸고 여러 goroutine을 동시에 실행하는 로직에서, 타임아웃 발생 시에도 goroutine이 계속 실행되는 문제가 있었다.
func fetchUserData(userID string) (*User, error) {
done := make(chan *User)
go func() {
user := callExternalAPI(userID) // 오래 걸릴 수 있음
done <- user
}()
select {
case user := <-done:
return user, nil
case <-time.After(3 * time.Second):
return nil, errors.New("timeout")
}
}
위 코드는 타임아웃 후에도 goroutine이 계속 실행되어 리소스 낭비가 발생한다.
해결 방법
context 패키지를 사용해서 goroutine에 취소 신호를 전달하도록 수정했다.
func fetchUserData(ctx context.Context, userID string) (*User, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
done := make(chan *User)
errCh := make(chan error)
go func() {
user, err := callExternalAPIWithContext(ctx, userID)
if err != nil {
errCh <- err
return
}
done <- user
}()
select {
case user := <-done:
return user, nil
case err := <-errCh:
return nil, err
case <-ctx.Done():
return nil, ctx.Err()
}
}
HTTP 클라이언트도 context를 받도록 수정했다.
func callExternalAPIWithContext(ctx context.Context, userID string) (*User, error) {
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
resp, err := httpClient.Do(req)
// ...
}
결과
타임아웃 발생 시 goroutine이 즉시 종료되고, 불필요한 외부 API 호출도 취소된다. context는 Go 1.7부터 표준 라이브러리에 포함되었고, 이제는 거의 모든 장기 실행 함수의 첫 번째 인자로 사용하는 것이 관례가 되었다.
Node.js에서는 Promise 취소가 표준이 아니라 타임아웃 처리가 까다로웠는데, Go의 context 패턴이 훨씬 명확하고 안전한 것 같다.