https://developer.apple.com/videos/play/wwdc2021/10132/
Sync와 Async
- Synchronous 함수는 실행되면 해당 쓰레드를 블록하고 작업을 완전히 마치면 다음 작업(함수) 수행
- Asynchronous 함수는 작업이 진행되고 있는 동안 해당 쓰레드가 다른 일을 할 수 있다. Asynchronous 작업이 완료되면 Completion Handler를 실행함으로서 동작 완료를 알린다.
예시
- Thumbnail 이미지를 가져오는 과정은 위의 그림과 같다. 각각의 작업은 순차적으로 이루어져야 한다.
thumbnailURLRequest
와UIImage(data:)
작업은 빠르게 진행될 수 있어서 동기 호출로 수행하는 것이 좋다.- 그러나,
dataTask(with:completion:)
과prepareThumbnail(of:completionHanlde:)
는 작업의 시간이 오래걸린다. 이때는 비동기 호출을 해야한다. ⇒ 함수의 파라미터로completionHandler
를 받아서 작업 완료 시점을 알린다.
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()
}
썸네일을 가져오는 함수를 구현하면 위와 같다. completionHandler를 통해 이미지 fetching 완료후 데이터를 전달하고 있다.
문제점
- 만약 우리가 실수로 코드에 취소선이 있는 부분에서의 completion 블록을 빼먹는다면 이미지를 가져오는데 실패했다는 것을 알 방법이 없어진다.
- Swift에서는 함수에서 Error을 throw 하여 에러핸들링을 하는 기능을 제공하고 있지만 completionHandler에서는 해당 에러처리 메커니즘을 사용할 수 없다는 단점이 있다.
⇒ 콜백 지옥
Completion Handler를 통한 비동기 처리는 모든 분기처리 문에서 completion Handler 호출을 잊지말고 넣어야한다는 문제점이 있다. 만약 실수로 넣지 않았을 때 디버깅+에러처리가 힘들다.
func fetchThumbnail(for id: String, completion: @escaping (Result<UIImage, 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()
}
- Result 타입을 사용하면 이전보다는 안전한 코드를 작성할 수 있다. 그러나 코드가 길어지고 가독성도 나빠진다.
Async/Await
- async와 await를 이용해 다음과 같이 획기적으로 코드를 개선할 수 있다.
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
}
- 코드의 길이가 대폭 감소하였다.
- Error throw를 통해 에러 처리가 가능해졌다. (await 앞에 try 붙이기)
- 분기 처리 구문에서 Completion Handler 호출을 잊는 실수를 방지할 수 있다.
- 위의 예시에서
maybeImage?.thumbnail
는 sdk에서 제공하는 것이 아닌 직접 구현한 프로퍼티이다. → 비동기 작업을 하는 함수를 어떻게 구현했는지 알아보자
extension UIImage {
var thumbnail: UIImage? {
get async {
let size = CGSize(width: 40, height: 40)
return await self.byPreparingThumbnail(ofSize: size)
}
}
}
- UIImage를 extension 하여 thumbnail을 가져오는 클로져이다. async, await 키워드를 붙여 구현한다.
- 구현 조건
- get 을 적어서 getter임을 명시해야한다.
- read-only properties만 async가 가능하다. (setter 불가)
반복문에서의 비동기처리
for await id in staticImageIDsURL.lines {
let thumbnail = await fetchThumbnail(for: id)
collage.add(thumbnail)
}
let result = await collage.draw()
- 반복문에서도 await 키워드를 이용해 비동기 처리를 쉽게 할 수 있다.
Async, Await의 동작 원리
- 일반적인 함수는 다음 함수를 실행하면 쓰레드 제어권을 해당 함수(thumbnailURLRequest)로 넘기고 이 함수가 끝나면 다시 원래 함수(fetchThumbnail)로 제어권을 반환하는 방식을 사용한다.
- async 함수는 이와 다르게 동작하며 그림으로 표현하면 아래와 같다.
- async 함수가 실행되면 쓰레드 제어권이 해당 함수에게 주어진다. (여기까진 동일)
- 해당 함수가 동작하는 동안 async function은 suspend 할 수 있다.
- suspend를 하게 되면 async function은 쓰레드 제어권을 포기한다.
- 이때, 제어권은 다시 이전의 함수로 돌아가는 것이 아니라 System에게 돌아간다. (일반적인 함수와 큰 차이)
- System은 작업의 우선순위에 따라 다른 작업을 진행하게 된다.
- 특정 시점에서 System은 가장 중요한 작업 (우선순위가 높은 작업)이 이전에 중단되었던 비동기 기능을 다시 실행하는 것이라고 결정할 것이다.
- 이때, System은 resume을 통해 suspend 되어있던 작업을 다시 시작하게 된다. → aysnc function이 다시 쓰레드 제어권을 가지게 된다.
1. async function은 필요하다면 수차례 suspend 할 수 있다.
2. 반대로 필요하지 않다면 suspend가 아예 안 일어날 수도 있다.
즉, async await 키워드를 넣었다고 해서 suspend가 반드시 일어나는 것이 아니다.
Testing asnyc code
- 기존의 방식
class MockViewModelSpec: XCTestCase {
func testFetchThumbnails() throws {
let expectation = XCTestExpectation(description: "mock thumbnails copletion")
self.mockViewModel.fetchThumbnail(for: mockID) { result, error in
XCTAssertNoThrow(try await self.mockViewModel.fetchThumbnail(for: mockID))
expectaion.fulfill()
}
wait(for: [expectation], timeout: 5.0)
}
}
- async 키워드 사용
class MockViewModelSpec: XCTestCase {
func testFetchThumbnails() async throws {
XCTAssertNoThrow(try await self.mockViewModel.fetchThumbnail(for: mockID))
}
}
Bridging from sync to async
struct ThumbnailView: View {
@ObservedObject var viewModel: ViewModel
var post: Post
@State private var image: UIImage?
var body: some View {
Image(uiImage: self.image ?? placeholder)
.onAppear {
Task {
self.image = try? await self.viewModel.fetchThumbnail(for: post.id)
}
}
}
}
- onAppear는 non-async closure이기 때문에 Task 블록으로 감싸서 async 구문을 사용한다.
'iOS > Swift' 카테고리의 다른 글
Understanding Swift Performance (2) | 2023.03.13 |
---|---|
Explore structured concurrency in Swift (0) | 2023.02.11 |
Meet AsyncSequence (0) | 2023.02.11 |
Use async/await with URLSession (0) | 2023.02.11 |
Opaque Type (0) | 2023.02.11 |
댓글