티스토리 뷰
안녕하세요 :) Zedd입니다.
오늘은 매우 핫한 async, await를 한번 보려고 합니다.
2021.03.25일 기준 async-await proposal은 Swift 5.5에서 구현된 상태입니다.
+ ) 2021.06.15 글 수정. @asyncHandler 내용 삭제.
# 문제
1. Swift 개발에서 Closure 및 completion handlers를 사용하는 asynchronous(비동기) 프로그래밍을 많이 함.
2. 많은 비동기 작업 / 오류 처리 / 비동기 호출간의 제어 흐름이 복잡할 때 문제가 됨
[1] 많은 비동기 작업
일련의 비동기 작업에는 deeply-nested closures가 필요.
func processImageData1(completionBlock: (_ result: Image) -> Void) {
loadWebResource("dataprofile.txt") { dataResource in
loadWebResource("imagedata.dat") { imageResource in
decodeImage(dataResource, imageResource) { imageTmp in
dewarpAndCleanupImage(imageTmp) { imageResult in
completionBlock(imageResult)
}
}
}
}
}
processImageData1 { image in
display(image)
}
[2] 오류 처리
콜백은 오류처리를 어렵고 매우 장황하게 만듬.
// (2c) Using a `switch` statement for each callback:
func processImageData2c(completionBlock: (Result<Image, Error>) -> Void) {
loadWebResource("dataprofile.txt") { dataResourceResult in
switch dataResourceResult {
case .success(let dataResource):
loadWebResource("imagedata.dat") { imageResourceResult in
switch imageResourceResult {
case .success(let imageResource):
decodeImage(dataResource, imageResource) { imageTmpResult in
switch imageTmpResult {
case .success(let imageTmp):
dewarpAndCleanupImage(imageTmp) { imageResult in
completionBlock(imageResult)
}
case .failure(let error):
completionBlock(.failure(error))
}
}
case .failure(let error):
completionBlock(.failure(error))
}
}
case .failure(let error):
completionBlock(.failure(error))
}
}
}
processImageData2c { result in
switch result {
case .success(let image):
display(image)
case .failure(let error):
display("No image today", error)
}
}
Swift.Result가 Swift 5.0에서 추가되면서..error를 처리하는게 더 쉬워졌지만,
여전히 closure 중첩 문제는 남아있음.
[3] 비동기 호출간의 제어 흐름이 복잡할 때
비동기 함수를 조건부로 실행하는 것은 고통 그자체...
예를들어, 이미지를 얻은 후 "swizzle"해야한다고 할 때,
1. 이미지가 있으면 바로 swizzle
2. 이미지가 없으면 decode후 swizzle
func processImageData3(recipient: Person, completionBlock: (_ result: Image) -> Void) {
let swizzle: (_ contents: Image) -> Void = {
// ... continuation closure that calls completionBlock eventually
}
if recipient.hasProfilePicture {
swizzle(recipient.profilePicture)
} else {
decodeImage { image in
swizzle(image)
}
}
}
그래서 위와같은 코드가 필요.
이 함수를 구조화 하는 방법은 위 코드와 같이 completion Handler에서 swizzle 코드를 작성하는 것.
func processImageData3(recipient: Person, completionBlock: (_ result: Image) -> Void) {
let swizzle: (_ contents: Image) -> Void = {
// ... continuation closure that calls completionBlock eventually ✅✅
}
if recipient.hasProfilePicture {
swizzle(recipient.profilePicture)
} else {
decodeImage { image in
swizzle(image)
}
}
}
1. 이 패턴은 함수의 자연스러운 하향식 구성을 반전시킴.
2. swizzle closure가 completion handler에서 사용되므로 capture에 대해 신중하게 생각해야함
➞
조건부로 실행되는 비동기 함수의 수가 증가함에 따라 문제는 더욱 악화
&&
본질적으로 반전된 "파멸의 피라미드(pyramid of doom)"를 생성.
[4] 실수하기 쉬움
func processImageData4a(completionBlock: (_ result: Image?, _ error: Error?) -> Void) {
loadWebResource("dataprofile.txt") { dataResource, error in
guard let dataResource = dataResource else {
return // ⚠️ <- forgot to call the block
}
loadWebResource("imagedata.dat") { imageResource, error in
guard let imageResource = imageResource else {
return // ⚠️ <- forgot to call the block
}
...
}
}
}
completionBlock을 호출하지 않고 그냥 return하고 잊어버리면 디버깅하기 어려움.
니가 만약 잊지않고 completionBlock을 호출했다고 치자?
func processImageData4b(recipient:Person, completionBlock: (_ result: Image?, _ error: Error?) -> Void) {
if recipient.hasProfilePicture {
if let image = recipient.profilePicture {
completionBlock(image) // ⚠️ <- forgot to return after calling the block
}
}
...
}
completionBlock호출하고 나서 return호출하는 것도 까먹을 수 있음 ㅎ
(guard는 return을 하지 않으면 컴파일에러를 주긴 한다만..항상 guard를 쓰는건 아니니까)
# 해결
위와같은 문제를 해결하기 위해 async-await proposal은 Swift에 코루틴(coroutine) 모델을 도입.
➞ 비동기 함수의 semantics 정의 But 동시성을 제공하지는 않음.
비동기함수(async/await)를 사용하면?
- 비동기 코드를 마치 동기 코드인것 처럼 작성 할 수 있음. ➞ 프로그래머가 동기 코드에서 사용할 수 있는 동일한 언어 구조를 최대한 활용 가능.
- 자연스럽게 코드의 의미 구조를 보존 ➞ 언어에 대한 최소한 3가지 교차 개선에 필요한 정보를 제공...(뭔소리지)
1) 비동기 코드의 성능 향상(better performance for asynchronous code)
2) 코드를 디버깅, 프로파일링 및 탐색하는 동안 보다 일관된 경험을 제공하기 위한 더 나은 도구 (better tooling to provide a more consistent experience while debugging, profiling, and exploring code)
3) 작업 우선 순위 및 취소와 같은 동시성 기능을 위한 기반. (a foundation for future concurrency features like task priority and cancellation)
# 예제
위에서 봤던
func processImageData1(completionBlock: (_ result: Image) -> Void) {
loadWebResource("dataprofile.txt") { dataResource in
loadWebResource("imagedata.dat") { imageResource in
decodeImage(dataResource, imageResource) { imageTmp in
dewarpAndCleanupImage(imageTmp) { imageResult in
completionBlock(imageResult)
}
}
}
}
}
processImageData1 { image in
display(image)
}
코드를 async/await를 사용하여 리팩토링하면,
func loadWebResource(_ path: String) async throws -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image
func dewarpAndCleanupImage(_ i : Image) async throws -> Image
func processImageData() async throws -> Image {
let dataResource = try await loadWebResource("dataprofile.txt")
let imageResource = try await loadWebResource("imagedata.dat")
let imageTmp = try await decodeImage(dataResource, imageResource)
let imageResult = try await dewarpAndCleanupImage(imageTmp)
return imageResult
}
짜잔
이렇게 마치 동기적으로 일어나는 것 처럼 코드를 작성할 수 있게 된다.
어떤식으로 사용하는지는...좀 사용해보고 보겠음.
# 사용해보기 (1)
Swift 5.5는 아직 릴리즈는 안됐고..스냅샷을 다운받아야 한다.
(Swift Snapshot써보기 글 참고)
Snapshots의 Xcode를 다운 받아준다.
Swift Snapshot써보기에 다 있으므로..따로 설명은 안함.
이렇게 뜨면 된것.
# 사용해보기 (2)
[1] -Xfrontend -enable-experimental-concurrency 플래그 추가
🚨 만약 Xcode 13 베타를 사용중이라면 안해도 됨 🚨
그냥 iOS 앱 프로젝트를 만든다고 치겠음.
타겟 > Build Settings > Other Swift Flags 선택
-Xfrontend -enable-experimental-concurrency추가.
이렇게 되어있으면 된다.
(사실 이걸 안해도 async/await는 사용할 수 있는 것 같은..?)
[2] 콜백 지옥 만들기
override func viewDidLoad() {
super.viewDidLoad()
self.getData { data in
self.decode(data: data) { contents in
print(contents) // "<!doctype html>~~~~
}
}
}
func getData(completion: (Data) -> Void) {
let url = URL(string: "https://zeddios.tistory.com")!
let data = try! Data(contentsOf: url)
completion(data)
}
func decode(data: Data, completion: (String) -> Void) {
let contents = String(data: data, encoding: .utf8)!
completion(contents)
}
대충 이런코드를 만들어줬다. 강제 언래핑과 try!는 일단 무시좀..
이걸 async/await를 바꿔보자
[3] completion을 return으로 바꿈
func getData() -> Data {
let url = URL(string: "https://zeddios.tistory.com")!
let data = try! Data(contentsOf: url)
return data
}
func decode(data: Data) -> String {
let contents = String(data: data, encoding: .utf8)!
return contents
}
[4] 메소드에 async 키워드 추가
func getData() ✅ async ✅ -> Data {
let url = URL(string: "https://zeddios.tistory.com")!
let data = try! Data(contentsOf: url)
return data
}
func decode(data: Data) ✅ async ✅ -> String {
let contents = String(data: data, encoding: .utf8)!
return contents
}
[5] 사용하는 쪽에서 await와 함께 호출해준다.
func process() async {
let data = ✅ await ✅ self.getData()
let contents = ✅ await ✅ self.decode(data: data)
print(contents)
}
[6] process 호출
override func viewDidLoad() {
super.viewDidLoad()
Task {
await self.process() // <!doctype html>~~~~
}
}
이런식으로 하면 된다.
일단 흐름만 보여주기 위해서 상세한 설명은 안했는데...
async/await를 하나씩 봐보자.
async
- 함수를 비동기 함수로 만들겠다.
[특징]
1. 프로토콜에서도 요구 가능하다.
protocol SomeDelegate {
func process() async
}
2. async와 throws를 같이 쓸 수 있다.
func getData() ✅ async throws ✅ -> Data {
let url = URL(string: "https://zeddios.tistory.com")!
let data = try Data(contentsOf: url)
return data
}
주의할 점은 반드시 async 키워드가 throws키워드 보다 먼저 선언되어야 한다.
throws async 이런거 안됨. rethrows역시 마찬가지.
await
- 비동기 함수 호출시 potential suspension point(잠재적인 일시 중단 지점) 지정
func process() async {
let data = await self.getData()
let contents = await self.decode(data: data)
print(contents)
}
예를 들어, getData(), decode()를 호출하는 동안 작업이 일시 중단 되어야 함.
(왜냐면 data 진짜 다 가져올 때 까지 작업을 더 진행 시키면 안되니까!!)
따라서 각 비동기 함수 호출 지점에는 potential suspension point(잠재적인 일시 중단 지점)이 있어야 하고
그것을 await로 할 수 있음.
suspension point에 관한 더 자세한 이야기는 proposal 참고
만약 비동기 함수가 에러를 던질 수 있다면,
func getData() async ✅ throws ✅ -> Data {
let url = URL(string: "https://zeddios.tistory.com")!
let data = try Data(contentsOf: url)
return data
}
func decode(data: Data) async ✅ throws ✅ -> String {
let contents = String(data: data, encoding: .utf8)!
return contents
}
func process() async {
do {
let data = ✅ try ✅ await self.getData()
let contents = ✅ try ✅ await self.decode(data: data)
print(contents)
} catch {
}
}
await 역시 try와 함께 써줘야 한다.
💡 proposal에서는 async는 "keyword", await는 "operand(피연산자)"로 표현하고 있다.
💡 글 내에서 "함수"와 "메소드"를 혼용해서 사용하고 있는데..함수 vs 메소드의 정의에 따라
의도적으로 달리 사용했습니다.
혹시나 왜 다르게 사용했지 라고 생각하실 분들을 위해 남깁니다..!
참고
github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md
'Swift > Concurrency' 카테고리의 다른 글
Combine → Swift Concurrency(async/await)로 바꾸기 (feat. 느낀점) (3) | 2022.01.25 |
---|---|
Actor (4) - Sendable (3) | 2021.10.11 |
Swift ) Actor (3) - isolated parameter / nonisolated keyword (1) | 2021.08.29 |
Swift ) Actor (2) - Actor isolation (7) | 2021.08.23 |
Swift ) Actor (1) (5) | 2021.08.22 |
- swift sort
- IOS
- WidgetKit
- swift tutorial
- WKWebView
- 피아노
- swift delegate
- swift 공부
- Xcode
- 스위프트
- np-complete
- fastlane
- UIBezierPath
- iOS delegate
- ios 13
- Accessibility
- Combine
- 제이슨 파싱
- swift array
- 회고
- actor
- swift3
- Git
- FLUTTER
- 스위프트 문법
- WWDC
- Swift
- SwiftUI
- np-hard
- github
- Total
- Today
- Yesterday