티스토리 뷰

반응형

안녕하세요 Pingu입니다.🐧

 

오늘은 WWDC 2021의 "Meet async / await in Swift"라는 영상을 정리한 글을 써보려고 합니다.

영상을 한 문장으로 요약하자면 "Swift 5.5에서 추가된 async / await 상세 사용법 및 동작원리" 정도? 였던 거 같습니다.

Meet async / await in Swift

Swift에 추가된 async / await로 이제 쉽고 안전하게 비동기 코드를 작성할 수 있다고 합니다!

지금까지 저도 위와 같이 completionHandler나 delegate 패턴을 활용해서 비동기 처리를 했었는데요, 이렇게 하면 비동기로 프로그램을 작성할 수 있었습니다.

위의 그림을 보면, 첫 번째 스레드는 동기 코드이고, 두 번째 스레드가 비동기 코드입니다. 비동기 코드로 프로그램을 작성하면, 어느 정도 시간이 걸리는 작업을 수행할 때 해당 작업이 완료되기를 기다리며 스레드를 차단하지 않고, 스레드가 다른 일을 하도록 합니다. 그러다가 수행하던 작업이 완료되면 completionHandler를 호출하여 다음 작업을 진행하게 되죠.

 

비동기 작업의 예를 보면 바로 이해가 됩니다.

위와 같이 앱에서 여러개의 이미지를 서버에서 받아올 때 이미지들이 순차적으로 로딩되는 것을 본 경험은 다들 해보셨을 거예요. 이러한 작업이 비동기 작업인데요, 해당 작업이 처리되는 과정은 아래와 같습니다.

 

  1. URL을 통해 썸네일 이미지를 요청하기 위한 URLRequest를 생성합니다.
  2. URLSession의 dataTask 메서드에서 URLRequest에 대한 데이터를 가지고 옵니다.
  3. 가지고 온 데이터를 UIImage의 init(data:)를 통해 이미지로 만듭니다.
  4. 만들어진 이미지를 prepareThumbnail 메서드로 썸네일을 만들어서 화면에 보여줍니다.

위와 같은 과정이 순차적으로 진행되어야 원하는 작업이 수행될 수 있어요. 그런데 여기서 1, 3번 작업은 빠르게 처리되겠지만 2,4번 작업은 시간이 좀 걸릴 수 있습니다. 특히 2번 작업의 경우엔 이미지의 크기가 크다면 상당히 오래 걸릴 수도 있죠. 또한 아까의 예와 같이 여러 개의 이미지 데이터를 다운로드하려면 시간이 더 필요할 수도 있습니다. 따라서 이미지를 다운로드하는 동안 다른 작업을 수행하고 싶고, 이를 위해서는 비동기 코드를 작성해야 합니다! 이를 위해 지금까지 작성했던 코드는 아래와 같습니다.

 

이걸 보고 애플에서도 비슷하게 코드를 짜는구나 ㅎㅎ 라고 생각했어요.🤣

 

어쨌든 코드를 보면 dataTask 메서드에서 여러개의 예외들을 조건문으로 분기하고 해당 예외에 맞는 에러로 completion 메서드를 호출하는 것을 볼 수 있습니다. 원하는 대로 데이터가 잘 넘어와도 UIImage 생성자에서 오류가 발생하면 guard let 구문으로 처리해주고 prepareThumbnail 메서드에서도 thumbnail이 잘 만들어졌는지 guard let으로 처리해주는 것도 볼 수 있어요.

 

근데 위의 코드를 보면 guard let 구문에서는 그냥 return만 작성되어있고 completion 메서드를 호출하지 않는 것을 볼 수 있습니다. 이건 개발자가 실수를 한 건데요, 그래서 수정한 코드는 아래와 같습니다.

 

이렇게 모든 예외상황에 completion을 호출해줘야 어떤 상황이 발생하더라도 원하는대로 작업이 수행될 수 있습니다.

 

일반 함수는 throw 키워드를 사용해서 에러를 던질 수 있는데, 이러한 completion 메서드를 사용하는 함수에서는 오류를 throw 할 수 없어서 Swift가 오류를 인지할 수가 없습니다. 즉 사용자가 사용하다가 우연히 오류를 발견해야 합니다. 이건 이미 사용자에겐 오류가 발생했으니.. 문제가 됩니다.

 

그리고 위와 같이 에러를 포함한 정상 처리까지 총 5개의 completion 메서드 호출을 작성해야 원하는 작업을 만들 수 있어서 코드가 지저분합니다.

방금 작업을 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
}

함수 선언부를 보면 async 키워드가 있는것을 볼 수 있습니다. 그리고 에러 처리를 위한 throws 키워드도 보입니다. 즉 아까 방식과는 다르게 async / await를 사용하면 에러 처리도 간단하게 할 수 있다는 말이 됩니다!

 

thumbnailURLRequest 메서드는 동기 코드니까 스레드를 차단한 상태로 진행되고, 그 다음 줄부터 비동기로 수행됩니다. data(for:) 메서드로 데이터와 HTTP 응답을 받아오는데, 이 코드 앞에 await라는 키워드가 보일 거예요. 이 코드가 있으면 해당 작업을 수행하는 동안 스레드를 점유 해제합니다. 그러면 다른 작업들이 수행될 수 있게 됩니다. 이렇게 async 함수에서 특정 작업이 수행되는 것을 기다리고 싶다면 await 키워드를 사용하면 됩니다.

 

그리고 뭐 어디서든 에러가 발생하면 아까와 다르게 그냥 던져버리면 됩니다. 그러다가 생성된 이미지의 썸네일 이미지를 만드는 곳에서 다시 await 키워드를 사용해서 해당 작업이 완료되는것을 기다리며 스레드를 반환합니다. 물론 여기서도 오류가 발생하면 에러를 던집니다.

 

async / await를 사용하니까 아까 20줄 가까이 되던 코드가 6줄로 줄어든 것을 볼 수 있습니다! 또한 코드가 수행순서대로 작성되어 있으며 에러 처리도 쉽게 할 수 있고 아까와 같이 에러를 빼먹는 실수도 방지합니다.

 

코드를 좀 더 자세히 보면, 마지막 썸네일을 만드는 코드는 함수 호출이 아니었는데도 await를 사용할 수 있었습니다. 즉 함수가 아니더라도 비동기로 만들 수 있답니다! 해당 코드를 보면 아래와 같습니다.

위와 같이 UIImage의 read only 프로퍼티로 thumbnail이 있고 해당 프로퍼티의 getter에는 async 키워드가 붙어있습니다. 이렇게 정의되어 있기 때문에 아까와 같이 await를 사용할 수 있었던 거죠. 또한 Swift 5.5부터는 getter에도 throw를 사용해서 에러 처리를 할 수 있다고 합니다. 그리고 중요한 부분인데 read only 프로퍼티에만 async를 사용할 수 있습니다.

 

이렇게 함수, 프로퍼티, 생성자에서 스레드 사용을 일시 중단할 위치를 나타내기 위해 await를 사용하면 됩니다.

 

이 외에도 await를 사용할 수 있는 부분이 있는데 for in 루프에서 async sequence를 사용할 수 있습니다.  Async sequence는 item을 비동기적으로 제공한다는 점을 제외하고는 일반적인 sequence와 동일하다고 합니다.

 

위 코드와 같이 여러개의 값들에 대해 비동기 작업을 진행할 수 있습니다. 그리고 아직은 for 루프에서만 await를 사용할 수 있다고 합니다.

 

Async Sequence에 대한 좀 더 자세한 내용은 아래 영상들을 참고하라고 합니다.

 

그럼 이렇게 async 함수를 실행하다가 await 키워드를 만나면 어떤 일이 발생하는지 살펴보도록 하겠습니다. 먼저 일반적인 함수의 경우입니다.

일반적인 함수를 호출하면 스레드도 함께 해당 함수에 넘겨줍니다. 그리고 호출된 함수는 스레드를 자신의 작업이 끝날 때까지 가지고 있죠. 함수가 종료되기 위해서는 정상적으로 값을 반환하거나 에러를 발생해야 하고, 이렇게 종료되면 스레드를 다시 호출자에게 넘겨줍니다. 즉 함수가 스레드를 반환할 수 있는 유일한 경우는 함수가 종료되는 경우입니다.

 

비동기 함수의 경우 일반적인 함수와 다릅니다. 일반 함수는 함수가 종료되는 경우에만 스레드를 반환할 수 있었지만 비동기 함수의 경우 suspending(일시 정지)를 통해 스레드를 반환할 수 있습니다. 비동기 함수도 호출될 때는 스레드에 대한 제어권을 받게 됩니다. 그러다 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
}

그럼 일시정지 됐들 때 어떤 일이 발생할 수 있는지 보기 위해 아까 코드를 다시 보겠습니다. URLSession.shared.data 메서드를 호출하면 스레드를 일시 정지할 수 있습니다. 아까 언급한 대로 이 경우 스레드를 시스템에 반환하고, 시스템은 URLSession.shared.data 메서드에 대한 작업을 예약합니다. 이 경우 해당 작업이 바로 시작될 수도 있고 다른 작업이 실행될 수도 있죠. 운영체제 시간에 배운 스케줄링 기법처럼 작업이 스케줄링되는 느낌입니다.

 

해당 작업이 예약되어 있는 상태에서 위와 같이 버튼을 탭해서 likeCurrentPost()라는 메서드를 호출합니다.

해당 메서드는 업로드하는 작업을 수행하게 되는데 위와 같이 방금 시스템에 반환된 스레드로 해당 작업을 실행할 수 있습니다. 물론 다른 작업들도 수행할 수 있겠죠? 이렇게 수행하다가 데이터 다운로드를 마치면 반환되어 호출자로 돌아가 다음 작업을 수행하게 됩니다.

 

이렇게 일시정지된 상태에서 다른 작업들이 수행될 수 있기 때문에 앱 상태가 일시정지 중에 변할 수 있음을 유의해야 합니다. 즉 async / await 블록은 하나의 트랜잭션으로 실행되지 않는다는 것을 알 수 있습니다. 또한 반환한 스레드가 아닌 다른 스레드에서 실행될 수도 있다고 합니다. 이는 공유자원이 보호될 수 없을 수도 있음을 의미하며 이를 해결하기 위해서 Actor라는 것이 있는데, 이에 대한 자세한 정보는 아래 영상에서 확인하라고 하네요.

 

async / await에 대해 기억해야할 몇 가지를 정리해보겠습니다.

  • 함수를 async로 작성하면 일시정지할 수 있습니다. 이 경우 자신을 호출한 호출자도 일시 중지됩니다. 따라서 호출자도 async로 정의되어 있어야 합니다.
  • 비동기 함수에서 일시정지될 수 있는 위치를 알리기 위해 await 키워드를 사용합니다.
  • 비동기 함수가 일시정지되는 동안 스레드는 시스템에 반환되어 다른 작업을 수행할 수 있으므로 앱 상태가 원하지 않게 변할 수 있습니다.
  • 비동기 함수가 다시 시작되면 호출한 비동기 함수의 반환 결과가 호출자로 전달되고 중단된 곳, 즉 await 키워드 다음 부분부터 다시 실행됩니다.

 

Migration with async / await

Testing

여기부터 다른 사람이 영상에 등장하면서  async / await가 생겨나면서 이전에 만들어뒀던 코드들을 수정해야 하는 일도 해줄 필요가 있는데, 이런 경우들을 보여준다고 합니다. 그중 Testing 코드부터 언급하네요.

 

Swift에서는 테스트 코드를 위해 XCTest 프레임워크를 제공합니다. 위의 코드는 XCTest를 활용해서 만든 async / await를 적용하기전 테스트입니다. XCTest에서는 wait라는 함수로 특정 작업들이 성공적으로 수행되는지 확인하는 코드들이 있었는데, 이게 임의로 시간을 설정해야 하다 보니 공부할 때 느낀 바로는 크게 효율적으로 보이진 않았습니다.

 

근데 이제 async / await를 적용해서 테스트를 작성하면 아까의 코드보다는 훨씬 짧고 직관적으로 코드를 수정할 수 있습니다. 시간을 임의로 결정하는 것이 아닌, 그냥 작업이 끝나면 Assertion을 처리하는 방식으로 바뀌었네요.

 

Application (SwiftUI)

다음은 앱 코드에 async / await를 적용해봅시다.

 

위 코드는 SwiftUI 코드로 뷰가 비동기적으로 id를 가지고 썸네일을 가지고 오는 코드입니다. 이를 async / await로 수정하면 아래와 같아집니다.

 

바뀐 부분은 아래와 같습니다.

  • completion Handler를 제거하고 try로 오류 처리를 가능하도록 만들고, 비동기 처리를 위해 await 키워드를 써줍니다.
  • Swift 컴파일러는 async 컨텍스트가 아닌 곳에서는 비동기 함수를 호출할 수 없기 때문에 Task로 해당 부분을 감싸줍니다.
    • 즉 async 컨텍스트가 아닌 곳에서도 비동기 함수를 호출할 수 있다는 것을 의미합니다.

SwiftUI에서의 비동기 코드를 잘 활용하는 상세한 방법은 아래 영상을 확인하라고 합니다.

이렇게 해서 테스트와 앱 코드까지 새로 나온 async / await로 변경이 가능하게 됐습니다.👍

 

Async API in the SDK

위와 같이 수많은 SDK들이 기존에는 completion handler로 비동기 작업을 제공했는데, 이제는 async / await로 더 자연스럽게 동일 기능을 제공한다고 합니다.

 

위와 같이 기존에 completion handler로 비동기 작업을 제공하던 API들은 다들 비슷한 모양을 가지고 있는데요, 이걸 이제 async / await를 사용해서 바꾸면 아래와 같아집니다.

API 자체가 훨씬 간단하고 직관적으로 표현되는 것을 볼 수 있습니다! 컴파일러가 예전 코드를 사용하면 async / await를 사용하라고 제안한다고 하니... 기존 코드에 노란색 경고가 많아질 것 같은 느낌이 드네요.🥲

 

또한 Delegate 패턴을 사용할 때도 completion handler를 많이 사용하는데 이러한 부분들도 모두 async / await를 지원하도록 개선했다고 합니다.

 

이렇게 기존 API들이 많이 변화되었는데 각각의 API에 대한 자세한 내용들은 아래 영상들을 참고하라고 합니다.

 

Async alternatives and continuations

이렇게 async / await가 생겨났고 많은 API들이 이를 지원하지만, 개발을 하다 보면 어쩔 수 없이 비동기 코드를 직접 만들어야 하는 경우도 생길 수 있습니다.

위 코드는 getPersistentPosts 함수를 사용하여 Core Data의 저장소에 존재하는 모든 게시물을 검색합니다. 이 함수는 앱 전체에서 사용하고 있어서 모두 async / await를 적용한다면 큰 변화를 얻을 수 있을 것 같습니다. 이 함수는 NSAsynchronousFetchResult를 사용하고 있어서 비동기 작업을 하고 있으니 직접 비동기 코드로 만들어봅시다.

 

일단 위와 같이 async 함수로 만들고 오류도 처리하기 위해 throws도 추가했습니다. 여기서 아까의 getPersistentPosts 함수를 호출하면 completion handler를 처리해줘야 되는데... async 함수는 일시 정지되면 시스템에 스레드를 반환하기 때문에 다음 작업을 수행할 적절한 시점을 찾아야 합니다.

 

이를 자세히 살펴보면 위의 그림과 같습니다. persistentPosts가 호출되면 Core Data를 호출합니다. 그런 뒤 Core Data에서 작업을 마치면 getPersistentPosts의 completion handler를 호출하게 됩니다. 코드에 await 키워드만 없었을 뿐 동작 방식은 비슷한 거 같습니다.

 

즉 메서드 호출자는 함수 호출의 결과를 기다리고 결과를 얻은 뒤 할 일을 클로저(completion handler)로 정해줍니다. 함수 호출이 완료되면 호출자는 completion handler를 호출해서 비동기 기능을 처리합니다. 이를 명확하게 처리하기 위해 Swift에서는 높은 수준의 안전한 방식으로 작업을 create, manage, resume 할 수 있는 continuation 기능을 제공하며 다시 코드로 돌아가서 이를 적용해보면 아래와 같습니다.

위의 코드처럼 withCheckedThrowingContinuation 함수를 사용하면 비동기 Swift 함수를 오류를 처리할 수 있도록 해줍니다. 만약 오류를 처리하고 싶지 않다면 withCheckContinuation을 사용하면 됩니다. 이러한 함수는 일시 정지된 async 함수를 다시 시작하는 데 사용할 수 있는 continuation value에 접근하는 방법입니다.

 

Continuation value는 completion handler의 처리하기 위한 값을 제공합니다. 이렇게 하면 completion handler와 async 함수를 함께 사용할 수 있습니다. 이렇게 async 함수의 실행을 수동으로 제어하기 위해 continuation을 사용할 때 주의할 점들이 있습니다.

 

Continuation을 사용할 때 주의할 점은 resume을 한 번만 호출해야 한다는 점입니다. 호출하지 않아서도 안되고, 위와 같이 두 번 호출해서도 안됩니다. 특히 위와 같이 resume을 여러 번 호출하면 Swift는 런타임에서 fatal error를 발생시킵니다.

 

이 외에도 checked continuation을 사용할 수 있는 부분은 이벤트 기반의 API가 많은 부분입니다.

위와 같이 특수한 지점에서 앱에 알리고 적절하게 응답할 수 있도록 delegate calback을 많이 사용하는데요, async / await를 사용하기 위해서는 continuation을 저장하고 나중에 resume 해야 합니다.

 

Continuation을 포함해서 Swift 동시성의 lower level에 대한 자세한 내용은 아래 영상을 참고하라고 합니다.

 

Summary

이렇게 Swift에 추가된 async / await에 대해 알아봤고 정리를 해보면..

  • async / await가 런타임에서 작동하는 방식과 앱과 프레임워크에서 사용하는 방법을 알아봤습니다.
  • SDK에서 생성할 수 있는 async API가 있고, 기존 코드를 async로 연결하는 방법을 알아봤습니다.

 

이렇게 이번 영상은 마무리됩니다.

 

이번 영상을 통해 Swift 5.5에 추가된 async / await의 사용법과 동작 원리를 어느 정도 이해할 수 있었고, 다음으로 보고 싶은 영상 정보도 알 수 있어 좋았습니다.☺️ 뭔가 async / await가 중요해 보여서 깊게 공부하고 싶은 생각이 드는 거 같습니다.

 

그럼 오늘 WWDC 영상인 "Meet async / await in Swift"는 여기까지 정리하도록 하겠습니다.

 

감사합니다.😄

 

 

반응형

'Apple > WWDC 2021' 카테고리의 다른 글

[WWDC 2021] Meet AsyncSequence  (3) 2023.03.07
[WWDC 2021] Protect mutable state with Swift actors  (1) 2021.11.30
[WWDC 2021] Meet Group Activities  (1) 2021.10.12
[WWDC 2021] What's new in Swift  (0) 2021.09.24
[WWDC 2021] What's new in UIKit  (0) 2021.09.05
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함