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
}
주의사항
- 고루틴 누수 가능성: 취소되지 않으므로 타임아웃은 별도 설정 필요
detachedCtx := context.WithoutCancel(ctx)
logCtx, cancel := context.WithTimeout(detachedCtx, 5*time.Second)
defer cancel()
-
남용 금지: 대부분 작업은 원래 컨텍스트를 따라야 한다. 정말 필요한 곳에만 사용
-
Go 버전 확인: 1.21 미만이면 polyfill 필요
func withoutCancel(ctx context.Context) context.Context {
return &detachedContext{parent: ctx}
}
결과
요청 취소 시에도 observability 데이터가 누락되지 않게 되었고, traceID 연결도 유지되어 디버깅이 수월해졌다. 다만 무분별한 사용을 막기 위해 팀 내에서 사용처를 문서화했다.