###### tags: 교육자료
# [WWDC 2021] Meet Async/Await in Swift
[TOC]
## 시작
### Async/Await 개념을 학습하는 이유
> CompletionHandler 를 계속 사용하다 보면 @escaping 클로저를 연속적으로 사용해야 할 때가 있습니다. 예를들면 HTTP GET 요청의 response로 deleteKey 값을 받아와 그 deleteKey 값으로 HTTP DELETE 요청을 할 때 가 있는데요, 문맥을 두번 들여쓰기를 해야하고 이 연속요청이 많으면 많을수록 마치 굼벵이 주름같은 들여쓰기 코드가 생깁니다. 가독성에 악영향을 미치죠. 이를 해결할 방법이 없을까요? 요청의 결과값을 반환할 수는 없을까 많은 궁금증을 가졌었습니다.
<br>
## 문제해결 예시
### @escaping 클로저를 사용하는 기본함수 예시
- UIKit의 기본적인 메서드인 `preparingThumbnail(of:)` 사용하면, 그 함수가 끝날 때 까지 thread 가 block 되어 아무것도 할 수 없게 됩니다.
- 반대로, completionHandler가 있는 비동기 함수인 `prepareThumbnail(of: completionHandler:)` 를 사용하면 thread 가 다른 일을 할 수 있게 됩니다. 이 함수가 끝나면 completionHandler로 해당 작업이 끝났음을 알려줍니다.
- 비동기 함수에는 이러한 장점이 있기 때문에, 네트워크 요청과 같이 비용이 많이들고 시간이 걸리는 작업을 비동기 함수로 처리하게 됩니다.
▼ 이미지를 가져올 때, 우리가 많이 사용하는 예시 코드입니다.
```swift
func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
let request = thumbnailURLRequest(for: id)
let task = URLSession. shared. dataTask(with: request) { data, response, error in
if let error = error {
completion (nil, error)
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
completion (nil, FetchError.badID)
} else {
guard let image = UIImage (data: data!) else {
return
}
image.prepareThumbnail (of: CGSize (width: 40, height: 40)) { thumbnail in
guard let thumbnail = thumbnail else {
return
}
completion (thumbnail, nil)
}
}
}
task.resume()
}
```
completion 이 3개가 사용되고 있네요! 이러면 완벽한 메서드가 되었을까요?
자세히 보면 guard 문에서 생긴 에러 처리를 해주지 않고 있어요.
▼ guard 문에 대한 completion 2개를 추가하면 총 5개의 completion 이 사용 될 것입니다.
```swift
func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
let request = thumbnailURLRequest(for: id)
let task = URLSession. shared. dataTask(with: request) { data, response, error in
if let error = error {
completion (nil, error)
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
completion (nil, FetchError.badID)
} else {
guard let image = UIImage (data: data!) else {
completion(nil, FetchError.badImage) // 추가
return
}
image.prepareThumbnail (of: CGSize (width: 40, height: 40)) { thumbnail in
guard let thumbnail = thumbnail else {
completion(nil, FetchError.badImage) // 추가
return
}
completion (thumbnail, nil)
}
}
}
task.resume()
}
```
이렇게 하면 오류처리가 완벽하게 된 것으로 보입니다. <span style="background:lightpink">하지만, 우리는 Swift의 기본적인 에러 핸들링 메커니즘을 사용할 수 없게 됩니다. 문제 안에서 에러를 던질 수 없다는 것입니다. </span>
두개의 guard문을 처리해주지 않고도 컴파일 에러가 나지 않은 이유는 swift가 우리의 작업을 확인할 수 없기 때문입니다(컴파일 단계에서 에러를 잡아낼 수 없다는 뜻)
▼ ResultType을 써서 좀 더 안전하게 처리해줄 수도 있습니다. 하지만 가독성에 악영향을 미치죠..
```swift
func fetchThumbnail(for id: String, completion: @escaping (Result<UlImage, Error>) -> Void) {
let request = thumbnailURLRequest(for: id)
let task = URLSession. shared. dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure (error))
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
completion(.failure(FetchError.badID))
} else {
guard let image = UIImage (data: data!) else {
completion(.failure (FetchError.badImage))
return
}
image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
guard let thumbnail = thumbnail else {
completion(. failure (FetchError .badImage))
return
}
completion (.success (thumbnail))
}
}
}
task.resume ()
}
```
<br><br>
### Async/await 도입으로 해결
- `thumbnailURLRequest(for:)`
- `dataTask(with:)`
- `UIImage(data:)`
- `prepareThumbnail(of:)`
▼ 위 네가지 스텝을 async/await 을 사용하여 구현 해보겠습니다.
```swift
func fetchThumbnail(for id: String) async throws -> UIImage {
let request = thumbnailURLRequest(for: id) // 1.동기함수이기 때문에 thread block
let (data, response) = try await URLSession.shared.data(for: request) // 2
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badID }
let maybeImage = UIImage(data: data) // 3 동기함수이기 때문에 thread block
guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage } //4
return thumbnail
}
```
- 위처럼 async/await 함수임을 나타내는 키워드 `async`와 에러를 던질 수 있음을 나타내는 키워드 `throws`가 있습니다.
1. `thumbnailURLRequest(for:)` 메서드는 동기함수 이므로 thread block이 됩니다.
2. `data(for:)` 메서드는 `dataTask()`와 마찬가지로 Foundation에서 제공되는 비동기 함수입니다. 하지만 기존의 dataTask와는 달리 await 키워드를 통해 기다려줄 수 있습니다. 이 메서드는 스스로 중지시켜서 thread unblock을 합니다. 다른 작업들이 실행될 수 있는 것이죠. 이전에 dataTask 를 통해 생긴 에러를 모두 completion에 보내주었던 것 기억 하시나요? 여기선 try 키워드 하나로 처리를 해주게 됩니다 !! 데이터 다운로드가 끝나면 `data(for:)`메서드가 재개(resume) 되고 fetchThumbnail로 돌아옵니다. 이때 가져오는 에러는 이 함수에서 처리됩니다.
3. 동기함수 이므로 thread block이 됩니다.
4. `thumbnail` 프로퍼티가 재개되고 fetchThumbnail로 돌아올 때 까지 다른 작업들이 실행될 수 있습니다.
<br>
completionHandler를 쓰던 이전버전과는 반대로, 여기서 <span style="background:lightpink">thumbnail이 받아와지지 않으면 에러를 던져주거나 값을 반환해주어야 합니다.</span> 그러지 않으면 컴파일 에러가 날 것입니다.(스위프트가 우리의 작업을 확인해줄 수 있다는 것입니다.)
<br>
이게 다입니다. 20줄 가량의 코드를 고작 6줄로 바꿨습니다. 이렇게 변환하게 되면 코드도 줄고 에러도 안전하게 처리할 수 있을 것입니다.
▼ 함수 뿐만 아니라 프로퍼티 또한 async 키워드를 사용할 수 있습니다. `thumbnail` 프로퍼티의 구현부 입니다
```swift
extension UIImage {
var thumbnail: UIImage? {
get async {
let size = CGSize(width: 40, height: 40)
return await self.byPreparingThumbnail(ofSize: size)
}
}
}
```
- 오직 읽기전용 프로퍼티에만 async 키워드를 사용할 수 있습니다. (swift 5.5 이후부터 프로퍼티의 getter 또한 throw를 사용할 수 있습니다.)
<br>
▼ async는 for-in loop 에서도 사용할 수 있습니다.
```swift
for await id in staticImageIDsURL.lines {
let thumbnail = await fetchThumbnail(for: id)
collage.add(thumbnail)
}
let result = await collage.draw()
```
- 위 내용을 더 자세히 보고싶다면
- [Meet AsyncSequence](https://developer.apple.com/videos/play/wwdc2021/10058) 와
- [Explore structured concurrency in Swift](https://developer.apple.com/videos/play/wwdc2021/10134)를 참고해주세요
<br><br>
## 설명
### async 함수가 suspend 한다는 것은 무엇을 뜻할까요 ? [15:20]

보통의 함수가 실행하고 끝나는 모습입니다. 함수가 끝나면 쓰레드 제어권을 포기하게 됩니다.
async 함수도 마찬가지로 실행이 끝나면 다시 상위 함수에게 제어권을 줍니다. 다만, 쓰레드 포기하는 방법이 전혀 다릅니다. 바로 `suspending` 입니다.

async 함수는 내부가 실행되고, await 할 수 있습니다. 이때 다시 함수에게 쓰레드 제어권이 가는 것이 아닌 system으로 갑니다. 이렇게 되면 상위 함수도 중지가 됩니다.
suspending 은 함수가 시스템에게 "할 일이 많은 것을 알아. 가장 중요한 일을 결정해!" 라고 알리는 방법입니다.
함수가 한번 중지되면 시스템은 다른 작업을 하는데에 해당 쓰레드를 사용할 수 있게 됩니다.
resume이 되면 suspend는 끝나고 다시 상위 함수로 돌아오게 됩니다.
```swift
func fetchThumbnail(for id: String) async throws -> UIImage {
let request = thumbnailURLRequest(for: id)
let (data, response) = try await URLSession.shared.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throws FetchError.badID}
let maybeImage = UIImage(data: data)
guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage}
return thumbnail
}
```
위 fetchThumbnail 에서 data 메서드 에서 중지되는 동안 데이터를 업로드 하는 어떤 버튼이 눌렸다고 가정해봅시다. 이때는 이전에 큐에 들어가있던 작업보다 이 버튼작업이 먼저 실행됩니다. 버튼 작업이 끝나고 나서야 data 메서드가 resume 되거나, 다른작업을 실행합니다. data 메서드가 한번 끝나면 다시 fetchThumbnail 함수로 돌아옵니다.
이는 async 함수가 중지되어있는 동안 다른 작업(버튼 탭)이 실행될 수 있고, 그래서 await 키워드를 붙여주는 것이라고 할 수 있겠습니다.
async 함수는 suspend 된 동안 다른 작업이 실행될 수 있기 때문에 함수가 다른 스레드에서 실행될 수도 있습니다. 해당 이슈는 [Protect mutable state with Swift actors](https://developer.apple.com/videos/play/wwdc2021/10133) 여기서 확인할 수 있습니다.
<br><br>
### Async/await facts
- `async` 키워드를 다는 것은 중지를 허용하는 것입니다.
- 함수를 중지한다면 그 함수의 호출자도 중지됩니다. 그래서 똑같이 호출자도 `async` 키워드를 써야만 합니다.
- 중지될 수 있는 `async` 함수의 앞에 `await` 키워드를 사용합니다.
- `await` 키워드가 무조건 중지한다는 뜻은 아닙니다
- `async` 함수가 중지되면, 쓰레드가 block 되지 않습니다. 그래서 system은 다른 작업들을 스케쥴링 할 수 있게 됩니다. (나중으로 미뤄진 작업이 첫번째로 실행될 수도 있습니다.)
- `async` 함수 호출이 한번 완료되면 `await` 이후에 실행이 재개됩니다.
<br><br>
### Adopting async/await [21:00]
#### 테스트에 적용 해보기
```swift
class MockViewModelSpec: XCTestCase {
func testFetchThumbnails() throws {
let expectation = XCTestExpectation(description: "mock thumbnails completion")
self.mockViewModel.fetchThumbnail(for: mockID) { result, error in
XCTAssertEaqual(result?.size, CGSize(width: 40, height: 40))
expectation.fulfill()
}
wait(for: [expectation], timeout: 5.0)
}
}
```
▼ 기본 completionHandler 를 사용하여 비동기 처리를 했을 때, 테스트 방식입니다. 이젠 async를 이용해 간단하게 표현할 수 있습니다.
```swift
class MockViewModelSpec: XCTestCase {
func testFetchThumbnails() throws {
let result = try await self.mockViewModel.fetchThumbnail(for: mockID)
XCTAssertEaqual(result?.size, CGSize(width: 40, height: 40))
}
}
```
<br>
#### Bridging from sync to async
Task 로 감싸는 것은 Global Dispatch Queue로 보내는 것과 같습니다.
- [Explore structured concurrency in Swift](https://developer.apple.com/videos/play/wwdc2021/10134)
- [Discover concurrency in SwiftUI](https://developer.apple.com/videos/play/wwdc2021/10019)
<br>
#### Async APIs in the SDK
swift 5.5 부터 기본내장 SDK에 async 코드가 추가되었습니다.
[deprecated getCurrentTimelineEntry](https://developer.apple.com/documentation/clockkit/clkcomplicationdatasource/1628051-getcurrenttimelineentry)
>위 링크에서 특정 함수가 async로 적용된 형태를 설명하고 있습니다. 심지어 기존의 함수는 이제 사용되지 않을 것이라고 설명되어 있습니다.

<br>
#### Async alternatives and continuations
```swift
func getPersistentPosts (completion: @escaping ([Post], Error?) -> Void) {
do {
let req = Post. fetchRequest ()
req. sortDescriptors = [ NSSortDescriptor (key: "date", ascending: true) ]
let asyncRequest = NSAsynchronousFetchRequest<Post>(fetchRequest: req) { result in
completion (result.finalResult ?? [1, nil)
}
try self.managedObjectContext.execute (asyncRequest)
} catch {
completion([], error)
}
}
```
위 함수를 우리가 직접 async 함수로 변환하는 과정을 거쳐보겠습니다.
```swift
func persistentPosts() async throws -> [Post] {
self.getPersistentPosts { posts, error in
}
}
```

반환값을 어떻게 넘겨주어야 할까요?
이러한 흐름은 모든 aync 적용에서 보이는 패턴입니다. 이를 continuation 이라고 합니다.
```swift
func persistentPosts() async throws -> [Post] {
typealias PostContinuation = CheckedContinuation<[Post], Error>
return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in
self.getPersistentPosts { posts, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: posts)
}
}
}
}
```
completionHandler 를 async 함수로 만드는 과정을 보셨습니다. continuation 은 async함수의 실행을 수동제어 하기에 강력한 방법입니다.
그러나 주의할 점이 있습니다. resume 은 모든 path에서 한번! 호출되어야 합니다. 0번 실행시 warning이 뜨고, 2번이상 호출시에는 compiler에서 더욱 심각한 에러를 발생시킵니다.
더 자세한 내용은 [Swift concurrency: Behind the scenes](https://developer.apple.com/videos/play/wwdc2021/10254)이 링크를 확인해주세요
빠진 내용이 있을 수 있습니다. 가장 좋은 것은 직접 WWDC 영상을 확인해 보시면 좋을 것 같습니다.
### Reference
- [WWDC 2021 - Meet Async/Await in Swfit](https://developer.apple.com/videos/play/wwdc2021/10132/)