###### 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] ![](https://i.imgur.com/WBLHFxP.png) 보통의 함수가 실행하고 끝나는 모습입니다. 함수가 끝나면 쓰레드 제어권을 포기하게 됩니다. async 함수도 마찬가지로 실행이 끝나면 다시 상위 함수에게 제어권을 줍니다. 다만, 쓰레드 포기하는 방법이 전혀 다릅니다. 바로 `suspending` 입니다. ![](https://i.imgur.com/sAo4xL5.png) 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로 적용된 형태를 설명하고 있습니다. 심지어 기존의 함수는 이제 사용되지 않을 것이라고 설명되어 있습니다. ![](https://i.imgur.com/rm86i3l.png) <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 } } ``` ![](https://i.imgur.com/js8N57k.png) 반환값을 어떻게 넘겨주어야 할까요? 이러한 흐름은 모든 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/)