# SE-0504 Task Cancellation Shields로 정리 코드를 취소에서 지키기

- 게시: 2026.07.14
- 원문(HTML): https://trendbrief.news/articles/se-0504-task-cancellation-shields%EB%A1%9C-%EC%A0%95%EB%A6%AC-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%B7%A8%EC%86%8C%EC%97%90%EC%84%9C-%EC%A7%80%ED%82%A4%EA%B8%B0.html
- 발행: [트렌드브리프](https://trendbrief.news/)

> Swift 6.4에 구현된 SE-0504 Task Cancellation Shields는 withTaskCancellationShield로 감싼 정리 코드가 취소된 태스크 안에서도 끝까지 실행되게 한다. 동작 원리와 async defer 활용, 올바른 사용 범위를 정리했다.

Swift의 태스크 취소는 강제 종료가 아니라 협조적이다. 취소 신호가 와도 코드가 스스로 확인해 멈춰야 하는데, 여기서 골치 아픈 게 정리 코드다. 파일을 닫거나 트랜잭션을 되돌리는 마무리 작업까지 취소에 걸려 중간에 끊기면 리소스가 새거나 데이터가 어정쩡한 상태로 남는다. SE-0504 Task Cancellation Shields는 이 정리 구간만 취소를 못 보게 가려 주는 기능이다. 새로 들어온 `withTaskCancellationShield`로 코드 한 블록을 감싸면 그 안에서는 `Task.isCancelled`가 `false`가 되고, 마무리 작업이 끝까지 실행된다. Swift Evolution 제안 SE-0504에 따르면 이 기능은 2026년 7월 기준 Swift 6.4에 구현돼 있고, 제안을 쓴 사람은 콘라트 말라프스키(Konrad Malawski)다. 이 글은 Swift 6.4 이상에서 구조적 동시성을 쓰는 코드를 기준으로 방패의 동작과 올바른 사용 범위만 다룬다. 취소를 감지하고 처리하는 기본 동작 자체는 다루지 않는다.

## 취소 방패가 푸는 문제

구조적 동시성(async/await로 시작한 비동기 작업을 부모-자식 트리로 묶어 함께 관리하는 방식)에서 취소는 신호일 뿐이다. 부모 태스크가 취소되면 자식으로 신호가 퍼지지만, 실제로 멈추는 건 각 코드가 `Task.isCancelled`를 확인하거나 `Task.checkCancellation()`을 호출해 스스로 빠져나올 때다. 콜백을 순차적인 async/await 코드로 옮기는 과정은 [async/await로 순차 코드를 짜는 글](https://trendbrief.news/articles/%EC%8A%A4%EC%9C%84%ED%94%84%ED%8A%B8-async-await-%EC%BD%9C%EB%B0%B1%EC%97%90%EC%84%9C-%EC%88%9C%EC%B0%A8-%EC%BD%94%EB%93%9C%EB%A1%9C.html)에서 따로 정리했다.

문제는 이 협조적 취소가 정리 코드까지 건드릴 때다. 열어 둔 커넥션을 닫고, 임시 파일을 지우고, 반쯤 쓴 레코드를 롤백하는 작업은 태스크가 취소됐든 아니든 끝까지 돌아야 한다. 그런데 정리 로직 안에서 또 다른 비동기 함수를 `await`로 호출하면, 그 함수가 취소를 감지하고 `CancellationError`를 던지며 도중에 튀어나올 수 있다. 결국 절반만 정리된 채로 끝나 버린다. 그동안 개발자들은 취소 여부를 별도 플래그로 넘기거나, 정리를 취소와 무관한 별도 태스크로 떼어 내는 식으로 우회했다. 손이 많이 가고 실수하기 쉬운 방식이었다.

## withTaskCancellationShield는 무엇을 하나

취소된 태스크 안에서도 감싼 블록만큼은 취소를 못 본 척하게 만든다. 방패 안으로 들어가면 `Task.isCancelled`가 `false`를 돌려주고, 취소를 확인하는 API도 취소되지 않은 것처럼 동작한다. 그래서 정리 코드가 중간에 튕겨 나가지 않고 끝까지 실행된다.

```
// 태스크가 이미 취소된 상황
print(Task.isCancelled) // true

withTaskCancellationShield {
 print(Task.isCancelled) // false
 // 이 안에서는 취소가 관찰되지 않는다
}

print(Task.isCancelled) // true — 방패 밖에서는 취소가 다시 보인다
```

여기서 짚어야 할 핵심은 방패가 취소를 지우지 않는다는 점이다. 이름은 방패지만, 취소 신호를 없애는 게 아니라 그 구간 동안 관찰만 막는다. 블록을 빠져나오면 태스크는 여전히 취소된 상태이고, 바깥 코드는 그 사실을 정상적으로 다시 본다. 제안 문서 기준으로 이 API는 두 가지 형태를 제공한다. 하나는 동기 클로저를 감싸는 형태이고, 다른 하나는 `await`가 필요한 비동기 클로저를 감싸는 형태다. 정리 작업이 비동기 호출을 포함하면 후자를 쓰면 된다.

## async defer와 정리 코드 패턴

SE-0504가 특히 잘 맞는 자리는 `defer` 블록이다. 제안 SE-0504는 같은 흐름에서 나온 SE-0493(defer 블록 안에서 async 호출을 허용하는 변경)과 함께 쓰도록 설계됐다고 제안 문서는 설명한다. `defer`는 함수를 어떻게 빠져나가든 마지막에 반드시 실행되는 정리 구문인데, 그 안에서 비동기 정리를 하면서 취소에 흔들리지 않게 하려면 방패로 한 번 더 감싸면 된다.

```
func exportReport() async throws {
 let file = try await openFile()
 defer {
  // 취소돼도 파일 닫기는 반드시 끝낸다
  await withTaskCancellationShield {
   await file.close()
  }
 }
 try await writeContents(to: file)
}
```

본문 작업인 `writeContents`가 취소로 중단돼도 `defer`는 실행되고, 그 안의 `file.close()`는 방패 덕분에 취소를 보지 않고 끝까지 돌아간다. 열어 둔 자원을 확실히 닫는 이 패턴이 방패가 노리는 가장 전형적인 쓰임새다.

## 언제 쓰고 언제 피해야 하나

이미 시작한 짧은 마무리나 롤백 작업을 온전히 끝내는 데만 쓴다. 방패는 정리를 지키는 도구이지, 취소를 무력화하는 스위치가 아니다. 네트워크 다운로드나 대량 계산처럼 오래 걸리는 본 작업을 방패로 감싸면, 사용자가 취소를 눌러도 반응하지 않는 먹통 코드가 된다. 취소에 협조하지 않는 코드는 구조적 동시성이 주는 이점을 그대로 깎아 먹는다.

판단 기준은 단순하다. 그 블록이 몇 밀리초에서 길어야 수백 밀리초 안에 끝나는 정리성 작업이면 방패가 어울리고, 초 단위로 늘어질 수 있는 작업이면 방패 밖에서 취소를 존중하도록 짜야 한다. 디버깅할 때 현재 코드가 방패 안에 있는지 확인하고 싶다면, 제안 문서에 따르면 `Task.hasActiveCancellationShield` 속성으로 방패 활성 여부를 조회할 수 있다. 참고로 이 방패라는 개념은 파이썬 진영의 Trio 동시성 라이브러리에서 처음 나온 용어를 Swift가 가져온 것이라고 제안은 밝힌다. 더 정확한 API 형태와 배경은 [Swift Evolution의 SE-0504 제안 문서](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0504-task-cancellation-shields.md)와 [Swift 공식 사이트](https://www.swift.org)에서 확인할 수 있다.

## 관련 글

- [SE-0502, 멤버와이즈 이니셜라이저에서 빠지는 private 프로퍼티](https://trendbrief.news/articles/se-0502-%EB%A9%A4%EB%B2%84%EC%99%80%EC%9D%B4%EC%A6%88-%EC%9D%B4%EB%8B%88%EC%85%9C%EB%9D%BC%EC%9D%B4%EC%A0%80%EC%97%90%EC%84%9C-%EB%B9%A0%EC%A7%80%EB%8A%94-private-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0.md)
- [SE-0532로 배우는 스위프트 에볼루션 제안 읽기](https://trendbrief.news/articles/se-0532%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EC%8A%A4%EC%9C%84%ED%94%84%ED%8A%B8-%EC%97%90%EB%B3%BC%EB%A3%A8%EC%85%98-%EC%A0%9C%EC%95%88-%EC%9D%BD%EA%B8%B0.md)
- [SE-0508: 배열·딕셔너리에 후행 클로저를 허용하는 스위프트 제안](https://trendbrief.news/articles/se-0508-%EB%B0%B0%EC%97%B4-%EB%94%95%EC%85%94%EB%84%88%EB%A6%AC%EC%97%90-%ED%9B%84%ED%96%89-%ED%81%B4%EB%A1%9C%EC%A0%80%EB%A5%BC-%ED%97%88%EC%9A%A9%ED%95%98%EB%8A%94-%EC%8A%A4%EC%9C%84%ED%94%84%ED%8A%B8-%EC%A0%9C%EC%95%88.md)
- [WWDC26 MetricKit, iOS 27의 Swift 새 API와 상태별 측정](https://trendbrief.news/articles/wwdc26-metrickit-ios-27%EC%9D%98-swift-%EC%83%88-api%EC%99%80-%EC%83%81%ED%83%9C%EB%B3%84-%EC%B8%A1%EC%A0%95.md)
- [에이전트 브라우저, 작동 원리와 개발 프레임워크 선택](https://trendbrief.news/articles/%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%9E%91%EB%8F%99-%EC%9B%90%EB%A6%AC%EC%99%80-%EA%B0%9C%EB%B0%9C-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%84%A0%ED%83%9D.md)