본문 바로가기
iOS/Swift

Meet async/await in Swift

by 바등쪼 2023. 2. 11.

https://developer.apple.com/videos/play/wwdc2021/10132/

 

Meet async/await in Swift - WWDC21 - Videos - Apple Developer

Swift now supports asynchronous functions — a pattern commonly known as async/await. Discover how the new syntax can make your code...

developer.apple.com

Sync와 Async

  • Synchronous 함수는 실행되면 해당 쓰레드를 블록하고 작업을 완전히 마치면 다음 작업(함수) 수행
  • Asynchronous 함수는 작업이 진행되고 있는 동안 해당 쓰레드가 다른 일을 할 수 있다. Asynchronous 작업이 완료되면 Completion Handler를 실행함으로서 동작 완료를 알린다.

예시

  • Thumbnail 이미지를 가져오는 과정은 위의 그림과 같다. 각각의 작업은 순차적으로 이루어져야 한다.
  • thumbnailURLRequestUIImage(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 키워드를 붙여 구현한다.
  • 구현 조건
    1. get 을 적어서 getter임을 명시해야한다.
    2. 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

댓글