기어가더라도 제대로

[iOS-WWDC] "Meet async/await in Swift" 요약 정리 본문

생각정리/번역글(blog, WWDC)

[iOS-WWDC] "Meet async/await in Swift" 요약 정리

Damagucci-juice 2023. 2. 4. 17:12

1. 비동기 처리를 위한 Completion Handler

  • 비동기 처리를 위해서 Completion handler를 많이 사용
  • 문제점
    • 비동기 작업이 실패하더라도 이 함수의 호출자(Caller)는 결과를 마냥 기다림
      • guard else { return } 을 사용하면 completion 에 담지 않아도 되는 경우가 생김
      • 에러 핸들링을 강제할 수가 없다는 게 문제
      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()
    • 이를 해결하기 위해서 Result<value, error> 를 만들었음
    • 그러나 여전히 들여쓰기나 에러 핸들링 때문에 코드를 이해하기 어려움
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()
}

2. async / await 대작전

  • 이런 고민을 말끔하게 씻어줄 녀석이 async/await
  • 들여쓰기 문제를 해결하기 위해 비동기 코드가 아니라 일반 코드처럼 선언할 수 있게 도와주는 키워드
    • 들여쓰기 문제가 해결
    • 코드 이해가 수월(위에 코드가 6줄로 끝남)
  • 읽기 전용 프로퍼티에도 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
}

extension UIImage {
    var thumbnail: UIImage? {
        get async {
            let size = CGSize(width: 40, height: 40)
            return await self.byPreparingThumbnail(ofSize: size)
        }
    }
}

3. 일반 함수와 비동기 함수(async func)의 차이

  • Thread를 제어권을 언제 넘길 수 있느냐 가 리빙 포인트

일반 함수

  • return이 호출되어야 caller 에게 제어권이 넘어감
  • 그 전까지는 제어권을 쥐고 있음

비동기 함수

  • return 호출되면 호출자(Caller)에게 제어권 넘어감
  • return 이 호출되기 전에도 쓰레드의 제어권을 Suspend 형식으로 포기 가능
    • 시스템에게 스레드의 제어 권한을 넘긴다는 의미

  • Suspending 이 일어날 수도 있고 안 일어날 수도 있는 지점을 알려주는 키워드가 await
    • 주의
      • await에서는 반드시 suspending이 일어난다 → X, 안 일어날 수도 있다 → O
      • await 에서는 단 한 번만 suspending이 일어 날 수 있다. → X, 여러번 일어날 수 있다 → O

Suspend가 일어나면 발생하는 일

  • await 가 있는 부분에서 suspend가 발생할 수 있는데, 그 때 시스템으로 제어권이 넘어감
    • “시스템아, 난 오래 걸리니까 너 먼저 할 일 있으면 해, 대신 일 끝나면 나한테 다시 스레드 제어권 돌려줘”
      • 이전 스레드와 다른 스레드를 받을 수도 있음
  • 다시 resume 할 수 있도록 시스템에 예약을 걸어 놓음
  • resume되면 await 이후로 돌아와서 나머지 일을 처리
  • 이것이 기술적으로 어떻게 가능한지는 mutate 상태를 보호하는 방법에 대한 학습 필요
  • Actor 키워드를 학습해야함

4. async/await 요약 정리

  • async는 함수를 suspend 시킬 수 있음
    • 해당 함수의 호출자(Caller)도 suspend 시킴
  • async 함수안에 await는 suspend를 실행할 수도 있음
  • suspend 상태에서 Thread-safe가 아니기 때문에 다른 일을 먼저할 수 있다.
    • 시스템이 다른 일을 예약
    • async 함수보다 늦게 시작되도 먼저 실행 가능
  • 기다렸던 비동기 호출이 완료되면, await 다음부터 원래 함수 실행

5. Task 훑어보기

  • 사용법
    • 동기적인 코드 흐름일 때, Task { } 내부에서는 비동기 코드가 들어갈 수 있다고 알려주는 유닛
    • 함수 자체가 비동기 함수(async 함수)일 땐 사용하지 않는다.
    • 동기 함수(일반 함수)안에서 사용
  • 역할
    • { } 내부를 패키징 해서 시스템에 다음 가용 쓰레드에서 즉시 실행할 수 있도록 보냄
    • 마치 DispatchQueue.global().async 와 비슷

6. Continuation 은 무엇일까?

  • 각종 SDK 안에 있는 컴플리션 핸들러들은 async 함수로 많이 변경이 되고 있음
    • ex) URLSession.dataTask -> URLSession.data
  • 그러나 아직 바뀌지 않은 경우나, 기존 프로젝트의 일반 코드를 async 함수로 변경을 도와주는 친구
  • Completion Handler 코드의 연장선으로써 resume 될 때 모든 상황을 다루어야함
// Existing function
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 ?? [], nil)
        }
        try self.managedObjectContext.execute(asyncRequest)
    } catch {
        completion([], error)
    }
}

// Async alternative
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)
            }
        }
    }
}

결론

  • async/await 아무렇게 쓰지 말고 알고 쓰자.
  • 아무렇게나 블로그, 스택오버플로우 코드를 긁어쓰다가 에러처리에 이틀 소모
  • “아~ WWDC와 공식문서는 신이구나” 하고서 공부에 돌입했는데, 시청과 필기 까지 3시간 30분 소요
    • 영어 실력이 늘면 더 빨리 학습 할 수 있음
  • 3시간 30분 아끼려다가 이틀씩 날리고 현타가 옴

출처

WWDC21 - “Meet async/await in Swift”

더 알아보기

Comments