WWDC21 : Meet async/await in Swift 세션을 그대로 번역한 내용입니다.
Swift는 이제 비동기 함수를 지원합니다. 이 패턴은 흔히 async/await로 알려져 있습니다. 새로운 문법이 어떻게 코드를 더 읽기 쉽고 이해하기 쉽게 만들어주는지 알아보세요. 함수가 일시 중단(suspend)될 때 무엇이 일어나는지, 그리고 기존의 완료 핸들러(completion handler)를 비동기 함수로 어떻게 변환할 수 있는지도 배워보세요.
안녕하세요, 저는 Apple의 Swift 팀 엔지니어 Nate입니다.
오늘 제 동료 Robert와 함께 Swift의 async/await에 대해 설명드리겠습니다.
비동기 프로그래밍은 여러분 중 많은 분들이 정기적으로 수행하는 작업일 것입니다.
비동기 코드는 장황하고 복잡하며 심지어 잘못된 코드를 작성하기 쉽다는 점을 잘 알고 계실 겁니다.
Swift의 async/await 기능은 이러한 문제를 해결하는 데 도움을 줄 수 있습니다.
이를 사용하면 비동기 코드를 작성하는 것이 일반 코드를 작성하는 것만큼이나 쉽습니다.
그리고 이렇게 하면 여러분의 코드가 아이디어를 더 잘 반영하게 되고, 더 안전해집니다.
게다가 SDK에는 사용 가능한 수백 가지의 비동기 메서드가 있습니다. 예를 들어, UIKit은 UIImage에서 썸네일을 생성하는 기능을 제공합니다. 사실, 이 작업을 완료하는 동기 및 비동기 함수 모두를 제공합니다.
간단히 상기시키자면, 동기 함수(일반 함수)를 호출하면 스레드는 해당 함수가 끝날 때까지 차단됩니다.
예를 들어, fetchThumbnail 함수가 UIKit에서 제공하는 동기 함수 prepareThumbnail을 호출하면, 작업이 완료될 때까지 스레드는 다른 일을 할 수 없습니다.
반면, 비동기 버전의 prepareThumbnail(of:completionHandler:)을 호출하면, 작업이 진행되는 동안 스레드는 다른 작업을 자유롭게 수행할 수 있습니다. 작업이 완료되면 완료 핸들러를 호출하여 알려줍니다.
SDK는 다양한 비동기 함수를 제공합니다. 이 함수들은 몇 가지 방법으로 완료됨을 알려줍니다.
일부는 이처럼 완료 핸들러를 사용하고, 다른 일부는 대리자(delegate) 콜백을 사용합니다. 그리고 많은 함수들이 async로 표시되어 값을 반환합니다.
이 비동기 함수들이 공통적으로 가진 것은, 호출 시 스레드를 빠르게 해제하고 작업을 시작한다는 점입니다. 이를 통해 해당 작업이 완료되는 동안 스레드가 다른 작업을 수행할 수 있습니다.
이 차이를 확인하기 위해 많은 분들에게 익숙할 수 있는 예제를 살펴보겠습니다.
Robert와 제가 함께 만들고 있는 앱에서는 서버에 저장된 이미지의 썸네일을 표시하는 항목 목록이 있습니다. 목록에 표시할 썸네일을 준비할 때, fetchThumbnail 메서드가 호출됩니다. 이 메서드는 문자열을 여러 단계를 거쳐 UIImage로 변환합니다.
먼저, 뷰 모델의 thumbnailURLRequest 메서드가 문자열에서 URLRequest를 생성합니다.
다음으로, URLSession의 dataTask 메서드가 해당 요청에 대한 데이터를 가져옵니다.
그런 다음, UIImage의 initWithData가 데이터를 사용하여 이미지를 생성하고,
마지막으로 UIImage의 prepareThumbnail 메서드가 원본 이미지에서 썸네일을 렌더링합니다.
이 작업들 중 일부는 결과에 의존하므로 순차적으로 수행되어야 합니다.
이 작업들 중 일부는 빠르게 값을 반환합니다.
예를 들어, 문자열에서 URLRequest를 생성하거나 데이터를 사용하여 UIImage를 생성하는 작업은 빠릅니다.
따라서 이러한 작업은 해당 함수가 실행 중인 스레드에서 동기적으로 수행해도 괜찮습니다.
하지만, 다른 작업들은 시간이 걸립니다. 예를 들어, 이미지를 구성하는 모든 데이터를 다운로드하는 데 시간이 걸리며, 그 이미지에서 멋진 썸네일을 렌더링하는 것은 장치가 상당한 작업을 수행해야 합니다. 그래서 SDK는 이러한 작업을 완료하는 비동기 함수를 제공합니다. 따라서 이러한 호출은 비동기적으로 수행되어야 합니다.
Robert와 제가 async/await을 사용하기 전에는 이 함수를 완료 핸들러를 사용하여 작성했습니다. 이 함수는 문자열과 완료 핸들러를 인수로 받습니다. fetchThumbnail이 호출되면, 먼저 thumbnailURLRequest를 호출합니다. 이 메서드는 동기적이므로 완료 핸들러가 필요하지 않습니다.
다음으로, 공유된 URLSession 인스턴스에서 dataTask를 호출하고, 해당 URLRequest와 완료 핸들러를 전달합니다. 이 작업은 동기적으로 URLSessionDataTask를 생성해야 하며, 비동기 작업을 시작하려면 이를 재개해야 합니다. fetchThumbnail은 그 다음 반환되고, 스레드는 다른 작업을 수행할 수 있게 됩니다. 이는 이미지를 다운로드하는 데 시간이 걸리기 때문에 매우 중요합니다. 데이터를 스트리밍하는 동안 스레드를 차단하고 싶지 않기 때문입니다.
결국 이미지 다운로드가 완료되거나 문제가 발생하면, 요청이 완료되고 dataTask에 전달된 완료 핸들러가 데이터, 응답 및 오류와 함께 호출됩니다. 문제가 발생하면 완료 핸들러를 호출하고 오류를 전달해야 합니다.
모든 것이 정상적으로 작동하면, UIImage의 initWithData를 사용하여 데이터에서 이미지를 생성합니다. 이 작업은 동기적이므로 결과를 처리하기 위해 일반적인 직선 코드로 작성할 수 있습니다. 이미지가 생성되지 않으면 작업이 종료됩니다. 이미지가 생성된 경우, 마지막으로 UIKit의 prepareThumbnail 메서드를 호출하고 완료 핸들러를 전달합니다. 썸네일이 준비되는 동안 스레드는 차단되지 않고 다른 작업을 수행할 수 있게 됩니다.
썸네일이 준비되면, 완료 핸들러가 이미지가 생성되었는지 아니면 실패했는지 여부에 따라 호출됩니다. 생성에 성공하면, 이미지를 완료 핸들러에 전달합니다.
그러나 Robert가 지적했듯이 문제가 있습니다. fetchThumbnail의 호출자는 이 함수가 작업을 완료했을 때, 심지어 실패했을 때도 알림을 받기를 기대합니다. 그러나 현재 우리는 호출자를 방치하고 있습니다. 저는 "guard else return"을 작성하는 데 익숙해져서 완료 핸들러를 두 번 호출하는 것을 잊어버렸습니다. 따라서 데이터에서 UIImage를 생성하거나 썸네일을 준비하는 데 실패하면 fetchThumbnail의 호출자는 알림을 받지 못하고 목록의 해당 항목은 계속 로딩 중 상태로 남아 있게 됩니다.
그래서 fetchThumbnail을 작성하는 우리가 어떤 일이 발생하든지 간에 호출자에게 알리는 것이 매우 중요합니다. 따라서 함수의 모든 경로가 호출자에게 알림을 보내야 합니다. 이를 위해 오류가 발생하면 완료 핸들러를 호출하고 오류를 전달해야 합니다. 일반 함수는 오류를 던져서 호출자에게 오류를 반환합니다. Swift는 함수 실행이 어떻게 진행되든 값이 반환되지 않으면 오류를 던지도록 보장합니다. 그러나 이 경우 Swift의 일반적인 오류 처리 메커니즘을 사용할 수 없습니다. 문제가 발생하면 이러한 완료 핸들러 내에서 오류를 던질 수 없습니다. 이것은 불행한 일이지만, Swift가 우리의 작업을 검사할 수 없다는 것을 의미합니다.
⭐️ Swift는 완료 핸들러를 단지 클로저로 간주하기 때문에 우리는 완료 핸들러가 항상 호출되도록 하고 싶지만, Swift에서는 이를 강제할 방법이 없습니다. 그래서 두 번째 guard에서 그냥 반환했을 때 컴파일 오류가 발생하지 않았습니다. 이 문제를 해결하라고 Robert가 지적해주지 않았다면, 놓쳤을 것입니다. 따라서 여러분이 직접 완료 핸들러를 반드시 호출하도록 해야 합니다.
우리가 이 함수를 작성할 때, 그저 몇 가지 작업을 차례로 수행하고 싶었을 뿐입니다. 두 개는 동기적이고 두 개는 비동기적이어서 완료 핸들러를 사용해야 했습니다. 우리는 성공했지만, 결과적으로 20줄의 코드에 다섯 번의 미묘한 버그가 생길 가능성이 있는 코드가 만들어졌습니다. 우리가 원했던 것은 네 가지 작업을 순차적으로 수행하는 것이었지만, 실제로 얻은 것은 복잡하고, 이해하기 어렵고, 우리의 의도를 가리는 코드였습니다.
이 작업을 조금 더 안전하게 만들 수 있는 방법들이 있긴 했습니다.
예를 들어, 표준 라이브러리의 Result 타입을 사용할 수 있었습니다. 이는 약간 더 안전하지만, 코드가 더 복잡해지고 길어집니다.
다른 방식으로 비동기 코드를 개선하기 위해 Futures와 같은 기법도 사용되곤 했습니다.
하지만 이러한 접근법 중 어느 것도 간단하고, 쉽고, 안전한 코드를 제공하지는 않습니다.
async/await을 사용하면 더 나은 방법을 사용할 수 있습니다.
Robert와 저는 이 네 단계를 수행하는 함수를 async/await을 사용하여 다시 작성했습니다.
이 함수는 여전히 문자열을 인수로 받지만, 이번에는 완료 핸들러 대신 함수가 async로 표시됩니다.
함수가 async로 표시되면, 함수와 함수의 시그니처가 더 간단해집니다.
이미지가 성공적으로 썸네일로 변환되면 그 썸네일이 반환되고, 오류가 발생하면 오류가 던져집니다.
fetchThumbnail이 호출되면, 이전과 마찬가지로 thumbnailURLRequest를 호출하면서 시작됩니다.
이 함수는 동기적이므로 스레드는 작업을 수행합니다.
다음으로, data(for: request) 메서드를 호출하여 데이터를 다운로드하기 시작합니다.
이 메서드는 비동기적이며, dataTask와 달리 awaitable입니다. 호출된 후, 빠르게 자신을 일시 중단하고 스레드를 해제합니다. 스레드는 다른 작업을 수행할 수 있게 됩니다.
try는 data 메서드가 throws로 표시되었기 때문에 필요합니다.
이전 버전에서는 오류를 확인하고 이를 완료 핸들러로 명시적으로 전달해야 했습니다.
그러나 awaitable 버전에서는 이 모든 코드가 try 키워드로 간소화되었습니다.
throws로 표시된 함수를 호출할 때 try가 필요하듯이,
async로 표시된 함수를 호출할 때 await가 필요합니다.
비동기 함수 호출이 여러 개 있는 표현식의 경우, await는 한 번만 작성하면 됩니다. 전체적으로 함수 호출은 try await로 표시됩니다.
⭐️ 오류를 던질 수 있는 비동기 표현식을 처리할 때는 await 앞에 try를 두어야 합니다.
결국, 데이터 다운로드가 완료되면 data 메서드가 다시 시작되고 fetchThumbnail로 돌아갑니다. 이 시점에서 data 메서드가 반환한 값 또는 던진 오류가 전달됩니다. 오류가 발생하면 fetchThumbnail이 자체적으로 해당 오류를 던집니다. 그렇지 않으면 data 및 response 변수가 정의됩니다. 이는 이전 버전의 fetchThumbnail에서 URLSession의 dataTask 메서드에 전달된 완료 핸들러가 호출되었을 때와 유사합니다.
두 버전 모두 URLSession의 비동기 메서드가 생성한 값과 오류가 전달되었습니다. 그러나 awaitable 버전은 훨씬 간단합니다. 정확히 우리가 의도한 것을 표현합니다. 이 요청을 수행하고 반환된 값을 변수에 할당하여 사용할 수 있도록 합니다. 문제가 발생하면 오류를 던집니다.
다음으로 fetchThumbnail은 다운로드한 데이터에서 UIImage를 생성하려고 합니다.
이 작업이 성공하면 해당 이미지의 thumbnail 속성을 통해 썸네일을 렌더링합니다. 썸네일이 형성되는 동안 스레드는 다른 작업을 수행할 수 있으며, 결국 thumbnail 속성이 다시 시작되고 fetchThumbnail로 돌아옵니다. 썸네일이 렌더링되면 fetchThumbnail은 이를 반환합니다. 그렇지 않으면 오류를 던집니다.
완료 핸들러 버전과 달리 썸네일이 렌더링되지 않은 경우 Swift는 여기서 오류를 던지거나 값을 반환하도록 보장합니다. 우리는 조용히 실패할 수 없습니다.
그리고 이게 전부입니다.
이 코드는 이전에 완료 핸들러 버전이 했던 작업을 동일하게 수행합니다. 그러나 20줄의 코드 대신, 단 6줄만 있습니다. 그리고 모두 직선 코드입니다.
순차적으로 수행해야 하는 네 가지 작업이 차례대로 나열되어 있으며,
Swift는 문제가 발생하면 반환하거나 오류를 던지면서 항상 호출자에게 알리도록 보장합니다.
이것은 async/await을 사용하여 비동기 Swift 코드를 더 안전하고, 더 짧고, 의도를 더 잘 반영하도록 변환하는 방법의 한 예일 뿐입니다.
fetchThumbnail이 구현되는 방법의 세부 사항을 좀 더 살펴보겠습니다.
마지막에서 두 번째 줄에서는 함수 호출이 없더라도 썸네일 렌더링을 시작하는 표현식이 await로 표시되어 있습니다.
이는 thumbnail 속성이 비동기이기 때문입니다. 함수뿐만 아니라 속성도 비동기일 수 있으며, 생성자도 비동기일 수 있습니다.
이제 thumbnail 속성은 SDK의 일부가 아닙니다. 실제로는 Robert가 추가한 것입니다. 이를 살펴보겠습니다.
그는 UIImage에 대한 확장에서 이 속성을 정의했으며, 구현은 짧습니다. CGSize를 형성하고 이를 byPreparingThumbnail(ofSize)에 전달하는 결과를 기다립니다. 참고로, 이 메서드는 이전에 사용한 메서드의 awaitable 버전입니다.
몇 가지 주목할 만한 점이 있습니다.
⭐️ 첫째, 명시적 getter가 있습니다. 속성을 비동기로 표시하려면 이 작업이 필요합니다. Swift 5.5부터 속성 getter도 오류를 던질 수 있습니다.
그리고 비동기 함수 시그니처에서와 마찬가지로, 속성이 비동기이고 오류를 던질 수 있는 경우, async 키워드는 throws 바로 앞에 와야 합니다.
⭐️ 둘째, 이 속성은 setter가 없습니다. 오직 읽기 전용 속성만 비동기로 만들 수 있습니다.
함수, 속성 및 생성자에서 await은 함수가 스레드를 차단할 수 있는 위치를 나타내기 위해 표현식에 사용할 수 있습니다.
await을 사용할 수 있는 또 다른 장소는 for 루프에서 비동기 시퀀스를 반복하는 것입니다. 비동기 시퀀스는 일반적인 시퀀스와 비슷하지만, 요소를 비동기적으로 전달합니다. 따라서 다음 항목을 가져오는 작업은 비동기로 표시되어야 하며, await 키워드를 사용하여 이를 표시합니다.
함수가 비동기 시퀀스를 반복할 때, 다음 요소를 기다리는 동안 스레드를 차단하지 않고, 요소가 남아 있지 않은 경우 루프 후에 다시 시작됩니다.
AsyncSequence에 대해 자세히 알아보려면 "Meet AsyncSequence" 세션을 참조하세요.
여러 비동기 작업을 병렬로 실행하는 방법에 관심이 있다면 "Structured Concurrency in Swift" 세션을 확인해 보세요.
따라서 await을 사용할 수 있는 곳이 많습니다. 이 키워드는 비동기 함수가 그 지점에서 일시 중단될 수 있음을 나타냅니다.
비동기 함수가 일시 중단된다는 것은 무엇을 의미할까요?
이 질문에 답하기 위해 함수를 호출할 때 일어나는 일을 생각해 봅시다.
어떤 함수를 호출하든, 그 함수가 실행 중인 스레드의 제어권을 해당 함수에 넘깁니다. 여기서 thumbnailURLRequest와 같은 일반 함수(normal function)를 호출하는 경우, 스레드는 그 함수가 끝날 때까지 그 함수의 작업을 대신 수행하며 완전히 점유됩니다. 그 작업은 함수 자체의 본문에 있거나, 함수가 호출하는 다른 함수에 있을 수 있습니다.
결국 그 함수가 끝나면, 값을 반환하거나 오류를 던져서 제어권을 다시 여러분의 함수로 넘깁니다.
일반 함수가 스레드의 제어권을 포기할 수 있는 유일한 방법은 끝나는 것뿐입니다. 그리고 제어권을 넘겨줄 수 있는 대상은 여러분의 함수뿐입니다.
비동기 함수를 호출하는 경우에는 상황이 다릅니다. 일반 함수처럼 완료되면 제어권을 여러분의 함수로 다시 넘깁니다.
그러나 일반 함수와 달리, 비동기 함수는 스레드의 제어권을 일시 중단(suspending)이라는 완전히 다른 방식으로 포기할 수 있습니다.
일반 함수처럼 비동기 함수를 호출하면 해당 함수에 스레드의 제어권을 넘깁니다.
실행 중인 비동기 함수는 일시 중단(await)될 수 있습니다. 그렇게 되면 스레드의 제어권을 포기합니다.
하지만 제어권을 여러분의 함수로 되돌리는 대신, 시스템에 넘깁니다. 그 시점에서 여러분의 함수도 일시 중단됩니다.
일시 중단은 함수가 시스템에 "당신은 할 일이 많다는 걸 알아요. 무엇이 가장 중요한지 결정하세요."라고 말하는 방식입니다.
얼마나 협조적인가요? 함수가 자신을 일시 중단하면, 시스템은 그 스레드를 사용하여 다른 작업을 수행할 수 있게 됩니다.
어떤 시점에서 시스템은 가장 중요한 작업이 이전에 일시 중단되었던 비동기 함수를 계속 실행하는 것이라고 결정할 것입니다.
그 시점에서 시스템은 그것을 다시 시작합니다. 그런 다음 그 비동기 함수는 다시 스레드의 제어권을 되찾고 작업을 계속 수행할 수 있습니다. 그리고 원한다면 다시 일시 중단할 수 있습니다. 사실, 필요한 만큼 여러 번 일시 중단할 수 있습니다.
반면, 전혀 일시 중단할 필요가 없을 수도 있습니다. 비동기 함수는 일시 중단될 수 있지만, async로 표시된다고 해서 반드시 일시 중단된다는 의미는 아닙니다. 같은 맥락에서 await를 본다고 해서 함수가 반드시 그 지점에서 일시 중단된다는 의미는 아닙니다.
그러나 궁극적으로, 일시 중단 없이 또는 마지막으로 다시 시작된 후 함수는 종료되고, 스레드의 제어권을 원래 함수로 되돌리고 값이나 오류를 전달합니다.
fetchThumbnail을 다시 살펴보면서 일시 중단되었을 때 어떤 일이 발생할 수 있는지 보겠습니다.
fetchThumbnail이 URLSession의 비동기 data 메서드를 호출할 때, data 메서드는 비동기 함수만이 할 수 있는 특수한 방식으로 스레드에서 실행을 멈춥니다. 즉, 일시 중단(suspending)입니다.
메서드는 스레드의 제어권을 시스템에 넘기고, 시스템에 URLSession의 data 메서드 작업을 예약하도록 요청합니다.
그러나 이 시점에서 스레드는 시스템이 제어하고 있으며, 그 작업이 즉시 시작되지 않을 수도 있습니다.
스레드는 대신 다른 작업에 사용될 수 있습니다. 이러한 일이 발생할 수 있는 상황을 살펴보겠습니다.
예를 들어, 사용자가 fetchThumbnail이 호출된 후에 게시물에 대한 반응을 업로드하는 버튼을 탭했다고 가정해 보겠습니다.
이 경우, 시스템은 사용자의 반응을 게시하는 작업을 먼저 실행할 수 있습니다. 늦게 시작된 작업이 완료된 후, URLSession의 데이터 메서드가 다시 시작될 수 있습니다. 또는 시스템이 다른 작업을 대신 실행할 수 있습니다. 마침내 data 메서드가 끝나면, fetchThumbnail로 돌아갑니다.
함수가 일시 중단되는 동안 다른 작업을 수행할 수 있다는 사실이 Swift가 비동기 호출을 await 키워드로 표시해야 한다고 강조하는 이유입니다. 함수가 일시 중단되면, 앱의 상태가 크게 변할 수 있다는 사실을 인식해야 합니다.
물론, 완료 핸들러를 사용할 때도 마찬가지입니다.
그러나 비동기/대기(async/await) 코드에서는 모든 형식과 들여쓰기가 없어도 되기 때문에, await 키워드를 통해 코드 블록이 하나의 트랜잭션으로 실행되지 않는다는 것을 알 수 있습니다. 함수가 일시 중단될 수 있으며, 함수가 일시 중단된 동안 다른 일이 발생할 수 있습니다. 더 나아가 함수는 완전히 다른 스레드에서 다시 시작될 수도 있습니다.
이 문제에 대해 알아보려면 "Protect mutable state with Swift actors" 세션을 참조하세요.
⭐️⭐️⭐️⭐️ 여기서 async/await에 대해 기억해야 할 몇 가지 중요한 사항이 있습니다. ⭐️⭐️⭐️⭐️
첫째, 함수를 async로 표시하면, 일시 중단될 수 있습니다. 그리고 함수가 자신을 일시 중단하면, 호출자도 일시 중단됩니다. 따라서 호출자도 async여야 합니다.
둘째, 비동기 함수에서 한 번 또는 여러 번 일시 중단될 수 있는 위치를 표시하려면 await 키워드를 사용합니다.
셋째, 비동기 함수가 일시 중단된 동안 스레드는 차단되지 않습니다. 따라서 시스템은 다른 작업을 예약할 수 있습니다. 나중에 시작된 작업도 먼저 실행될 수 있습니다. 즉, 함수가 일시 중단된 동안 앱의 상태가 크게 변할 수 있습니다.
마지막으로, 비동기 함수가 다시 시작되면 호출한 비동기 함수에서 반환된 결과가 원래 함수로 다시 전달되며, 실행이 중단된 지점부터 계속됩니다.
이제 Swift에서 async/await이 어떻게 작동하는지 보셨습니다. 이제 Robert가 여러분의 프로젝트에서 이를 사용하는 방법을 보여드리겠습니다.
감사합니다, Nate. 이전에 Nate가 함께 만들고 있는 앱을 보여드렸습니다. 그가 async/await를 사용하도록 변환한 썸네일 함수가 몇 곳에서 호출되었기 때문에, 이를 비동기식으로 변경해야 합니다.
현대 소프트웨어 개발에서 중요한 것부터 시작하겠습니다. 바로 테스팅입니다.
우리는 비동기 코드를 테스트하는 것이 동기 코드를 테스트하는 것만큼 쉽기를 원했기 때문에, XCTest는 비동기를 바로 지원합니다.
이전에는 기대치를 설정하고, 테스트할 API를 호출하고, 기대치를 충족시키고, 임의의 시간 동안 기다리는 번거로운 과정을 거쳤지만,
이제는 테스트 함수에 async 키워드를 추가하고, XCTest 기대치와 그 충족을 제거한 후, Nate가 이전에 보여준 새로운 비동기 fetchThumbnail 함수를 호출한 결과를 기다리면 됩니다.
이제 테스트가 완료되었으니, 애플리케이션 코드 자체를 살펴보겠습니다. 특히 이 목록의 각 행에 있는 썸네일 뷰 뒤에 있는 SwiftUI 코드를 살펴보겠습니다.
이미지 셀이 게시물과 함께 생성되며, 각 게시물에는 ID가 있습니다. 우리는 이 ID를 viewModel에 전달하여 썸네일을 비동기적으로 검색할 수 있습니다. 여러분은 이미 테스트 코드에서 이 호출을 변환하는 방법을 보았으니, 한 번 해보겠습니다.
먼저 완료 핸들러를 제거한 다음,
오류를 처리하기 위해 try를 추가하고, 비동기 함수 호출을 완료하기 위해 await를 추가합니다.
그러나 이 코드를 빌드하려고 하면 문제가 발생합니다. Swift 컴파일러는 비동기 함수가 비동기화되지 않은 컨텍스트에서 호출될 수 없다고 경고합니다.
여기서 onAppear 수정자는 non-async 클로저를 사용하므로, 동기식 세계와 비동기식 세계 간의 간극을 연결할 방법이 필요합니다.
해결책은 async task 함수를 사용하는 것입니다.
async task는 클로저 내의 작업을 패키지화하여 다음 사용 가능한 스레드에서 즉시 실행할 수 있도록 시스템에 보냅니다.
(like the async function on a global dispatch queue)
여기서 주된 이점은 동기 컨텍스트 내에서 비동기 코드를 호출할 수 있다는 점입니다.
다시 빌드하니 컴파일러가 만족했습니다.
async tasks는 익숙하고 자연스럽게 구조화된 스타일로 풍부한 동시성 Swift 코드를 작성할 수 있는 API 제품군의 일부입니다.
자세한 내용을 알아보려면 "Explore structured concurrency in Swift" 세션을 참조하세요.
그리고 SwiftUI 앱에서 비동기 코드를 최대한 활용하는 방법에 대해 자세히 알아보려면 "Discover concurrency in SwiftUI" 세션을 참조하세요.
우리는 fetchThumbnail 함수를 호출하는 모든 곳을 마이그레이션하는 작업을 마쳤습니다. 하지만 우리 앱에는 async/await을 채택할 수 있는 기회가 훨씬 더 많습니다.
빠르게 시작하려면 기존 API의 비동기 대안을 작은 규모로 시작하는 것이 좋습니다.
SDK에는 비동기 방식으로 작업을 수행하기 위해 완료 핸들러를 사용하는 수백 가지 API가 제공됩니다.
이러한 API가 나란히 배열되면 패턴이 나타나기 시작합니다.
이름과 목적이 다르더라도 이 모든 함수는 동일한 필수 API 계약을 가지고 있습니다. 여러분이 호출하고, 제공된 완료 핸들러로 결과를 되돌려줍니다.
이전에 Nate가 비동기 함수를 await 해서 더 자연스러운 코드를 작성할 수 있음을 보여주었습니다. 이 콜백 블록을 이러한 비동기 함수로 변환할 수 있다면 멋지지 않겠습니까?
Swift 5.5부터 이것이 바로 일어납니다.
Swift 컴파일러는 Objective-C에서 가져온 완료 핸들러 코드를 자동으로 분석하고 비동기 대안을 제공합니다.
그러나 거기서 멈추지 않았습니다.
많은 대리자(delegate) API도 완료 핸들러를 전달하는 메서드를 포함합니다.
완료 핸들러를 호출하면 비동기 작업이 완료되었음을 프레임워크에 협력적으로 알립니다.
주어진 게시물에 대한 타임라인 항목을 표시하기 위해 fetchThumbnail을 호출하는 ClockKit ComplicationDataSource 를 예로 들어보겠습니다.
이전과 마찬가지로 모든 경로에서 완료 핸들러를 호출해야 하며, 클로저 때문에 많은 불필요한 코드가 존재합니다. async await을 사용하면 더 이상 그럴 필요가 없습니다.
이 대리자 메서드는 우리가 대신 사용할 수 있는 비동기 대안을 제공합니다.
먼저, 비동기 대안의 이름을 사용하여 앞부분의 "get"을 삭제합니다.
우리는 비동기 함수가 호출의 결과가 직접 반환되지 않을 때 이를 전달하는 "get"과 같은 선행 단어를 생략할 것을 권장합니다.
어쨌든, 이것은 비동기 대체 함수이므로 타임라인 항목을 직접 반환합니다.
이제 비동기 컨텍스트가 설정되었으므로, 비동기 버전의 fetchThumbnail을 호출합니다.
마지막으로, 이제 삭제된 완료 블록을 호출하는 대신 이 메서드에서 TimelineEntry을 반환합니다.
우리가 여기서 강조한 비동기 API는 빙산의 일각에 불과합니다.
자세히 알아보려면, API 자체 및 async/await을 채택할 때 사용할 수 있는 방법에 대해 더 자세히 설명하는 세션을 참조하세요.
이 모든 예는 Swift가 여러분을 대신하여 비동기 대안을 만들어주는 상황의 예입니다.
그러나 여러분의 코드에서 직접 비동기 대안을 만들어야 하는 상황도 분명히 발생할 것입니다. 실제로 이것이 어떻게 보이는지 살펴보겠습니다.
우리 앱에서 우리는 이 getPersistentPosts 함수를 사용하여 Core Data 저장소에 저장된 게시물을 검색합니다. 이 함수는 async thumbnail 함수보다 훨씬 더 많은 곳에서 호출되므로, 비동기를 어디에나 사용하는 것은 큰 변화일 것입니다. 그리고 우리는 NSAsynchronousFetchRequest를 사용하고 있기 때문에, 이 함수는 비동기 대안을 위한 완벽한 후보입니다.
먼저 비동기 함수를 만들고 반환 값을 변환합니다. 이 함수가 오류를 발생시킬 수 있으므로, 이 함수도 "throws"로 표시합니다.
다음으로 getPersistentPosts의 완료 핸들러 버전을 호출하고, 이제는 멈추게 됩니다. 콜백에서 반환된 결과를 비동기 persistentPosts 함수를 기다리는 위치로 다시 반환해야 합니다.
뿐만 아니라, 그 호출자들은 중단된 상태에 있습니다. 우리는 그들이 나머지 작업을 계속할 수 있도록 올바른 시점과 올바른 데이터에서 그들을 다시 시작해야 합니다.
이전에 Nate는 Swift와 시스템이 비동기 코드를 다시 시작(resume)하기 위해 어떻게 협력하는지 보여주었습니다. 이 중단/재개 프로세스가 어떻게 작동하는지 좀 더 깊이 파고들어 우리의 문제에 대한 유사한 해결책을 찾을 수 있는지 살펴보겠습니다.
비동기 버전의 persistentPosts가 호출되면 Core Data에 호출됩니다.
나중에 Core Data는 완료 핸들러를 호출하고 검색 요청의 결과를 전달합니다. 이 상황은 fetchThumbnail 함수가 시스템(이 경우 Core Data가 아니라)에 일시 중단된 비동기 함수 호출을 다시 시작하도록 요청했던 이전 상황과 거의 동일합니다. 여기서 누락된 것은 완료 핸들러를 기다리고 검색 요청의 결과로 다시 시작하기 위한 다리입니다.
이 패턴은 항상 등장하며, 이는 "continuation"이라는 이름을 가지고 있습니다. 이 세션 동안 Nate와 나는 이미 continuations의 예를 많이 보여드렸습니다. 바로 완료 블록을 사용하는 메서드입니다.
메서드의 호출자는 함수 호출의 결과를 기다리며, 다음에 수행할 작업을 지정하기 위해 클로저를 제공합니다.
함수 호출이 완료되면, 완료 핸들러를 호출하여 호출자가 결과로 수행하고자 하는 작업을 재개합니다. 이러한 종류의 협력 실행은 Swift의 비동기 함수가 작동하는 방식과 정확히 일치합니다.
이를 명시적으로 만들기 위해, Swift는 continuations을 높은 수준에서 안전하게 생성, 관리 및 재개할 수 있는 기능을 제공합니다.
우리 예제로 돌아가서, continuations가 비동기 대안을 작성하는 데 어떻게 도움을 줄 수 있는지 살펴보겠습니다.
withCheckedThrowingContinuation 함수는 오류를 포함한 완료 블록을 비동기 Swift 함수로 끌어올립니다. 이 함수는 오류를 발생시키지 않을 것이라는 것을 알 때 사용할 수 있는 withCheckedContinuation이라는 대응물이 있습니다. 이러한 함수들은 중단된 비동기 함수를 다시 시작하는 데 사용할 수 있는 연속 값에 접근하는 방법입니다. 이를 통해 getPersistentPosts 호출을 기다릴 수 있는 첫 번째 다리를 만들 수 있습니다.
다리를 완성해 봅시다.
continuation 값은 완료 핸들러에서 결과를 가져와서 넣을 수 있는 resume 함수를 제공합니다.
뿐만 아니라, resume은 persistentPosts 함수의 결과를 기다리는 모든 호출을 재개하는 데 필요한 링크를 제공합니다.
이렇게 해서 완료 핸들러에서 비동기 함수로 연결되는 완성된 다리가 하나로 정리된 것입니다.
continuations는 비동기 함수의 실행을 수동으로 제어할 수 있는 강력한 방법을 제공하지만, 몇 가지 유의해야 할 사항이 있습니다.
continuations에는 간단하지만 중요한 계약이 있습니다. 모든 경로에서 resume은 정확히 한 번 호출되어야 합니다.
하지만 걱정하지 마세요. Swift가 여러분을 도와줍니다. resume이 호출되지 않은 상태에서 continuation이 삭제되면, Swift 런타임은 비동기 호출이 다시 시작되지 않게 되므로 경고를 기록합니다.
그러나 동일한 함수에서 continuation이 여러 번 다시 시작되는 것은 더 심각한 오류입니다. 이는 프로그램 데이터가 손상될 수 있기 때문입니다. 이를 방지하기 위해 Swift 런타임은 resume을 여러 번 호출하려는 시도를 감지하고 두 번째 호출 시 치명적인 오류가 발생하도록 보장합니다.
이 점을 염두에 두고, checked continuations를 사용할 수 있는 중요한 곳을 하나 더 강조해 보겠습니다. 많은 API는 이벤트 기반입니다. 이러한 API는 특정 중요한 시점에서 우리 애플리케이션에 알림을 제공하고, 적절하게 응답할 수 있도록 대리자 콜백을 제공합니다. async/await을 제대로 채택하려면 continuation을 저장하고 나중에 이를 재개해야 합니다.
이전과 마찬가지로, checked continuation을 생성합니다.
그런 다음 이를 저장하고
작업을 시작합니다.
checked continuations의 API 계약을 존중하려면 활성 continuation을 재개하고,
마지막으로 이를 nil로 설정하여 동일한 값을 여러 번 호출하지 않도록 보호합니다.
항상 기억하세요: 여기서 checked continuation 값은 이 API에 대한 모든 비동기 호출을 수동으로 재개할 수 있는 능력을 나타내므로 모든 경로에서 호출되어야 합니다.
대리자 API가 여러 번 호출되거나, 특정 상황에서 전혀 호출되지 않는 경우, 모든 활성 continuation을 정확히 한 번 재개하는 것이 중요합니다.
Swift 동시성의 저수준 세부 사항, 특히 continuations에 대해 자세히 알아보려면 "Swift Concurrency: Behind the Scenes" 세션을 참조하세요.
이것으로 Swift의 async/await에 대한 개요를 마치겠습니다. 우리는 async 및 await 키워드가 런타임에서 어떻게 작동하는지, 그리고 이를 애플리케이션 및 프레임워크에서 사용하는 방법을 보여드렸습니다. 시작할 수 있도록, SDK에서 사용할 수 있는 비동기 API의 샘플을 제공하고, 동기식 세계에서 비동기식 세계로 코드를 전환하는 방법을 보여드렸습니다.
async/await은 Swift 동시성 기능의 전체 우주(eco-system)의 기초입니다. 여러분이 이 기능들을 활용하여 어떤 것들을 만들어낼지 기대가 큽니다. 시청해 주셔서 감사합니다.
'비동기와 반응형 > Swift Concurrency (고수준 비동기 처리)' 카테고리의 다른 글
비동기 작업 cancel 꼬꼬무해보기 (with LLDB 디버깅) (1) | 2024.10.05 |
---|---|
[Swift] Call to main actor-isolated instance method in a synchronous nonisolated context (0) | 2024.08.04 |