WWDC/Swift

[Swift] WWDC21 - Meet async / await in Swift

유정주 2022. 9. 20. 13:57
반응형

서론

이번에 들은 WWDC 영상은 "Meet async / await in Swift" 입니다.

처음으로 async / await에 대해 소개해주는 영상인데요.

사실 지난 포스팅에서 다룬 내용과 겹치는 내용이 많아 복습하는 기분이 들긴 했습니다.

그래도 async / await는 아직도 어려운 부분이 있어서 정리하면 좋겠죠.

(아마 동일한 주제로 2~3개 더 올리지 않을까... 싶네요 ㅎㅎ;)

 

Functions: synchronous and asynchronous

UIKit에는 이미 많은 동기, 비동기 함수가 있습니다.

이번 영상에서는 UIImage를 예시로 들었습니다.

size에 맞게 썸네일을 가져오는 메서드로 위쪽 메서드는 동기 버전, 아래쪽 메서드는 비동기 버전입니다.

비동기 버전에는 escaping 클로저인 completionHandler를 전달 받는 것이 특징입니다.

동기 버전 메서드는 호출되면 스레드를 차단해서 해당 메서드가 종료될 때까지 다른 작업을 시작할 수 없습니다.

하지만 비동기 버전 메서드는 해당 메서드가 진행되는 동안 다른 작업을 수행할 수 있습니다.

메서드가 완료되면 completionHandler를 통해 사용자에게 완료되었음을 알려줍니다.

prepareThumbnail(of:completionHandler:)와 fetchThumbnail 사이의 빈 상자들이 다른 작업을 의미합니다.

 

대부분의 비동기 코드는 이런 방식으로 처리됩니다.

어떤 작업을 요청한 뒤 해당 thread는 다른 일들을 처리하다가, 나중에 완료 요청을 받으면 그때 이어서 처리합니다.

이로 인해 오래 걸리는 작업이라도 효율적으로 Thread를 다룰 수 있습니다.

 

썸네일을 생성하는 예시를 봅시다.

String를 이용해 썸네일 UIImage를 생성하는 과정입니다.

위 과정은 이전 작업의 결과에 따라 달라지기 때문에 반드시 순서대로 수행되어야 합니다.

4개 작업은 각각 수행 속도가 다릅니다.

String에서 URLRequest를 만드는 작업과 Data를 UIImage로 만드는 작업은 빠르게 끝낼 수 있습니다.

이렇게 빨리 끝나는 작업은 동기 호출로 해도 무방합니다.

 

하지만,

dataTask(with:completion:)과 prepareThumbnail(of:completionHandler:)는 시간이 오래 걸리는 작업입니다.

이 작업들을 동기로 하면 오랜 시간동안 앱이 멈출 것입니다.

따라서 이 작업들은 동기가 아닌 비동기적으로 이루어져야 합니다.

 

비동기 코드 - completionHandler 작성

그럼 completionHandler로 fetchThumbnail을 구현해 봅시다.

 

1. 함수 파라미터 정의

먼저 fetchThumbnail은 id String을 받아 썸네일을 생성하기 때문에 id를 입력 받습니다.

 

2. thumbnailURLReqeust

다음은 String을 URLReqeust로 변경합니다.

위에서 말했듯이 이 작업은 빠르게 끝나므로 동기적으로 수행이 가능합니다.

 

3. dataTask(with:completion:)

shared URLSession 인스턴스를 이용해 dataTask로 Reqeust를 수행합니다.

이 작업은 네트워크 작업으로 시간이 오래 걸립니다.

따라서 비동기적으로 수행해야 합니다.

dataTask가 호출되고 completion이 호출될 때까지 다른 작업을 수행합니다.

시간이 지나면 작업이 정상적으로 완료되거나 에러가 발생할 것입니다.

 

4. UIImage(data:)

3번 과정이 정상적으로 완료되면 completionHandler를 통해 data를 전달 받습니다.

이 data를 UIImage로 변경해야 합니다.

Data를 UIImage로 변경하는 작업도 금방 끝나기 때문에 동기적으로 수행됩니다.

data를 UIImage로 변경할 수 없다면 프로세스는 종료되고, 변경이 되었다면 5번 과정을 진행합니다.

 

5. prepareThumbnail(of:completionHandler:)

4번에서 생성한 이미지로 썸네일을 생성합니다.

prepareThumbnail 작업은 오래 걸리는 작업으로 비동기적으로 수행됩니다.

작업이 완료되면 thumbnail을 전달하거나 nil을 전달합니다.

thumbnail이 nil이라면 프로세스가 종료되지만, thumbnail이 존재한다면 completion으로 thumbnail을 전달합니다.

 

코드에서 문제점 찾기

1~5 과정에서 본 코드는 어떤 문제가 있을까요?

바로 이 부분입니다.

guard else return 패턴이 익숙한 나머지 무의식적으로 작성했는데요.

completion을 호출하지 않고 바로 return을 하면 호출부에서는 이 작업이 완료되었음을 알지 못합니다.

그래서 무한정 대기를 하게 되죠.

 

문제가 없게 하려면 이렇게 변경해야 합니다.

return 전에 completion을 호출하고 에러에 맞는 badImage를 전달합니다.

이제서야 호출부에서 에러가 났다는 것을 알게 되고 badImage를 세팅할 것입니다.

 

이렇게 completionHandler는 에러 상황에 있어 개발자의 실수가 발생할 가능성이 크고,

Swift의 에러 핸들링 방식인 throws를 활용하지 못합니다.

에러여도, 정상 호출이어도 모두 completion으로 처리하기 때문이 가독성도 좋지 않습니다.

위 예시 코드는 completion이 5개나 들어갔는데 이 중 하나라도 빠지면 수행에 문제가 생깁니다 ㄷㄷ...

 

Result 타입으로 바꾸기

좀 더 나은 가독성을 위해 Result 타입을 지원하는데요.

솔직히 더 나아진 거 같지도 않고 코드만 더 길어졌습니다 ..;;

 

Async / Await

async / await는 기존 비동기 방식보다 쉽고, 간단하고, 안전하게 비동기 처리를 할 수 있습니다.

completion을 이용한 코드를 async / await로 바꿔봅시다.

 

1. 함수 파라미터 정의

기존처럼 id를 입력받는데 completionHandler는 파라미터에서 사라졌습니다.

대신 async throws가 추가되었고, UIImage를 반환합니다.

 

async는 비동기로 수행된다는 것을 의미하고, throws는 에러가 발생할 수 있다는 것을 의미합니다.

만약 fetchThumbnail의 수행이 정상 완료하면 UIImage를 반환할 것이고, 에러가 발생하면 try - throws 구문에 의해 처리될 것입니다.

 

벌써 함수 파라미터가 직관적으로 변경되었네요.

 

2. thumbnailURLReqeust

thumbnailURLRequest는 기존과 동일합니다.

동기적으로 수행되어 Thread를 블로킹 합니다.

 

 

3. data(for:)

데이터를 요청하는 코드는 많은 변화가 생겼습니다.

data(for:)은 dataTask 메서드와 다르게 await를 사용할 수 있습니다.

에러가 발생할 수 있기 때문에 try를 붙이고 data를 요청하고 대기해야 하므로 await 키워드가 붙었습니다.

이 작업은 비동기적으로 수행되기 때문에 Thread를 블로킹하지 않습니다.

 

기존과 동일하게 fetch가 완료되면 데이터와 response를 전달 받습니다.

하지만 기존과는 다르게 매우 매우 간단한 형태입니다.

불필요한 클로저가 사라지고 (data, response)에 이쁘게 들어갑니다.

만약 에러가 발생해서 response의 statusCode가 200이 아니라면 에러를 throw 합니다.

이는 Swift의 에러 핸들링 방법이라 안전하고 직관적입니다.

 

4. UIImage(data:)

UIImage(data:)도 기존과 동일하게 동기 수행입니다.

 

5. thumbnail

마지막으로 thumbnail을 생성하는 코드입니다.

아주 심플하죠?

UIImage에서 thumbnail을 가져와서 nil이라면 에러를 throw하고, nil이 아니라면 생성된 thumbnail을 return 합니다.

기존에는 completionHandler를 이용해서 정상 결과, 에러 모두 처리했는데요.

지금은 정상 결과는 return, 에러는 throw를 사용하여 가독성이 오르고 의미도 명확해졌습니다.

 

또한 기존과 마찬가지로

이 과정은 비동기로 수행되어 Thread를 블로킹하지 않습니다.

 

이렇게해서 20줄의 복잡했던 코드가 단 6줄로 줄어들었답니다 ㅎㅎㅎ

 

근데 갑자기 UIImage에서 thumbnail 프로퍼티가 나왔는데요.

이 thumbnail 프로퍼티는 어떻게 만들어졌을까요?

 

Async 프로퍼티

async는 read-only 프로퍼티에도 추가할 수 있습니다.

 

thumbnail 프로퍼티를 가져오는데 await 키워드가 붙었습니다.

이는 thumbnail 프로퍼티를 읽는 작업이 비동기로 수행된다는 것을 의미합니다.

 

이 프로퍼티가 어떻게 생겼는지 봅시다.

thumbnail은 getter만 가지고 있는 read-only 프로퍼티입니다.

그래서 async를 붙일 수 있고 내부에 await 키워드도 들어있네요.

 

(영상에서는 이 타이밍에 Async sequences에 관한 내용이 나오는데요.

이 부분은 조금 더 학습해서 포스팅을 따로 하도록 하겠습니다.)

 

Await를 만나면 발생하는 일

지금까지의 코드를 따라오면서 await를 만나면 "대기"한다는 것은 아셨을 것입니다.

이 대기가 어떻게 구현되었는지 더 자세히 알아보도록 합시다.

 

먼저 일반적인 함수의 경우입니다.

일반적인 함수를 호출하면 Thread도 함께 해당 함수에 넘겨줍니다.

그리고 호출된 함수는 Thread를 자신의 작업이 끝날 때까지 가지고 있습니다.

함수가 종료되기 위해서는 정상적인 값을 반환하거나 에러를 발생해야 하고, 함수가 종료되면 이때 Thread를 호출자에게 넘겨줍니다.

즉, 함수가 Thread를 반환할 수 있는 유일한 방법은 함수가 종료되는 것 뿐입니다.

 

하지만 비동기 함수는 일반적인 함수와 좀 다릅니다.

비동기 함수는 suspending(일시 정지)를 통해 Thread를 반환할 수 있습니다.

비동기 함수도 호출될 때는 Thread에 대한 제어권을 받는데, await 키워드를 만나면 Thread를 반환합니다.

다만, 호출자에게 Thread를 반환하는 것이 아니라 시스템에게 반환해요.

따라서 시스템은 해당 Thread를 활용하여 다른 작업들을 수행하고, 어느 시점이 되면 비동기 함수에게 다시 Thread 제어권은 줍니다.

마지막으로, 비동기 함수가 실행을 마쳐서 결과나 에러를 반환하고 Thread 권한을 호출자에게 다시 넘기게 됩니다.

 

일시 정지가 됐을 때 어떤 일이 발생하는지를 fetchThumbnail 메서드를 통해 알아보도록 합시다.

fetchThumbnail 메서드에서 try await ... data(for:)을 호출하면 Thread를 일시 정지할 수 있습니다.

위에서 말한대로 Thread를 시스템에 반환하고 시스템은 data(for:)에 대한 작업을 예약합니다.

예약한 작업은 바로 시작될 수도 있고 다른 작업이 먼저 실행될 수도 있습니다.

 

이 타이밍에 "좋아요" 버튼을 탭해서 likeCurrentPost()를 호출했다고 합시다.

data(for:)이 시스템에게 Thread를 반환했기 때문에

Thread는 urgentlyLikePost 메서드를 시작할 수 있습니다.

이렇게 다른 작업들이 수행 중이다가 data 다운로드를 완료하면 Thread가 반환되어 호출자로 돌아가 나머지 작업을 수행하게 됩니다.

 

이렇게 일시 정지된 상태에서 다른 작업들이 수행될 수 있기 때문에 앱 상태가 일시 정지 중에 변할 수 있음을 유의해야 합니다.

즉, async / await 블록은 하나의 Transaction으로 실행되지 않는다는 것을 유의해야 합니다.

또한 Thread를 받아 다시 실행될 때 반환한 Thread가 아닌 다른 Thread에서 실행될 수 있다고 합니다.

이는 공유 자원이 보호될 수 없을 수도 있는데, 이 문제는 Actor를 사용하여 해결 가능합니다.

(참고: https://developer.apple.com/videos/play/wwdc2021/10133)

 

Async / Await 정리

첫 번째로, 함수를 async로 작성하면 일시 정지할 수 있습니다.

이 경우 자신을 호출한 호출자도 일시 정지되므로 호출자도 async로 정의되어 있어야 합니다.

 

두 번째로, 비동기 함수에서 일시 정지되는 위치를 알리기 위해 await 키워드를 사용해야 합니다.

 

세 번째로, 비동기 함수가 일시 정지되는 동안 Thread는 시스템에 반환되어 다른 작업을 수행할 수 있으므로 앱 상태가 원하지 않게 변할 수 있습니다.

 

마지막으로, 비동기 함수가 다시 시작되면 await 키워드 다음 부분부터 다시 실행됩니다. 이때, 반환한 Thread가 아닌 다른 Thread에서 수행될 수 있습니다.

 

 


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

 

 

 

반응형