Swift 6.4의 async defer도 취소된 작업에서 정리를 보장하지 않는다
Swift 6.4: Async defer doesn't make cleanup cancellation-safe
핵심 요약
- Swift 6.4에서는 defer 안에서 await할 수 있지만, 비동기 정리 작업은 호출 작업의 취소 상태를 그대로 관찰하므로 필요한 작업이 반드시 끝난다는 뜻은 아니다.
- 예제의 flush()는 먼저 Task.checkCancellation()을 호출한다. 작업이 이미 취소됐다면 이 지점에서 오류가 발생해 버퍼를 비우지 못한다.
- close()는 flush()의 오류를 잡은 뒤에도 isClosed를 true로 설정한다. 따라서 닫기 처리가 완료된 것처럼 보여도 버퍼에 남은 데이터가 파일에 기록되지 않을 수 있다.
도입
Swift 6.4는 defer 블록 안에서 await를 허용한다. 하지만 defer가 실행된다는 사실만으로 비동기 정리 작업의 완료까지 보장되지는 않는다. 보고서 내보내기 도중 사용자가 작업을 취소하면, 취소를 인식하는 flush()가 오류로 끝날 수 있다. 정리 코드가 오류를 잡고 닫기 상태를 설정해도 버퍼 데이터는 기록되지 않은 채 남을 수 있다.
상세 요약
async defer와 취소는 별개의 문제
Swift 6.4에서는 defer 안에서 비동기 정리 코드를 await할 수 있다. 그러나 defer가 실행된다는 사실과 그 안의 비동기 작업이 성공적으로 끝난다는 사실은 다르다. 정리 코드는 호출한 작업의 취소 상태를 그대로 관찰한다. 취소된 작업에서 수행되는 정리가 취소에 민감한 API를 호출하면, 정리 절차가 중간에 실패할 수 있다. Swift의 defer가 스코프 종료 시 정리 코드를 실행하는 방식은 [Swift defer로 스코프 종료 시 정리 작업 실행하기]에서 다룬다. 이 기사에서 짚는 문제는 비동기 정리의 실행 자체가 아니라, 정리 중 호출된 작업이 취소를 만나 어떻게 끝나는지다.
예제에서 데이터가 빠지는 순서
기사의 BufferedFileWriter는 기록할 행을 buffer에 모으고, flush()가 성공하면 이를 flushed로 옮긴다. flush()의 순서는 다음과 같다.
- 먼저
Task.checkCancellation()을 호출한다. - 취소되지 않았다면 50밀리초 동안
Task.sleep을 실행한다. - 두 단계가 모두 성공한 뒤에 버퍼 내용을
flushed에 추가하고buffer를 비운다.
Task.sleep은 작업이 취소되면 오류를 던진다. 따라서 flush()는 취소 확인이나 대기 중 오류가 발생하면 마지막 데이터 이동 단계에 도달하지 못한다.
close()는 flush()를 호출하고 오류를 잡아 출력한 다음 isClosed를 true로 설정한다. 호출 작업이 이미 취소된 상태라면 flush()의 첫 취소 확인에서 오류가 나고, close()는 이를 처리한 뒤 닫힌 상태를 표시한다. 이때 메서드는 반환되지만 버퍼는 비워지지 않는다. 닫기 처리가 끝났다는 표시가 데이터 기록 완료를 의미하지 않는 이유다.
적용 범위와 주의점
예제의 flush()는 취소 확인과 Task.sleep으로 취소 가능한 비동기 I/O를 흉내 낸다. 원문은 실제 파일 API의 동작은 다를 수 있다고 한정한다. 반면 Task.sleep이나 checkCancellation을 사용하는 네트워크 업로드와 URLSession 기반 작업 등은 같은 방식으로 취소에 반응할 수 있다고 설명한다.
따라서 defer에 비동기 정리를 넣었다는 사실만으로 작업 취소 뒤에도 flush가 반드시 완료된다고 볼 수 없다. 정리에서 끝까지 수행해야 할 작업이 있다면 취소에 노출되지 않도록 보호해야 한다. 다만 제공된 본문은 그 보호 방법의 구체적인 코드나 API를 설명하기 전에 끊겨 있으므로, 여기서는 구현 방식을 추가하지 않는다.
결론
핵심은 defer의 실행 여부와 정리 작업의 성공 여부를 구분하는 것이다. 예제에서는 close()가 호출되고 정상적으로 반환되더라도 flush()가 취소 오류로 중단되면 데이터가 버퍼에 남는다. 따라서 async defer만으로 취소 시 완료해야 하는 작업을 보호할 수는 없다. 원문은 반드시 끝나야 하는 작업을 취소로부터 차단해야 한다고 설명하지만, 제공된 본문에는 그 구체적인 구현 방법이 포함돼 있지 않다.