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로 업그레이드 후 이 패턴을 여러 곳에 적용했고, 로그 유실 문제가 해결되었다.