-
[SE-0528] Continuation — Safe and Performant Async ContinuationsSwift 2026. 5. 30. 11:19
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 SE-0528 — 안전하고 성능 좋은 Async Continuation, Continuation 타입 도입에 대해 정리해보겠습니다 🙋🏻

Intro
- Proposal: SE-0528
- Authors: Fabian Fett, Konrad Malawski
- Review Manager: Joe Groff
- Status: Accepted with revisions
Motivation
Continuation은 콜백 기반 API를 Swift Structured Concurrency로 연결하는 핵심 메커니즘입니다.
그런데 현재 개발자는 두 가지 옵션 중 하나를 골라야 하는 불편한 선택을 강요받고 있어요.
- UnsafeContinuation — 오버헤드는 없지만, 두 번 resume하면 undefined behavior, resume을 빠뜨리면 조용히 task가 영원히 leak됩니다 🐛
- CheckedContinuation — 런타임 bookkeeping으로 실수를 감지하지만, allocation과 atomic 연산 오버헤드가 발생합니다.
Continuation은 정확히 한 번만 resume해야 하는 "use-exactly-once" 값입니다.
이건 move-only 타입이 강제할 수 있는 계약이에요. 타입 시스템이 이 문제를 해결해야 합니다!
Proposed Solution
Continuation<Success: ~Copyable, Failure: Error>, 즉~Copyablestruct를 도입합니다. 세 가지 메커니즘으로 올바른 사용을 강제합니다.- Move-only semantics (~Copyable) — continuation을 복사할 수 없으므로, 두 곳에서 resume하는 것이 불가능합니다.
- consuming 메서드 — 모든 resume 메서드는 self를 consume해 두 번째 호출은 컴파일 에러가 됩니다.
- deinit 트랩 + discard self — resume 없이 continuation이 drop되면 fatalError. 정상 resume 시엔 discard self로 deinit을 억제해 오버헤드가 없어요.
Double-resume — 컴파일 에러로 해결!
actor LegacyBridge { func complete(with value: String) { if let continuation { continuation.resume(returning: value) // ✅ consumes continuation continuation.resume(returning: value) // ❌ compile error: // 'continuation' used after consuming use } } }Missing-resume — 런타임 트랩으로 포착!
actor LegacyBridge { func cancel() { // Bug: resume 없이 continuation을 지워버림 self.continuation = nil // 💥 runtime trap: "This continuation was dropped." } }
Detailed Design
Continuation 타입 정의
@frozen public struct Continuation<Success: ~Copyable, Failure: Error>: ~Copyable, Sendable { deinit { fatalError("The continuation was dropped without resuming.") } @inlinable public consuming func resume(returning value: consuming sending Success) { ... } @inlinable public consuming func resume(throwing error: Failure) { ... } }- Sendable — 다른 task/thread에서 resume할 수 있도록 isolation boundary를 넘을 수 있습니다.
- consuming — 각 resume 메서드가 self를 consume해 이후 사용이 불가능해집니다.
- sending Success — non-Sendable 값을 async task에 안전하게 전달할 수 있습니다.
- consuming Success — noncopyable 타입도 값으로 전달할 수 있습니다.
- discard self — 정상 resume 시 deinit을 억제해 오버헤드가 없습니다.
withContinuation — of: 와 throwing: 파라미터
// Before: 클로저 파라미터에 타입 어노테이션 필요 let data = await withCheckedContinuation { (continuation: UnsafeContinuation<Data, Never>) in bridge.store(continuation) } // After: 호출부에서 타입이 명확하게 보임 let data = await withContinuation(of: Data.self) { continuation in bridge.store(continuation) }throwing:파라미터는 SE-0413(typed throws)를 활용해 세 가지 경우를 하나의 함수로 통합합니다.// Non-throwing let data = await withContinuation(of: Data.self) { continuation in ... } // Typed throwing let data = try await withContinuation(of: Data.self, throwing: NetworkError.self) { continuation in ... } // Untyped throwing let data = try await withContinuation(of: Data.self, throwing: (any Error).self) { continuation in ... }CheckedContinuation으로 변환하기
콜백이 여러 개인 경우처럼 noncopyable Continuation을 사용할 수 없는 경우엔 CheckedContinuation으로 변환해 사용할 수 있습니다.
try await withContinuation(of: Int.self, throwing: (any Error).self) { c in let checked = CheckedContinuation(c) someLib.onSuccess { checked.resume(returning: $0) } // ✅ someLib.onFailure { checked.resume(throwing: $0) } // ✅ }이 때문에
CheckedContinuation과UnsafeContinuation은 deprecated되지 않고 계속 유지됩니다.동작 비교표
시나리오 UnsafeContinuation CheckedContinuation Continuation 정상 resume ✅ 동작 ✅ 동작 ✅ 동작 Double resume ⚠️ Undefined Behavior 💥 Runtime trap ❌ 컴파일 에러 Missing resume 😶 Silent hang ⚠️ Runtime warning 💥 Runtime trap 런타임 오버헤드 없음 allocation + atomic 없음
Implications on Adoption
Before After withCheckedContinuation { … } withContinuation(of: T.self) { … } withCheckedThrowingContinuation { … } withContinuation(of: T.self, throwing: (any Error).self) { … } CheckedContinuation<T, E> Continuation<T, E> 단,
Continuation은~Copyable이기 때문에, 클로저 캡처처럼 continuation을 암묵적으로 복사하는 패턴은 컴파일 에러가 됩니다.이 경우엔 기존
Checked/UnsafeContinuation을 사용해야 합니다.
Source Compatibility / ABI
- 이번 변경은 순수 additive 변경으로, 기존 코드에 영향을 주지 않습니다.
withContinuation과Continuation이라는 이름은 기존 표준 라이브러리 API와 충돌하지 않습니다.- ABI 변경도 없습니다.
Future Directions
~Discardable 프로토콜 도입
현재는 continuation이 drop될 때 런타임 트랩에 의존하고 있지만,
~Copyable타입을 위한 새로운 모드인~Discardable을 도입해 "반드시 명시적으로 consume해야 함"을 컴파일러가 강제하게 할 수 있어요.이렇게 되면 런타임 트랩마저 컴파일 에러로 격상될 수 있습니다.
Conclusion
드디어 오버헤드 없이 안전한 Continuation이 생겼습니다 🙌
기존에는 안전성(CheckedContinuation)과 성능(UnsafeContinuation) 사이에서 트레이드오프를 강요받았는데, move-only 타입의 강점을 활용해 double-resume은 컴파일 에러로, missing-resume은 런타임 트랩으로 잡아주면서 오버헤드는 UnsafeContinuation과 동일하게 유지됩니다.
Swift 타입 시스템의 힘으로 콜백 브리징의 고질적인 버그 두 가지를 완전히 해결한 우아한 변경이라고 생각합니다 😄
References
swift-evolution/proposals/0528-noncopyable-continuation.md at main · swiftlang/swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution
github.com
'Swift' 카테고리의 다른 글
[SE-0522] Source-Level Control Over Compiler Warnings (0) 2026.05.23 [SE-0523] Hashable conformance for UnownedTaskExecutor (1) 2026.05.15 [SE-0524] Add withTemporaryAllocation using Output(Raw)Span (1) 2026.05.09 [SE-0521] Improved Syntax for Optionals of Opaque and Existential Types (0) 2026.05.01 [SE-0520] Discardable result use in Task initializers (0) 2026.04.25