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 업그레이드 후 가장 유용하게 쓰고 있는 기능이다.