아래는 WWDC21 Meet async/await in Swift 및 Swift concurrency: Behind the scenes를 보고 정리한 내용입니다!
만약 네트워크 통신을 통해서 이미지를 받아오고, 썸네일을 만들어본다고 가정해봅시다.
해당 과정은 아래의 도식화된 그림과 같은 과정을 따르게 됩니다.
근데, dataTask(with:completion:) 메소드와 prepareThumbnail(of:completionHandler:)의 경우에는 시간이 오래걸리기에 비동기 코드를 활용해야됩니다. 그러면 아래와 같이 completion handler를 활용한 코드로 구현할 수 있습니다.
위 코드에는 보자마자 알 수 있드시, 생길 수 있는 문제점들이 많습니다.
먼저 completionHandler를 호출하지 못할 가능성이 있습니다. 또한, completion handler가 연속되어 나올 경우, 장풍 코드(들여쓰기가 많은)가 만들어집니다.
그래서 위와 같이 Result 타입을 활용할 수 있지만, 이러한 방법도 가독성을 크게 증가시켜주지는 못합니다.
그러나, async / await를 활용할 경우 이러한 코드를 아래와 같이 작성할 수 있습니다.
위 코드에서 async는 해당 코드가 비동기 코드라는 것을 알려줍니다. 또한 해당 함수는 동시 컨텍스트(Concurrent Context)에서만 실행이 가능합니다. 그리고 async 함수는 await 키워드를 통하여 실행이됩니다. await 키워드를 통하여 비동기 함수가 언제 끝나는 지를 알 수 있습니다. 즉, await 키워드는 해당 작업이 끝날 때까지 기다리게 됩니다.
async, await은 위의 코드와 같이 읽기 전용 코드에서도 활용이 가능합니다.
위와 같이 for 문에서도 await을 활용할 수 있습니다. await 키워드가 붙은 작업이 끝나야지 다음 라인으로 진행이됩니다.
여기서 URL의 lines 프로퍼티는 URL에서 모든 line들을 가져오는 비동기 시퀀스를 생성하는 읽기 전용 프로퍼티입니다. 이 프로퍼티는 내부적으로 network request도 만들고, data를 fetching해오고, string으로 변환을 해줍니다.
그렇다면, 동기함수와 비동기함수의 스레드가 어떻게 작동하는 지 알아보겠습니다.
일반적으로 호출된 함수(sync 함수)는 왼쪽 그림과 같이 스레드를 자신의 작업이 끝날 때까지 가지고 있다가 함수가 끝나면 스레드 제어권을 호출자에게 넘깁니다. 즉, fetchThumbnail 함수가 thumbnailURLRequest 동기 함수를 호출하면, fetchThumbnail 함수는 스레드 제어권을 thumbnailURLRequest에게 주고, thumbnailURLRequest가 끝나서야 스레드 제어권을 받게됩니다. 즉, fetchThumbnail 함수는 thumbnailURLRequest 함수에게 스레드 제어권을 다시 받기 전까지 작업을 하지 못합니다.
그러나 비동기 함수의 호출은 오른쪽 위의 그림과 같이 앞선 일반 함수와는 조금 다른 과정을 가집니다. await 를 만나면 잠깐 suspend를 하여 thread 제어권을 system에게 주고, system은 스레드 제어권을 가지고 다른 작업을 하다가 어느 시점이 되었을 때 스레드 제어권을 호출자에게 넘깁니다. 그러면 비동기 함수를 마저 실행시킬 수 있게 됩니다. 이러한 코드의 일시 중단 지점을 스레드 양보라고도 합니다.
다시 정리해보자면, async 키워드는 함수가 suspend할 수 있게 합니다.(그렇다고 항상 하는 것은 아닙니다.) 그리고, await 키워드는 async 함수가 suspend할 수 있도록 마크를 해줍니다.(그렇다고 항상 suspend되는 것은 아닙니다.) 그리고 await을 통해서 쓰레드 제어권이 시스템에게 넘어감으로(suspend가 되어), 그 때는 시스템은 다른 작업을 수행할 수 있습니다.
테스트 코드에서도 async, await를 사용할 수 있습니다.
기존의 코드는 위와 같은 방식으로 비동기 함수를 테스트해야만 했습니다.
그러나, 이제는 기본적으로 XCTest가 async/await를 지원하기에 위와 같이 훨씬 짧은 코드로 테스트 코드를 작성할 수 있습니다.
SwiftUI에서도 async, await를 사용할 수 있습니다. 기존에는 위와 같이 completionHandler를 활용해서 비동기 코드를 핸들링했습니다.
그러나, 이제는 async, await를 통하여 같은 기능을 수행할 수 있습니다. 추가적으로 Task 클로져를 통하여 sync 컨텍스트와 async 컨텍스를 서로 이어줄 수 있습니다.
또한, 애플의 여러가지 SDK가 Async/Await API를 지원하고 있습니다.(ex. URLSession, AVPlayer...)
이러한 API들이 존재하지만, 어쩔 수 없이 직접 비동기 코드를 작성해야하는 경우가 생깁니다. 아래와 같은 코드를 통하여 해당 상황을 살펴봅시다.
getPersistentPosts 함수는 Core Data에 저장된 모든 게시물을들을 검색합니다. 왼쪽의 코드는 오른쪽과 같이 async를 활용한 persistentPosts()함수로 wrapping을 하여 변경될 수 있습니다. 근데 앞서알아봤다시피 await 키워드를 만나면 스레드 제어권을 시스템에게 주고, 적당한 타이밍에 system이 다시 스레드 제어권을 돌려준다고 했습니다. 그렇다면 위의 함수에서는 어떠한 방식으로 resume을 해주어야할까요?
getPersistentPosts는 CoreData에서 데이터를 비동기적으로 뽑아온 후, 작업이 끝나면 getPersistentPosts의 completionHandler를 호출하게 됩니다.
즉, 메소드의 호출자는 함수의 호출의 결과 값을 기다립니다.(await) 그리고 다음에 무엇을 할 지에관한 closure를 제공합니다.
함수 호출이 끝나게 되면, completion handler를 호출하여 받아온 result 값을 통하여 원하는 작업을 합니다. Swift는 이러한 작업을 하기위하여 continuation이라는 기능을 제공합니다.
위와 같이 withCheckedThrowingContinuation함수를 통하여 비동기 함수 실행 중 생기는 오류를 처리할 수 있습니다. 그리고 completionHandler에서는 continuation을 resume 해버립니다. 이러한 방식을 활용하면 async 함수와 completionHandler를 동시에 사용할 수 있게 됩니다. 쉽게 말해서 우리를 [Post]라는 return 값을 받기 위아여 위와 같이 async 함수와 continuation을 활용하여 completionHandler 속 값을 받아올 수 있습니다.
Continuation은 이와 같이 async 함수의 실행을 컨트롤하는 강력한 방법을 제공합니다. 그러나, 주의해야할 점들이 있습니다.
- resume은 오직 하나의 path에 한번만 호출이되어야합니다.
우리는 종종 continuation을 저장하고, 나중에 실행시켜야할 때가 있습니다. 이전과 같이 위의 예에서는 withCheckedThrowingContinuation을 생성합니다. 그리고, 저장을 한 후, 일을 시작합니다. 그러면 아래의 delegate 메소드는 해당 continuation을 실행시키고, nil로 할당을 해버립니다. 이렇게 우리는 async 호출을 임의적으로 resume할 수 있게 됩니다.
'Swift' 카테고리의 다른 글
[Swift] 셀 재사용에 따른 중복 binding 이슈(feat.disposeBag) (0) | 2024.04.09 |
---|---|
[Swift] Async, Await (2) [feat. WWDC] (1) | 2023.08.28 |
[Swift] WWDC21 - ARC in Swift: Basics and Beyond (0) | 2023.08.15 |
[Swift] [weak self]는 언제 사용할까? (0) | 2023.08.11 |
[Swift] WWDC16 Understanding Swift Performance(3) (0) | 2023.08.03 |