WWDC

WWDC 2021 - Meet AsyncSequence 정리

Goniii 2025. 9. 8. 21:06

https://developer.apple.com/videos/play/wwdc2021/10058/

 

Meet AsyncSequence - WWDC21 - Videos - Apple Developer

Iterating over a sequence of values over time is now as easy as writing a “for” loop. Find out how the new AsyncSequence protocol enables...

developer.apple.com

 

 

목차 

  1. What is AsyncSequence?
  2. Usage and APIs
  3. Adopting AsyncSequence

 


1. What is AsyncSequence? (0: 36)

all_month.csv

아래는 최근의 지진 정보 다운로드하는 endpoint이다

@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)")
        }
    }
}

보통 다운로드 작업은 시간이 걸릴 수 있는 비동기 작업이지만,

이 경우에는 모든 데이터를 다 받을 때까지 기다리지 않고 데이터를 받는 대로 표시하고 싶음

→ async/await 기능을 사용하여 이 endpoint에서 응답받은 줄들을 가져오자

이 데이터는 쉼표로 구분된 텍스트로 포맷되어 있으며, 각 줄은 완전한 데이터 행

다운로드의 용량이 클 수 있지만, 데이터를 받는 대로 방출하면 반응성이 좋아짐

또한, 익숙한 일반 시퀀스에서 쓰던 것들을 새로운 async 컨텍스트에서도 사용 가능함

→ 새로운 for-await-in 구문으로 반복할 수 있고, map, filter, reduce, dropFirst와 같은 함수를 사용하여 값을 조작할 수 있음

 

AsyncSequence 동작원리 (01:48)

동작 원리는 async/await 세션에서 설명한 기반 위에 있으므로 핵심 포인트를 정리해보자

  • async 함수는 await 키워드를 사용해 콜백 없이 동시성을 가진 코드를 작성할 수 있음
  • async 함수를 호출하면, 값이나 에러가 생성될 떄까지 일시 중단(suspend) 되었다가 다시 재개(resume)

→ 반면에, AsyncSequence는 각 요소마다 suspend 되고 underlying iterator가 값을 생성하거나 throw할 때 resume

 

 

일반적인 Sequence와 거의 같지만 차이점이 있음 (02:29)

 

  • 가장 큰 차이는 각 요소가 비동기적으로 전달되는 것
  • 비동기 전달이기 때문에 실패 가능성도 존재함
  • async sequence는 throw할 수도 있지만, 실패하지 않을 수도 있음
  • 마치 throw하는 함수처럼, 컴파일러는 반복(iteration)하거나 합성(composing)할 때 에러 처리를 반드시 보장해줌
  • 일반적으로 비동기 시퀀스는 ‘시간에 따라 값을 생성하는 방식’으로 볼 수 있음

즉, async sequence는 0개 이상의 값을 가질 수 있고, iterator가 nil을 반환하면 완료를 알림 (일반 sequence와 동일)

에러가 발생하면 async sequence의 종료 지점이며, 이후에는 iterator의 next를 호출해도 nil을 반환함

→ 아래의 iteration 예제를 보면 iterator가 nil을 반환한다는 의미를 알 수 있음

 

일반적인 반복(iteration)부터 살펴보자

sequence로부터 quake를 반복하면서, 진도(magnitude)가 일정 값 이상일 때 함수를 호출하는 예제

 

이 코드에서 컴파일러가 어떻게 동작하는지 살펴보자

 

다음은 컴파일러가 위 코드를 빌드할 때 대략적으로 일어나는 일이다

  • 먼저, iterator 변수를 만들고, 그 다음에 while 루프를 사용해 next가 호출될 때마다 생성되는 quake를 가져옴
  • 만약 next를 호출했는데 nil이라면 while문 종료

이 코드는 async/await 기능을 활용하려면 next 함수를 비동기 함수로 바꾸면 됨

→ 다음 quake를 기다릴 수 있음

 

 

 

다시 앞으로 돌아가서 이 루프가 async sequence였다면 어땠을까?

앞에서 말했듯, 우리는 async sequence의 각 아이템을 기다려야 함

즉, Sequence를 다룰 줄 안다면, AsyncSequence도 쉽게 다룰 수 있다는 뜻

 


2-1. AsyncSequence 사용 방법 (04:21)

  • for-await-in 구문
  • async sequence가 throw 한다면, for-try-await-in 구문
  • break, continue 지원

 

에러 처리 지원

  • throwing 함수와 마찬가지로, try 키워드가 빠지면 컴파일 에러를 감지해줌

→ async sequence가 에러가 가능하다면 항상 안점함 왜냐, Swift가 에러를 반드시 던지거나 잡도록 가능제하기 때문

 

 

또, 위에서 do 내부 반복은 위 quekes 반복이 끝난 뒤 순차적으로 실행됨

만약 반복을 동시에 실행하고 싶으면 반복을 감싼 새로운 async task를 만들면 됨

→ 사용 중인 async sequence가 무한히 실행될 때 유용함

 

 

또한, 이러한 방식은 외부에서 반복을 취소할 떄도 유용함

→ Task를 사용하면, 무한할 수 있는 반복을 어떤 컨테이너의 생명주기에 맞춰 쉽게 범위를 제한할 수 있음

 

 

2-2. AsyncSequence API (07:15)

AsyncSequence API들은 macOS Monterey iOS 15, tvOS15, watchOS 8부터 사용 가능함

 

파일/네트워크 I/O

파일을 읽는 것은 대표적인 비동기 동작이다

for try await line in FileHandle.standardInput.bytes.lines {
    ...
}
  • FileHandle에는 bytes 프로퍼티가 생김
  • 해당 FileHandle에서 오는 바이트들의 async sequence를 제공함
  • 또한 async sequence를 line 단위로 변환할 수 있음

 

파일 처리는 흔하기 때문에 URL 자체에도 bytes와 lines 접근자가 추가됨

let url = URL(fileURLWithPath: "/tmp/somefile.txt")
for try await line in url.lines {
    ...
}
  • URL에서 파일이든 네트워크든, 그 내용으로부터 line 단위 AsyncSequeqnce를 반환하는 프로퍼티
  • 이 기능은 이전의 복잡했던 방식보다 훨씬 쉽고 안전함

 

네트워크에서 데이터를 가져올 때 응답 처리나 인증 제어가 필요하기도 함

따라서 URLSession에는 주어진 URL이나 URLRequest로부터 바이트들의 async sequence를 가져오는 bytes 함수가 생김

→ 자세한 URLSession의 비동기 기능 내용은 아래 세션 참고

2025.09.05 - [WWDC] - WWDC 2021 - Use async/await with URLSession 정리

 

WWDC 2021 - Use async/await with URLSession 정리

https://developer.apple.com/videos/play/wwdc2021/10095/ Use async/await with URLSession - WWDC21 - Videos - Apple DeveloperDiscover how you can adopt Swift concurrency in URLSession using async/await and AsyncSequence, and how you can apply Swift concurren

soo-hyn.tistory.com

 

https://developer.apple.com/videos/play/wwdc2021/10095/

 

Use async/await with URLSession - WWDC21 - Videos - Apple Developer

Discover how you can adopt Swift concurrency in URLSession using async/await and AsyncSequence, and how you can apply Swift concurrency...

developer.apple.com

 

 

Notification (09:10)

Notification도 새로운 비동기 API를 사용 가능함

특정 storeUUID가 일치하는 첫 번째 원격 변경 알림을 가다리는 예시

let center = NotificationCenter.default
let notification = await center.notifications(named: .NSPersistentStoreRemoteChange).first {
    $0.userInfo[NSStoreUUIDKey] == storeUUID
}
  • notifications(name:)는 AsyncSequence를 리턴함
  • 즉, 지정한 이름의 Notification이 올 때마다 그 이벤트를 차례로 흘려보내는 스트림이 생김
  • first(where:)는 AsyncSequence에서 제공되는 비동기 연산자
  • 조건에 맞는 첫 번째 요소를 찾을 때까지 대기(suspend)
  • 조건이 맞는 값이 오면 즉시 반환 → await 풀림
  • 조건이 맞는 게 끝내 오지 않으면 → 그냥 Task는 계속 suspend 상태로 유지됨

 

기존 NotificationCenter API는 델리게이트/클로저 기반이라서

center.addObserver(forName: .NSPersistentStoreRemoteChange, object: nil, queue: nil) { notification in
    ...
}

이렇게 쓰면 콜백 안에서 분기 처리를 해야 했음

 

하지만, AsyncSequence를 쓰면?

  • 필요할 때 await로 기다리기만 하면 됨
  • 값이 올 때까지 Task는 suspend → 스레드 변환 → 효율적
  • Notification 흐름을 map/filter 등으로 조합 가능

 

 

AsyncSequence 값을 비동기적으로 조작할 수 있는 새로운 API (09:47)

  • Sequence에서 할 수 있는 거의 모든 것이 Async Sequence에도 대응됨

 

4. Adopting AsyncSequence (10:14)

직접 async sequence를 만들려면 어떻게 할까?

  • AsyncSequence를 구현하는 방법은 몇 가지 있지만, 기존 코드를 어떻게 적응시킬 수 있는지에 초점을 맞추자
  • AsyncSequence와 잘 맞는 디자인 패턴이 있고, 기존의 코드들을 새로운 개념과 상호작용하도록 만드는 데 좋은 도구들이 있음

  • 예를 들어, 여러 번 호출되는 클로저나 delegate 패턴이 잘어울림
  • 응답이 필요없고 새로운 값이 발생했음을 알리기만 하면 되는 것들도 잘어울림

 

아래는 흔한 handler 패턴의 예시이다

  • 핸들러 프로퍼티와 start, stop 메서드를 가진 클래스

 

기존 사용법

  • 모니터 객체를 만들고, 값을 받을 핸들러를 할당한 다음, 모니터를 시작해서 지진 데이터를 핸들러에 보내는 방식
  • 나중에 모니터가 중지되면 이벤트 생성도 취소됨

 

이제 이와같은 인터페이스를 AsyncStream 타입으로 적응시킬 수 있음 (11:38)

  • AsyncStream을 생성할 때는 element 타입과 생성 클로저를 지정함
  • 이 클로저는 여러 번 값을 전달 할 수 있는 continuation을 받으며,
  • 종료(finish)하거나 취소(termination)도 처리할 수 있음
  • 즉, 이 경우는 monitor 객체를 생성 클로저 안에서 만들 수 있음
  • 그리고 핸들러는 continuation으로 quake를 전달하도록 설정함
  • onTermination에서는 취소와 정리를 처리함
  • 그 다음 모니터링 시작
  • 앞서 사용하던 모니터 코드를 AsyncStream의 생성에서 쉽게 캡슐화할 수 있음

→ 매번 같은 로직을 반복할 필요가 줄음 (중복 코드 개선)

 

AsyncStream 사용법 (12:30)

  • 변환 함수(filter)와 새로운 for-await-in 구문을 활용할 수 있음
  • bookkeeping? 걱정 대신 코드의 의도(intent)에만 집중할 수 있음
  • AsyncStream을 사용하면 자신만의 async sequence를 만드는 데 굉장히 유연함

 

여기서는 한 가지 예시만 봤지만, 다양한 방식으로 응용 가능

  • AsyncStream은 기존 코드를 async sequence로 적응시키는 훌륭한 방법
  • 안정성, 반복, 취소, 버퍼링까지 async sequence에서 기대하는 모든 것을 처리 가능
  • AsyncStream은 자신만의 async sequence를 만들 때 탄탄한 방법이자, API의 반환 타입으로도 적합
  • 왜냐, 생성 클로저만이 요소들을 만들어내는 유일한 소스이기 때문

 

에러를 던지는 경우엔?

  • AsyncThrowingStream은 AsyncStream과 거의 동일하지만 에러를 처리할 수 있음
  • AsyncThrowingStream도 AsyncStream과 같은 유연성과 안전성을 제공하면서, 반복 중에 에러를 throw할 수 있음

 


 

AsyncStream 자세히 알아보기 (WWDC 내용 X)

 

AsyncStream이란?

  • AsyncStream<Element>는 AsyncSequence의 한 구현체
  • 외부에서 값을 push하는 비동기 이벤트 스트림을 만들 수 있는 도구
  • 비동기적으로 여러 개의 값을 차례차례 흘려보내는 스트림을 표현
  • 원래 콜백, delegate, 클로저로 전달되던 이벤트들을 for await 문으로 받을 수 있도록 바꿔줌
  •  

구조

let stream = AsyncStream(Int.self) { continuation in
    // continuation을 통해 외부에서 값을 밀어 넣음
    continuation.yield(1)
    continuation.yield(2)
    continuation.finish()
}
  • 생성 시점에 클로저를 받고, 그 안에 contination 객체가 주어짐
  • continuation 역할
    • yield(_:): 값 하나를 스트림에 흘려보냄
    • finish(): 스트림 종료
    • onTermination: 소비자가 중간에 스트림을 끊었을 때 호출됨 (리소스 정리할 떄 유용)
          continuation.onTermination = { termination in
              switch termination {
              case .finished:
                  print("finished")
              case .cancelled:
                  print("cancelled")
              }
          }
      • finish: contination.finish() 로 종료되었을 때
      • cancelled: 스트림이 취소되었을 떄

소비자 측

for await value in stream {
    print("받은 값:", value)
}
  • 스트림이 새로운 값을 받을 때마다 for await 루프가 하나씩 깨어나서 값을 처리함
  • finish() 호출 후에는 루프가 끝남
728x90