Go 1.21 Context WithoutCancel 사용기

문제 상황

사용자 요청 처리 중 외부 API 호출이 실패하면 DB에 실패 로그를 남기는 로직이 있었다. 그런데 클라이언트가 요청을 취소하면 부모 context가 cancel되면서 로그 저장도 함께 중단되는 문제가 있었다.

func HandleRequest(ctx context.Context) error {
    err := callExternalAPI(ctx)
    if err != nil {
        // ctx가 이미 cancel된 상태면 저장 실패
        saveErrorLog(ctx, err) 
        return err
    }
    return nil
}

Go 1.21의 해결책

8월에 릴리즈된 Go 1.21에서 context.WithoutCancel이 추가되었다. 부모 컨텍스트의 값은 유지하면서 취소 신호는 전파되지 않는 새 컨텍스트를 만들어준다.

func HandleRequest(ctx context.Context) error {
    err := callExternalAPI(ctx)
    if err != nil {
        // 부모가 cancel되어도 로그는 저장
        logCtx := context.WithoutCancel(ctx)
        saveErrorLog(logCtx, err)
        return err
    }
    return nil
}

기존 방식과 비교

이전에는 context.Background()context.TODO()를 사용했는데, 이러면 부모의 값(trace ID, user ID 등)을 잃어버렸다. 값을 수동으로 복사하는 헬퍼 함수를 만들어 쓰기도 했지만 번거로웠다.

// 기존 방식
logCtx := context.Background()
logCtx = context.WithValue(logCtx, "trace_id", ctx.Value("trace_id"))
logCtx = context.WithValue(logCtx, "user_id", ctx.Value("user_id"))

WithoutCancel은 이런 보일러플레이트를 제거해준다.

적용 사례

  • 감사 로그 저장
  • 메트릭 전송
  • 정리(cleanup) 작업
  • graceful shutdown 시 진행 중인 작업 완료

단, 무한정 대기하지 않도록 별도 타임아웃은 설정해야 한다.

logCtx, cancel := context.WithTimeout(
    context.WithoutCancel(ctx), 
    5*time.Second,
)
defer cancel()
saveErrorLog(logCtx, err)

Go 1.21로 업그레이드 후 이 패턴을 여러 곳에 적용했고, 로그 유실 문제가 해결되었다.