-
Notifications
You must be signed in to change notification settings - Fork 0
Meet AsyncSequence ‐ WWDC 2021
AsyncSequence는 Swift의 새로운 기능으로, 시간에 걸쳐 비동기적으로 값들을 순회할 수 있게 해줍니다.
다음은 AsyncSequence의 강력함을 보여주는 간단한 도구입니다. URL 엔드포인트에서 최근 지진 데이터를 가져오는데, 일반적으로 다운로드는 시간이 걸리는 비동기 작업입니다. 하지만 여기서는 모든 것이 다운로드될 때까지 기다리지 않고, 데이터가 수신되는 대로 표시하고 싶습니다.
@main
struct QuakesTool {
static func main() async throws {
let endpointURL = URL(string: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.csv")!
// skip the header line and iterate each one
// to extract the magnitude, time, latitude and longitude
for try await event in endpointURL.lines.dropFirst() {
let values = event.split(separator: ",")
let time = values[0]
let latitude = values[1]
let longitude = values[2]
let magnitude = values[4]
print("Magnitude \(magnitude) on \(time) at \(latitude) \(longitude)")
}
}
}데이터는 comma-separated 텍스트로 포맷되어 있어서 각 줄이 완전한 데이터 행입니다. async sequence of lines는 각 줄을 수신하는 대로 emit하기 때문에, 정말 큰 다운로드가 앞에 있더라도 스니펫은 매우 반응적으로 느껴집니다.
"The most awesome part about it is that you can use the same things that you're familiar with using from regular sequences in this new async context." (가장 멋진 점은 일반 시퀀스에서 익숙하게 사용하던 것들을 이 새로운 async 컨텍스트에서 동일하게 사용할 수 있다는 것입니다.)
새로운 for-await-in 구문으로 순회할 수 있고, map, filter, reduce 같은 함수들이나 이 샘플처럼 dropFirst 함수로 값들을 조작할 수 있습니다.
async/await의 핵심 포인트를 복습해봅시다.
Async 함수는 await 키워드를 사용하여 콜백 없이 동시성 코드를 작성할 수 있게 해줍니다. async 함수를 호출하면 suspend되었다가 값이나 에러가 생성될 때 resume됩니다.
AsyncSequence는 각 요소에서 suspend하고, 기본 iterator가 값을 생성하거나 throw할 때 resume합니다.
"Basically, as the name implies, they're just like regular sequences but with a few key differences. Namely, each element is delivered asynchronously." (기본적으로 이름이 암시하듯이, 일반 시퀀스와 같지만 몇 가지 핵심적인 차이점이 있습니다. 즉, 각 요소가 비동기적으로 전달됩니다.)
비동기 전달이기 때문에 실패 가능성이 있습니다. 일부 async sequence는 throw하지만, 실패가 옵션이 아닌 경우 throw하지 않는 것도 있습니다. throw하는 함수처럼, 컴파일러가 순회하거나 조합할 때 에러가 처리되도록 도와줍니다.
일반적으로 비동기 시퀀스는 시간에 걸쳐 값을 생성하는 방법에 대한 설명입니다. async sequence는 0개 이상의 값을 가질 수 있고, 시퀀스처럼 iterator에서 nil을 반환하여 완료를 알립니다. 에러가 발생하면 그것도 async sequence가 터미널 상태에 있는 지점이며, 에러 후에는 iterator의 next에 대한 후속 호출에 nil을 반환합니다.
일반적인 for-in 루프로 시작해봅시다:
for quake in quakes {
if quake.magnitude > 3 {
displaySignificantEarthquake(quake)
}
}컴파일러가 이 순회를 어떻게 처리하는지 살펴봅시다. 이것이 대략 컴파일러가 이전 코드를 빌드할 때 하는 일입니다:
var iterator = quakes.makeIterator()
while let quake = iterator.next() {
if quake.magnitude > 3 {
displaySignificantEarthquake(quake)
}
}먼저 iterator 변수를 생성한 다음 while 루프를 사용하여 next가 호출될 때 iterator가 생성하는 모든 quake를 가져옵니다.
새로운 async/await 기능을 사용하려면 한 가지 약간의 변경만 하면 됩니다. next 함수를 비동기 함수로 바꾸는 것만큼 간단합니다:
var iterator = quakes.makeAsyncIterator()
while let quake = await iterator.next() {
if quake.magnitude > 3 {
displaySignificantEarthquake(quake)
}
}이제 다음 quake를 await하여 순회가 Swift 동시성에 참여할 수 있습니다.
루프가 async sequence에 있었다면 어땠을지 되돌아봅시다. 앞서 언급했듯이, async sequence에서 각 항목을 await해야 합니다. 이것이 새로운 for-await-in 구문에 반영됩니다:
for await quake in quakes {
if quake.magnitude > 3 {
displaySignificantEarthquake(quake)
}
}"This all means that if you know how to use Sequence, you already have a good idea on how to use AsyncSequence." (이 모든 것은 Sequence 사용법을 알고 있다면, AsyncSequence 사용법에 대해서도 이미 좋은 아이디어가 있다는 것을 의미합니다.)
async sequence를 활용하는 몇 가지 방법이 있습니다. 방금 다룬 것처럼 새로운 for-await-in 구문을 사용할 수 있고, async sequence가 throw하면 새로운 for-try-await-in 구문을 사용할 수 있습니다.
이것은 비동기적으로 생성된 값들을 클로저를 다룰 필요 없이 쉽게 순회할 수 있게 해주며, 이미 익숙한 구문을 사용하여 순회할 수 있습니다. break와 continue 같은 것들도 그냥 작동합니다.
async sequence를 순회할 때, breaking은 루프 내부에서 순회를 조기에 종료하는 좋은 방법입니다. 이것은 일반 시퀀스처럼 작동합니다. 여기서는 quake에 위치 데이터가 없을 때 break합니다:
for await quake in quakes {
if quake.location == nil {
break
}
if quake.magnitude > 3 {
displaySignificantEarthquake(quake)
}
}건너뛰고 싶은 값이 있으면 continue를 사용할 수 있습니다. 이 경우, depth가 특정 값보다 크면 건너뛰고 다음 quake를 await합니다:
for await quake in quakes {
if quake.depth > 5 {
continue
}
if quake.magnitude > 3 {
displaySignificantEarthquake(quake)
}
}다운로드에서의 다음 순회는 이전과 같이 작동하지만, 이 경우 소스가 에러를 throw할 수 있습니다. throw하는 함수처럼, 순회되는 AsyncSequence가 throw할 수 있을 때 각 요소를 처리하기 위해 try가 필요합니다:
do {
for try await quake in quakeDownload {
...
}
} catch {
...
}throw하는 함수처럼, 컴파일러가 try를 빠뜨렸을 때 감지하고 실수를 수정하는 Fix-it을 제공합니다. 이것은 async sequence가 에러를 생성할 수 있을 때, 언어가 그 에러를 throw하거나 catch하도록 강제하기 때문에 항상 안전하다는 것을 의미합니다.
두 번째 순회는 첫 번째 루프의 순회 후에 순차적으로 실행됩니다. 코드를 순차적으로 실행하는 것이 항상 원하는 것은 아닙니다. 다른 진행 중인 작업과 동시에 순회를 실행하는 것이 유용하다면, 순회를 캡슐화하는 새로운 async task를 생성할 수 있습니다.
"This can be useful when you know the async sequences you're using might run indefinitely." (사용 중인 async sequence가 무한정 실행될 수 있다는 것을 알 때 유용할 수 있습니다.)
시퀀스가 무한정일 가능성이 있지만 발생하는 것은 상당히 덜 일반적입니다. 하지만 비동기 동작의 세계에서는 훨씬 더 일반적이며 사용할 때 고려해야 할 사항입니다. 다행히 Swift의 동시성 기능이 이것을 정말 쉽고 안전하게 만들어줍니다.
이것은 외부에서 순회를 취소하고 싶을 때도 정말 유용합니다. 두 순회를 동시에 실행하고 나중에 종료할 수 있습니다:
let iteration1 = Task {
for await quake in quakes {
...
}
}
let iteration2 = Task {
do {
for try await quake in quakeDownload {
...
}
} catch {
...
}
}
//... later on
iteration1.cancel()
iteration2.cancel()무한정일 수 있는 순회 작업의 범위를 컨테이너의 수명으로 지정하는 것이 task로 꽤 쉽습니다.
macOS Monterey, iOS 15, tvOS 15, watchOS 8부터 사용 가능한 AsyncSequence API들을 살펴봅시다.
파일에서 읽기는 종종 비동기 동작의 주요 사용 사례입니다. FileHandle은 이제 그 FileHandle에서 비동기 바이트 시퀀스에 접근할 수 있는 새로운 bytes 프로퍼티를 가지고 있습니다. 이것은 비동기 바이트 시퀀스를 줄로 변환하는 AsyncSequence의 새로운 extension과 함께 사용할 수 있습니다:
for try await line in FileHandle.standardInput.bytes.lines {
...
}파일 다루기가 너무 일반적이어서 URL이 bytes와 lines 둘 다에 대한 접근자를 가져야 한다고 결정했습니다. 이것이 초기 예제에서 사용한 동일한 API입니다. 파일이나 네트워크에서 내용의 줄들의 AsyncSequence를 반환하는 URL의 편의 프로퍼티입니다:
let url = URL(fileURLWithPath: "/tmp/somefile.txt")
for try await line in url.lines {
...
}"I'm certain this will make a number of previously really complicated tasks easy and safe." (이것이 이전에 정말 복잡했던 많은 작업들을 쉽고 안전하게 만들 것이라고 확신합니다.)
때때로 네트워크에서 데이터를 가져오려면 응답과 인증에 대한 더 많은 제어가 필요합니다. 그래서 URLSession은 이제 URL이나 URLRequest가 주어지면 비동기 바이트 시퀀스를 가져오는 bytes 함수를 가지고 있습니다:
let (bytes, response) = try await URLSession.shared.bytes(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 /* OK */
else {
throw MyNetworkingError.invalidServerResponse
}
for try await byte in bytes {
...
}파일과 네트워크만이 AsyncSequence에 적합한 것은 아닙니다. Notifications도 이제 새로운 notifications API로 await할 수 있습니다. 순회만이 AsyncSequence와 상호작용하는 유일한 방법은 아닙니다:
let center = NotificationCenter.default
let notification = await center.notifications(named: .NSPersistentStoreRemoteChange).first {
$0.userInfo[NSStoreUUIDKey] == storeUUID
}이 예제에서는 매칭하는 store UUID를 가진 원격 변경에 대한 첫 번째 notification을 await하고 있습니다. firstWhere 같은 메서드를 notifications async sequence와 함께 사용하면, 이전에 복잡한 로직을 표현하던 코드가 이제 간결하고 읽기 쉬워지는 정말 멋진 새로운 디자인 패턴이 가능합니다.
async sequence에서 값을 비동기적으로 조작하기 위한 완전히 새로운 API들이 있습니다. 이것들은 Sequence에서 사용 가능한 동일한 함수들이기 때문에 꽤 익숙할 것입니다. dropFirst와 firstWhere를 이미 다뤘지만, 그것들 외에도 훨씬 더 많습니다. Sequence에서 사용할 수 있다고 생각하는 거의 모든 것이 이제 AsyncSequence와 작업하기 위한 비동기 대응물을 가지고 있습니다.
이제 "새로운 API들은 정말 멋지고 구문도 정말 깔끔한데, 어떻게 나만의 async sequence를 만들 수 있을까?"라고 생각할 수 있습니다.
async sequence를 구현하는 몇 가지 방법이 있지만, 기존 코드를 어떻게 적응시킬 수 있는지에 집중하겠습니다. 특히, AsyncSequence와 정말 잘 작동하는 몇 가지 디자인 패턴이 있고, 이미 가지고 있는 것을 이 새로운 개념과 상호작용하게 만드는 훌륭한 기능들이 있습니다.
"Pretty much anything that does not need a response back and is just informing of a new value that occurs can be a prime candidate for making an async sequence." (응답이 필요 없고 단지 발생하는 새로운 값을 알리기만 하는 거의 모든 것이 async sequence를 만드는 주요 후보가 될 수 있습니다.)
이러한 디자인 패턴들은 정말 일반적이고 오늘날 앱에 이미 몇 개 있을 것입니다.
이것은 일반적인 핸들러 패턴의 예입니다. 핸들러 프로퍼티와 start, stop 메서드를 가진 클래스입니다. AsyncSequence의 완벽한 후보처럼 보입니다:
class QuakeMonitor {
var quakeHandler: (Quake) -> Void
func startMonitoring()
func stopMonitoring()
}기존 사용법은 모니터를 생성하고, 값을 받을 핸들러를 할당한 다음, quake가 핸들러로 보내질 수 있도록 모니터를 시작하는 것일 수 있습니다. 나중에 생성되는 이벤트를 취소하기 위해 모니터가 중지될 수 있습니다.
동일한 인터페이스를 사용하여 새로운 AsyncStream 타입에 사용법을 적응시킬 수 있습니다. 사용하는 데 약간의 코드만 필요하고 async sequence를 빌드할 수 있게 해줍니다:
let quakes = AsyncStream(Quake.self) { continuation in
let monitor = QuakeMonitor()
monitor.quakeHandler = { quake in
continuation.yield(quake)
}
continuation.onTermination = { @Sendable _ in
monitor.stopMonitoring()
}
monitor.startMonitoring()
}
let significantQuakes = quakes.filter { quake in
quake.magnitude > 3
}
for await quake in significantQuakes {
...
}AsyncStream을 생성할 때, element 타입과 construction 클로저가 지정됩니다. 클로저는 값을 여러 번 yield하거나, finish하거나, 종료를 처리할 수 있는 continuation을 가집니다.
이 경우:
- 모니터를 construction 클로저 내부에서 생성할 수 있습니다
- 핸들러는 continuation에 quake를 yield하도록 할당할 수 있습니다
-
onTermination은 취소와 정리를 처리할 수 있습니다 - 그런 다음 모니터링을 시작할 수 있습니다
이전에 가지고 있던 동일한 모니터 코드가 async stream의 construction 내에 쉽게 캡슐화될 수 있습니다. 이것은 모든 사용 위치에서 동일한 로직을 복제할 필요성을 줄여줍니다.
filter 같은 강력한 변환 함수들과 새로운 for-await-in 구문을 사용할 수 있습니다. 이것은 모든 것이 한 곳에 래핑되어 있기 때문에 기록 유지에 대해 걱정할 필요 없이 코드의 의도에 집중할 수 있게 해줍니다.
AsyncStream으로 나만의 async sequence를 만드는 데 많은 유연성이 있습니다. 이것은 정말 하나의 예일 뿐이고 자신의 코드에서 적응시킬 수 있는 수많은 다른 것들이 있을 것입니다.
AsyncStream은 기존 코드를 async sequence로 만드는 훌륭한 방법입니다. 안전성, 순회, 취소처럼 async sequence에서 기대하는 모든 것을 처리하지만, 버퍼링도 처리합니다.
AsyncStream은 나만의 async sequence를 빌드하는 견고한 방법이며, 생성되는 요소의 유일한 소스가 construction에서 오기 때문에 자신의 API에서 적합한 반환 타입입니다.
에러가 throw되는 것을 표현해야 한다면? 그것을 위한 타입이 있습니다! AsyncThrowingStream은 AsyncStream과 같지만 에러를 처리할 수 있습니다. AsyncStream과 동일한 유연성과 안전성을 제공하지만 순회에서 throw하여 실패를 처리할 수 있습니다.
참고: [minsone.github.io - AsyncStream, AsyncThrowingStream 알아보기](https://minsone.github.io/swift-concurrency-asyncstream-AsyncThrowingStream)
AsyncStream을 생성하는 방법에는 Continuation 방식과 Unfolding 방식 두 가지가 있습니다.
외부 이벤트(Timer, 콜백 등)에서 값을 받아 스트림에 전달할 때 사용(Continuation을 직접 호출):
let counter = AsyncStream<String> { continuation in
var countdown = 3
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
guard countdown > 0 else {
timer.invalidate()
continuation.yield("🎉 Hello")
continuation.finish()
return
}
continuation.yield("\(countdown)...")
countdown -= 1
}
}Continuation을 직접 호출하지 않고 클로저에서 값을 반환하는 방식. nil을 반환하면 스트림이 종료됩니다:
var countdown = 3
let counter = AsyncStream<String> {
try? await Task.sleep(nanoseconds: 1_000_000_000)
defer { countdown -= 1 }
switch countdown {
case (1...): return "\(countdown)..."
case 0: return "🎉 Hello"
default: return nil // nil 반환 시 스트림 종료
}
}| 방식 | 에러 전달 방법 |
|---|---|
| Continuation | continuation.finish(throwing: error) |
| Unfolding | throw error |
AsyncSequence는 시간에 걸쳐 비동기적으로 값들을 순회할 수 있게 해주는 프로토콜입니다. 일반 Sequence처럼 작동하지만, 각 요소가 비동기적으로 전달됩니다. for-await-in 구문으로 순회하며, map, filter, reduce 등 익숙한 시퀀스 연산을 그대로 사용할 수 있습니다. AsyncStream을 통해 기존 콜백 기반 코드를 async sequence로 쉽게 변환할 수 있습니다.
| 키워드 | 설명 |
|---|---|
AsyncSequence |
비동기 시퀀스 프로토콜 |
for-await-in |
비동기 순회 구문 |
for-try-await-in |
throw 가능한 비동기 순회 구문 |
AsyncStream |
async sequence를 쉽게 생성하는 타입 |
AsyncThrowingStream |
에러 처리 가능한 AsyncStream |
continuation.yield() |
AsyncStream에서 값 전달 |
onTermination |
종료/취소 처리 핸들러 |
// 1. 기본 AsyncSequence 순회
for await quake in quakes {
if quake.magnitude > 3 {
displaySignificantEarthquake(quake)
}
}
// 2. throw 가능한 AsyncSequence
do {
for try await line in url.lines {
print(line)
}
} catch {
print("Error: \(error)")
}
// 3. AsyncStream으로 기존 콜백 패턴 변환
let quakes = AsyncStream(Quake.self) { continuation in
let monitor = QuakeMonitor()
monitor.quakeHandler = { quake in
continuation.yield(quake) // 값 전달
}
continuation.onTermination = { @Sendable _ in
monitor.stopMonitoring() // 정리
}
monitor.startMonitoring()
}Sequence는 요소들에 대한 동기적 순차 접근을 제공합니다.
next()호출 시 즉시 값을 반환합니다.AsyncSequence는 요소들에 대한 비동기적 순차 접근을 제공합니다.
next()가async이므로 값이 준비될 때까지 suspend했다가 resume됩니다. 또한 에러를 throw할 수 있습니다.
| 구분 | Sequence | AsyncSequence |
|---|---|---|
| Iterator 생성 | makeIterator() |
makeAsyncIterator() |
| 다음 요소 | next() -> Element? |
next() async throws -> Element? |
| 순회 구문 | for-in |
for await-in / for try await-in
|
| 에러 처리 | 불가능 | 가능 (throws) |
AsyncStream은 AsyncSequence 프로토콜을 준수하는 구체적인 struct입니다. 기존 콜백/델리게이트 기반 API를 async/await 패턴으로 변환할 때 사용합니다.
continuation.yield()로 값을 전달하고,onTermination으로 정리 작업을 처리할 수 있어서, AsyncSequence를 직접 구현하는 것보다 훨씬 간편합니다.
Task로 감싸서 외부에서 취소 가능하게 만듭니다.
task.cancel()로 순회를 종료할 수 있습니다.
- AsyncStream: 에러를 throw하지 않는 async sequence 생성
- AsyncThrowingStream: 에러를 throw할 수 있는 async sequence 생성
- Continuation 방식: 외부 이벤트에서
yield()로 값 전달,finish()로 종료- Unfolding 방식: 클로저에서 값 반환,
nil반환 시 종료 (더 간단한 경우에 적합)
- [WWDC 2021 - Meet AsyncSequence](https://developer.apple.com/videos/play/wwdc2021/10058/)
- [Apple Documentation - Sequence](https://developer.apple.com/documentation/swift/sequence/)
- [Apple Documentation - AsyncSequence](https://developer.apple.com/documentation/swift/asyncsequence/)
- [Apple Documentation - AsyncStream](https://developer.apple.com/documentation/swift/asyncstream/)
- [minsone.github.io - AsyncStream, AsyncThrowingStream 알아보기](https://minsone.github.io/swift-concurrency-asyncstream-AsyncThrowingStream)