Task.checkCancellation()과Task.isCancelled의 차이와 각각의 사용 시점은?withTaskCancellationHandler는 언제 필요한가요?
답변 ① — throw로 끊을 것인가, 내가 마무리할 것인가 🔥 핵심
전제부터 짚으면, Swift의 취소는 협력적(cooperative)입니다. 취소는 강제 중단이 아니라 "그만해 달라"는 플래그일 뿐이고, 코드가 스스로 확인해야 멈춥니다. 두 API는 그 확인 결과를 어떻게 처리하느냐에서 갈립니다.
Task.checkCancellation()— 취소됐으면CancellationError를 throw합니다. 즉시 중단하고 에러를 위로 전파하면 되는 경우에 씁니다. 이미throws컨텍스트라면 한 줄로 끝나 가장 간결합니다.Task.isCancelled— Bool을 반환합니다. 중단 전에 할 일이 남아 있을 때 씁니다. 지금까지 모은 부분 결과를 반환하거나, 정리 작업을 하거나, throw할 수 없는 non-throwing 컨텍스트일 때가 그렇습니다.
선택 기준은 간단합니다. 그냥 끊어도 되면 checkCancellation(), 끊기 전에 뭔가 해야 하면 isCancelled입니다. 어느 쪽이든 비싼 작업의 자연스러운 분기점(작업 전후)에 넣어 줍니다.
// checkCancellation: 취소면 throw로 즉시 탈출
func process(_ items: [Item]) async throws {
for item in items {
try Task.checkCancellation() // 취소 시 CancellationError
await handle(item)
}
}
// isCancelled: 지금까지 모은 부분 결과를 돌려주고 싶을 때
func loadAll(_ ids: [ID]) async -> [Item] {
var result: [Item] = []
for id in ids {
if Task.isCancelled { return result } // 여기까지만 반환(throw 안 함)
result.append(await load(id))
}
return result
}답변 ② — 잠들어 있는 동안 온 취소에 반응해야 할 때
withTaskCancellationHandler는 task가 suspend되어 멈춰 있는 동안 취소가 와도 즉시 부수효과를 실행해야 할 때 필요합니다. 대표적으로 URLSession task를 즉시 취소하거나, delegate·continuation 기반 작업을 취소 시점에 에러로 resume해서 매달린 task를 풀어 주는 경우입니다.
왜 isCancelled 폴링으로는 안 될까요? 폴링은 코드가 실행 중일 때만 확인할 수 있습니다. await에서 잠들어 있으면 확인할 코드 자체가 돌지 않으므로, 콜백 형태의 훅이 없으면 깨어날 때까지 아무 반응도 못 합니다. 콜백이 영영 안 오는 API라면 영원히 못 깨어납니다.
핸들러는 @Sendable이고 취소를 호출한 스레드에서 operation과 동시에 실행됩니다. 따라서 스레드 안전한 상태(actor·락·continuation)만 건드려야 합니다. 또 진입 시 이미 취소된 상태라면 즉시 실행되고, 최대 1회 호출되며, operation을 강제로 멈추지는 않습니다 — 여전히 코드가 isCancelled를 확인해야 합니다.
func download(_ url: URL) async throws -> Data {
let task = URLSession.shared.dataTask(with: url)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { cont in
// task 완료 시 cont.resume(...) 으로 연결
task.resume()
}
} onCancel: {
task.cancel() // @Sendable · 취소한 스레드에서 즉시 실행 · 최대 1회
// ← 이게 없으면 취소해도 다운로드가 계속 돈다
}
}쉽게 이해하기
취소는 "야, 이제 그만할래?"라고 물어보는 것이지, 하던 걸 억지로 뽑아버리는 게 아니에요. 그래서 내가 중간중간 물어봐야 멈춥니다. 아무도 안 물어보면 계속 일해요.
(질문 ①) 물어본 다음이 갈립니다. checkCancellation()은 그만이면 그 자리에서 손 놓고 나가버리는 것이고, isCancelled는 "알겠어, 그럼 여기까지만 정리하고 갈게"라며 하던 걸 마무리하는 거예요. 지금까지 받은 것만 화면에 보여주고 싶다면 두 번째를 써야겠죠.
(질문 ②) 그런데 내가 자고 있으면(다운로드를 기다리며 멈춰 있으면) 스스로 물어볼 수가 없어요. 이때 필요한 게 withTaskCancellationHandler, 즉 알람입니다. 취소가 오면 알람이 대신 울려서 "지금 당장 다운로드부터 꺼!"를 처리해 줘요. 알람이 없으면, 취소 버튼을 눌러도 다운로드는 끝날 때까지 계속 돕니다.
취소는 명령이 아니라 부탁이다. 깨어 있을 땐 내가 확인하고, 자고 있을 땐 알람이 대신 처리한다.