Search

[정리] 어쩔 async, 어쩔 await

어쩔맥세이프~ 어쩔트랙패드~ 어쩔애플워치~ 어쩔아이폰13~ 어쩔맥북프로~ 어쩔맥북프로풀셋장착~
contents

async / await

이번 WWDC21에서 async/await가 소개되었습니다. async/await는 Swift 5.5 그리고 iOS 15부터 사용이 가능합니다. iOS 15.0부터 사용 가능하기 때문에 당장 릴리즈를 위한 코드에 사용하기엔 부담이 있을 거 같지만, 그게 아니라면 여러모로 사용해봐도 좋을 것 같습니다!
async / await : 비동기 코드를 동기적으로 작성하게 해주는 swift extension async - 함수를 비동기 함수로 만들겠다. await - 비동기 함수 호출시 potential suspension point 지정

async / await 가 왜 필요할까?

여러분들 이런 경험이 있을 겁니다.
DispatchQueue.main.async { fetchWeatherHistory() calculateAverageTemperature() upload() }
Swift
복사
이런식으로 동기 처리를 하려고 하면, 우리가 원하는 순서가 아닌 멋대로 순서를 정해서 동기 처리를 실행하는 걸 볼 수 있습니다. 우리가 원하는 순서는 fetchWeatherHistory() -> calculateAverageTemperature() -> upload() 인데 불구하고 순서가 뒤죽박죽이 될 수도, 다 같이 실행이 될 수도 있어요.
뒤죽박죽의 문제
문제를 해결하기 위해선 어떻게 해야할까요? 다들 아마 이 방식을 사용했을 겁니다.
func fetchWeatherHistory(completion: @escaping ([Double]) -> Void) { /// fetch code } func calculateAverageTemperature(for records: [Double], completion: @escaping (Double) -> Void) { /// calculate code } func upload(result: Double, completion: @escaping (String) -> Void) { /// upload code }
Swift
복사
바로 completion를 사용하는 겁니다. completion를 사용하게 되면 동기처리가 확실하게 됩니다.
왜냐면 completion이 해당 함수가 실행되고 난 후에 실행이 될 것이 확실하게 보장이 되거든요. 덧붙여서 언제 해당 함수가 불러질지 확실하게 알 수 있습니다.
하지만 문제가 한 가지 있다면 클로저가 중첩되기 때문에 JS에서 나타나는 콜백 지옥을 보게되는거죠.
fetchWeatherHistory { records in calculateAverageTemperature(for: records) { average in upload(result: average) { response in print("Server response: \(response)") } } }
Swift
복사
현재는 함수가 3개지만 훨씬 많은 함수를 사용하게 된다면 굉장히 복잡해질겁니다.
거기다가 분기 처리 등의 추가 작업이 이루어진다면 가독성은 떨어지고 복잡도는 급격하게 상승하겠죠?
JS에서는 Promiseasync/await를 사용하는데, 그 부분이 Swift로도 넘어온 거 같습니다.
이제 Swift에서도 async/await를 사용해서 비동기적 처리 속에서 동기 처리를 할 수 있습니다.

async / await 활용

직관적이고 깔끔한 코드 작성이 가능해집니다.
func fetchWeatherHistory() async -> [Double] { /// fetch code } func calculateAverageTemperature(for records: [Double]) async -> Double { /// async code } func upload(result: Double) async -> String { /// upload code } func processWeather() async { let records = await fetchWeatherHistory() let average = await calculateAverageTemperature(for: records) let response = await upload(result: average) print("Server response: \(response)") }
Swift
복사
각각의 함수는 비동기 함수지만 await로 흐름을 제어함으로써 동기적인 코드 작성이 가능해졌습니다.
또한 클로저를 활용한 코드보다 상대적으로 가독성이 좋습니다.
completion은 매번 사용을 위해서 원하는 부분에 completion 코드를 넣어줘야 했는데, 이제는 함수 내에서 원하는 기능을 하고 return해주면 됩니다. completion를 잘못 놓거나 놓지 않아서 생기는 문제는 발생하지 않는다는거죠.

Deep async / await

위의 예시들을 통해서 completionHandler를 넘겨주는 대신 function에 async를 표시해주면 된다는 걸 깨달았을 겁니다. 이제 조금 다른 예시를 통해서 깊게 설명해볼게요.(WWDC21 Example)
func fetchThumbnail(for id: String) async throws -> UIImage { }
Swift
복사
뒤에 throws가 생겼습니다. async는 꼭 throws 앞에 붙여줘야 합니다. 그 외의 위치는 먹히질 않습니다.
어떻게 동작하나요?
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 { throw FetchError.badID } let maybeImage = UIImage(data: data) guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage } return thumbnail }
Swift
복사
throws로 표시된 함수 호출 → try 필요
async로 표시된 기능 호출 → await 필요
unblock thread
전체적으로 Safe, Short → 에러를 만나면 throw해주기 때문
extension UIImage { var thumbnail: UIImage? { get async { let size = CGSize(width: 40, height: 40) return await self.byPreparingThumbnail(ofSize: size) } } }
Swift
복사
read-only properties(getter)만 async를 표시 가능
property getter는 throw 가능
protocol SomeDelegate { func process() async }
Swift
복사
프로토콜에서도 요구 가능

normal Function vs async Function

normal Function
normal function을 call하면, 작업이 끝날 때까지 thread를 occupied합니다.
func thumbnailURLRequest(for id: String) -> URLRequest { // ... return request }
Swift
복사
async Function
async Function은 suspend될 수 있고 그동안 다른 일을 수행 가능합니다.
Suspend
만약, fetchThumbnail 내부에 있는 URLSession.shared.data(for: request)를 사용하다가 중간에 버튼을 누르는 이벤트가 발생했다고 하면
1.
data 함수 실행
2.
버튼 이벤트 처리
3.
data 함수 resume
4.
fetchThumbnail로 return

async let

이번 Concurrency 업데이트에서는 async let이라는 기능도 추가되었습니다.
평행한 두 개의 비동기 작업을 처리하는 작업이 가능해집니다. → 비동기 처리 + 병렬
위에서 봤던 예시들은 병렬이 아닌 async 함수를 호출했습니다. 따라서 async가 한 번에 되지 않고 await를 통해서 중간중간 suspend를 겪습니다.
let firstPhoto = await downloadPhoto(named: photoNames[0]) let secondPhoto = await downloadPhoto(named: photoNames[1]) let thirdPhoto = await downloadPhoto(named: photoNames[2]) let photos = [firstPhoto, secondPhoto, thirdPhoto] show(photos)
Swift
복사
하지만 async let를 사용하면 병렬로 async 함수를 호출할 수 있습니다. 한 번에 원하는 부분들을 가져올 수 있는거죠.
async let firstPhoto = downloadPhoto(named: photoNames[0]) async let secondPhoto = downloadPhoto(named: photoNames[1]) async let thirdPhoto = downloadPhoto(named: photoNames[2]) let photos = await [firstPhoto, secondPhoto, thirdPhoto] show(photos)
Swift
복사
만약에 우리가 서버 통신을 통해 두 개의 값을 가져와야 한다면 async let이 훨씬 편하겠죠?
func fetchA() async -> AModel { let A = try await KMNetworkManager.shared.getA() return A } func fetchB() async -> BModel { let B = try await KMNetworkManager.shared.getB() return B } func process() async { async let a = fetchA() async let b = fetchB() let viewModel = await ViewModel(a, b) self.navigateToAlarmSetting(viewModel) }
Swift
복사

Swift 예외 처리

를 하고 싶었지만, async / await만으로도 내용이 충분히 길어서... 나중에 알아보겠습니다
async / await를 공부하면서 Actor라는 참조 타입도 찾게 됐는데 고것도 나중에 함 설명해드리겠습니다
참고 자료