Swift Concurrency 둘러보기: async, await

Swift Concurrency 둘러보기: async, await

Swift Concurrency 이전에는 RxSwift, Completion 핸들러, GCD 등이 사용되었습니다.
Swift Concurrency는 Swift 언어에서 지원하는 비동기 및 병렬 처리 방식입니다.
Swift Concurrency는 개발자가 비동기, 병렬 처리를 지원하기 위해 여러 방법을 지원하는데 이번 글에서는 async, await에 대해 알아보고자 합니다.

async, await

async, await가 대표적으로 비동기를 구성하기 위해 사용되는 키워드입니다.
메소드, 변수에 async 키워드가 붙어 있으면 실행 중 중지가 가능한 특별한 메소드/변수라는 것을 알 수 있습니다.

func listPhotos(inGallery name: String) async -> [String] {
    let result = await fetchPhotos() // ... some asynchronous networking code ...
    return result
}

fetchPhotos() 구문 앞을 보면 await 키워드가 있는 것을 확인할 수 있습니다. await 키워드는 해당 지점이 중단되는 지점이라는 것을 컴파일러에 알려주고 있습니다. 컴파일러는 await를 통해 fetchPhotos()의 결과가 오기 전까지 함수 실행이 중지됩니다.
근데, 함수 실행이 중지된다는 것은 정확히 어떤 의미일까요?

함수 실행의 중지(Suspension)

함수 실행이 중지된다는 것은 실행 중인 함수가 일시정지되고 다른 작업을 처리할 수 있음을 의미합니다. 즉, 함수 실행이 쓰레드를 블록킹하지 않는다는 의미입니다.
간단한 예를 통해 일반적인 함수 호출과 async/await 함수 호출이 다른 점을 비교해봅시다. 네트워크 호출을 통해 여러 개의 사진을 반환하는 fetchPhotos() 메소드가 있다고 가정해봅시다.

async 프로퍼티

async 키워드는 메소드/함수 뿐만 아니라 프로퍼티에서도 사용이 가능합니다. 다만 프로퍼티에서 사용하기 위해서는 해당 프로퍼티가 read-only 여야 합니다.

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

thumbnail 변수를 호출할 때도 suspension 변수이기 때문에 await 키워드와 함께 호출해야 합니다.

let thumbnail = await UIImage.thumbnail

async-let

async let은 메소드 실행을 병렬적으로 받아볼 수 있는 방법입니다. 다음의 코드를 살펴볼까요?

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])


let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

위 코드는 3개의 이미지를 다운받아 보여주는 코드입니다. downloadPhoto(named:) 앞에 await 키워드가 있어 중지가 가능한 함수인 것을 알 수 있습니다. 하지만 사진 데이터를 얻을 때마다 함수가 중지되는 것을 확인할 수 있습니다. 사실 사진 데이터 3개는 서로 종속적이지 않기 때문에, 매번 사진 데이터를 얻어올 때 마다 함수가 중지되는 것은 비효율적입니다. 위의 사진 데이터를 효율적으로 받아올 수 있는 방법으로 변경해 보겠습니다.

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])


let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

위 코드도 마찬가지로 3개의 이미지를 다운로드 받습니다. 하지만 downloadPhoto(named:) 앞에 await 키워드가 붙어있지 않기 때문에 함수가 계속 실행됩니다. 대신 await 키워드가 있는 let photos = await [firstPhoto, secondPhoto, thirdPhoto] 구문에서 함수가 중지됩니다. 이 구문에서 각각 3개의 사진 요청은 병렬적으로 실행되고 완료된 후 다시 show() 구문이 실행됩니다. async-let이 실행 도중 실패하는 경우도 있을텐데요, 그렇게 되면 실패한 함수 이외의 다른 함수들에게도 에러를 전파합니다. 이는 다음에 소개할 Task와 연관이 있으니 다음 글에서 더 자세히 작성해보도록 하겠습니다.

정리

참고 자료