Go 1.23 Context.WithoutCancel 활용기

배경

API 서버에서 요청이 취소되더라도 로그 전송, 메트릭 기록 같은 후처리 작업은 완료되어야 했다. 기존에는 context.Background()를 새로 생성했는데, 부모 컨텍스트의 값(traceID 등)을 상속받지 못하는 문제가 있었다.

// 기존 방식
func handleRequest(ctx context.Context) {
    // ... 메인 로직
    
    // 로그 전송 시 traceID 손실
    go sendLog(context.Background(), data)
}

Go 1.21+ 해결 방법

context.WithoutCancel을 사용하면 부모의 값은 유지하면서 취소 신호만 분리할 수 있다.

func handleRequest(ctx context.Context) error {
    result, err := processMainTask(ctx)
    if err != nil {
        return err
    }
    
    // 부모가 취소되어도 로그는 전송
    detachedCtx := context.WithoutCancel(ctx)
    go func() {
        sendLog(detachedCtx, result)
        recordMetrics(detachedCtx, result)
    }()
    
    return nil
}

주의사항

  1. 고루틴 누수 가능성: 취소되지 않으므로 타임아웃은 별도 설정 필요
detachedCtx := context.WithoutCancel(ctx)
logCtx, cancel := context.WithTimeout(detachedCtx, 5*time.Second)
defer cancel()
  1. 남용 금지: 대부분 작업은 원래 컨텍스트를 따라야 한다. 정말 필요한 곳에만 사용

  2. Go 버전 확인: 1.21 미만이면 polyfill 필요

func withoutCancel(ctx context.Context) context.Context {
    return &detachedContext{parent: ctx}
}

결과

요청 취소 시에도 observability 데이터가 누락되지 않게 되었고, traceID 연결도 유지되어 디버깅이 수월해졌다. 다만 무분별한 사용을 막기 위해 팀 내에서 사용처를 문서화했다.