Go 1.20 context.WithCancelCause로 취소 이유 전달하기
문제 상황
마이크로서비스 간 gRPC 통신에서 요청이 취소될 때 원인을 파악하기 어려웠다. 기존에는 context.Canceled 에러만 받아서 타임아웃인지, 클라이언트 취소인지, 서버 장애인지 구분이 안 됐다.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 어딘가에서 cancel() 호출
err := processRequest(ctx)
if err == context.Canceled {
// 왜 취소됐는지 알 수 없음
}
WithCancelCause 적용
Go 1.20에서 추가된 context.WithCancelCause를 사용하면 취소 원인을 명시할 수 있다.
ctx, cancel := context.WithCancelCause(context.Background())
go func() {
if dbErr := checkDB(); dbErr != nil {
cancel(fmt.Errorf("database health check failed: %w", dbErr))
return
}
if cacheErr := checkCache(); cacheErr != nil {
cancel(fmt.Errorf("cache unavailable: %w", cacheErr))
return
}
}()
err := processRequest(ctx)
if err != nil {
cause := context.Cause(ctx)
log.Printf("request failed: %v, cause: %v", err, cause)
}
실제 적용 사례
결제 처리 워커에 적용했다. 외부 PG사 API 호출, DB 락 획득 실패, 재고 부족 등 다양한 이유로 작업이 취소되는데, 이제 각 원인을 명확히 추적할 수 있게 됐다.
func (w *PaymentWorker) Process(ctx context.Context, paymentID string) error {
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
if !w.acquireLock(paymentID) {
cancel(ErrLockAcquisitionFailed)
return context.Cause(ctx)
}
if err := w.callPG(ctx, paymentID); err != nil {
cancel(fmt.Errorf("PG API failed: %w", err))
return context.Cause(ctx)
}
return nil
}
모니터링 대시보드에서 취소 원인별로 집계해서 어떤 실패 유형이 많은지 한눈에 파악할 수 있게 됐다. Go 1.20 업그레이드 후 가장 유용하게 쓰고 있는 기능이다.