Async/Await

Yoo Hwa Park
9 min readJun 30, 2021

--

대부분 iOS 개발자는 비동기 프로그래밍 경험이 있을 것입니다. async/await는 비동기 프로그래밍을 더 쉽고 안전하게 해줍니다. 이 글은 애플의 Meet async/await in Swift 영상에 기반합니다.

Async/await은 Swift 5.5부터 지원합니다. 이 키워드를 사용하면 동기 코드를 작성하듯이 간단하게 비동기 코드를 작성할 수 있습니다. iOS SDK에는 await로 표현할 수 있는 수많은 비동기 함수가 존재합니다.

iOS SDK의 비동기 함수는 다양한 방법으로 호출자에게 자신의 작업이 끝났음을 알립니다.

  • 컴플리션 핸들러 (completion handler)
  • delegate 콜백
  • async 키워드 사용

어떤 방법을 사용하든, 비동기 함수는 자신이 호출된 스레드를 블락하지 않습니다.

Completion Handler vs. Async/Await

아래 예시와 함께 completion handler와 async/await의 차이점에 대해서 알아봅시다.

Meet async/await in Swift 중 02:47 캡처

여러분이 개발하고 있는 앱에서 썸네일을 보여줘야한다고 가정합시다. String을 썸네일로 변환하기 위해 위의 fetchThumbnail이라는 함수는 아래의 과정을 수행합니다.

  1. thumbnailURLRequest를 통한 URLRequest를 생성
  2. URLSession의 dataTask(with:completion:)를 통한 Data 받음
  3. UIImage의 initWithData를 통한 UIImage를 생성
  4. UIImage의 prepareThumbnail(of:completionHandler:)을 통한 이미지 썸네일로 렌더링

위 과정은 모두 이전 과정의 결과에 의존하니 순서대로 실행되어야합니다. 위 과정 중 어떤 과정은 금방 값을 리턴하지만, 어떤 과정은 리턴하기까지 비교적 오래 걸립니다. 예를 들어, 이미지를 구성하는 데이터를 다운로드하는 과정과 이미지를 썸네일로 렌더링하는 과정은 조금 더 비싼 작업입니다. 그렇기 때문에 SDK는 2번과 4번 과정에 대한 비동기 함수를 지원합니다.

Completion Handler 사용 예시

Meet async/await in Swift 중 05:45 캡처

위의 코드는 completionHandler를 사용할 때 빈번히 발생하는 실수입니다. fetchThumbnail 함수의 호출자는 함수의 작업이 끝나거나 작업이 실패했을 때 그 결과에 대해 보고받아야합니다. 하지만 위 코드 중 데이터를 이미지로 변환하는 부분이나 썸네일을 준비하는 부분 중 문제가 발생하면, 컴플리션 핸들러를 호출하지 않고 리턴합니다. 위처럼 코드를 작성하게 되면 fetchThumbnail 함수의 호출자는 결과를 절대 전달받지 못하고, 앱에서 표출해야할 썸네일은 절대 업데이트되지 않습니다.

Meet async/await in Swift 중 06:31 캡처

위의 실수를 만회하려면 누락됐었던 두 부분도 completion handler를 호출해서 에러를 넘겨줘야합니다. 보통의 함수라면 호출자에게 에러를 throw하겠지만, 이 경우엔 적용되지 않습니다. Swift에서 위 예시의 completion handler는 그냥 closure입니다. 안타깝게도, closure는 에러를 throw할 수 없기 때문에 Swift가 우리의 작업을 확인해줄 수 없습니다. 이 예시의 경우에도 completion handler 호출을 누락했을 때 컴파일 에러가 나지 않았습니다. 결국, 개발자가 직접 확인해야합니다.

우리는 위의 예시를 보다 안전하게 구현하기 위해 Result type 혹은 Futures를 사용할 수도 있지만 이 방법들도 읽기 쉽고 안전한 코드를 보장해주지 않습니다. 하지만 async/await를 사용한다면 달라집니다.

Async/Await 사용 예시

Meet async/await in Swift 중 12:59 캡처

이전 예시와 같은 역할의 함수지만, completion handler 대신 async/await를 사용했습니다. 이 함수는 썸네일을 무사히 가져온다면 썸네일을 리턴하고, 그 과정에 에러가 생긴다면 에러를 throw합니다. 함수 두번째 줄에 이전 방식과의 차이가 드러납니다. dataTask와 data 함수 모두 비동기지만, data 함수는 awaitable합니다. 그 뜻은, 호출된 즉시 자기 자신을 중지시켜 스레드가 블락되는 것을 방지합니다. awaitable 함수를 사용해 try 키워드와 await 키워드를 혼합할 수 있었고, 그로 인해 에러 핸들링 코드를 이전 completion handler를 활용했을 때보다 더 간결하게 나타낼 수 있습니다. await 키워드는 async라고 마크된 함수를 부를 때 항상 붙습니다. data 함수가 재개되어 동작이 완료되면 리턴 값에 따라 에러를 throw하거나, data와 response 변수의 값이 정의될 것입니다. 이 동작은 completion handler를 사용한 예시의 것과 동일하지만, 훨씬 간단합니다. 그 이후엔 data를 이미지로 변환하고, 이미지의 thumbnail 프로퍼티에 접근해 이미지를 썸네일로 렌더링합니다. 썸네일이 만들어지는 동안 스레드는 다른 작업을 수행할 수 있습니다.

completion handler를 사용했을 때와 비교해서, 20줄이었던 코드가 단 6줄로 줄었습니다. 코드의 깊이도 감소했습니다. 또한, async/await를 사용하면 Swift가 작업이 완료되었을 때 항상 호출자에게 알립니다. Async/await를 사용함으로써 코드가 더 안전해지고, 짧아지고, 가독성이 좋아졌습니다.

Meet async/await in Swift 중 13:25 캡처

이번엔 fetchThumbnail async 함수의 마지막에서 두번째 줄에 주목합시다. 함수 호출부가 없음에도 await 키워드가 있습니다. 그 이유는 thumbnail 프로퍼티가 async하기 때문입니다. 꼭 함수만 async일 수 있는아니라 프로퍼티, 그리고 생성자도 async일 수 있습니다. 이 예시의 경우, thumbnail 프로퍼티는 SDK의 한 부분이 아니라 UIImage의 extension으로 직접 정의한 부분입니다.

async 프로퍼티의 특성을 찾아봅시다. 첫번째로, 명시적으로 getter가 정의되어 있습니다. 이 부분은 async 프로퍼티의 필수 조건입니다. 두번째로, setter가 없습니다. read-only 프로퍼티만 async할 수 있습니다. async 키워드는 함수, 프로퍼티, 그리고 생성자에 쓰일 수 있습니다. 또한, async sequences를 도는 for loops에도 사용될 수 있습니다. async sequence는 일반 sequence와 동일하지만, 요소에 비동기로 접근한다는 차이가 있습니다.

Meet async/await in Swift 중 14:30 캡처

비동기 함수 실행 중지(suspend)의 의미

어떤 함수에 async 키워드가 붙는다는 것은, 그 함수가 실행을 중지(suspend)할 수 있다는 뜻입니다. 그렇다면 비동기 함수가 실행을 중지한다는 게무슨 뜻일까요? 먼저 일반 함수를 호출하면 어떤 일이 일어나는지 알아봅시다.

일반 함수 호출 — Meet async/await in Swift 중 15:52 캡처

우리가 실행 중인 어떤 함수가 다른 함수를 호출하면 스레드의 컨트롤을 호출된 함수에게 넘기게 됩니다. 만약 그 함수가 일반 함수라면 스레드는 그 함수가 실행 완료될 때까지 그 함수 하나만의 작업을 수행합니다. 함수의 작업을 완료하면, 해당 함수는 스레드의 컨트롤을 호출부로 다시 넘깁니다. 이처럼 일반 함수가 스레드의 컨트롤을 넘기는 경우는 함수의 작업이 완료됐을 때 뿐입니다.

비동기 함수 호출 — Meet async/await in Swift 중 17:59 캡처

비동기 함수도 함수의 작업 완료 시점에 스레드의 컨트롤을 호출부로 다시 넘긴다는 점은 똑같습니다. 하지만 일반 함수와 다르게 비동기 함수는 정지하며 스레드의 컨트롤을 넘길 수도 있습니다. 비동기 함수가 실행 중 정지를 하면 컨트롤이 호출한 함수로 넘어가는 것이 아니라 시스템으로 넘어갑니다. 이 경우 비동기 함수를 실행한 함수도 정지됩니다. 시스템은 실행해야하는 많은 작업 중 어느 작업을 먼저 실행할지 판단합니다. 언젠가는 시스템이 정지된 비동기 함수를 재실행하는 것이 가장 중요하다고 판단할 것이고, 스레드의 컨트롤을 넘길 것입니다. 그렇게 결국엔 비동기 함수의 작업이 완료됩니다. 이 과정 중 비동기 함수는 여러번 정지할 수 있고, 때로는 아예 정지하지 않을 수도 있습니다.

비동기 함수의 실행이 중지된 중 다른 작업이 진행될 수 있기 때문에 Swift 문법은 비동기 호출부에 await 키워드를 넣도록 하고 있습니다. 함수가 정지된 와중에 앱의 상태가 크게 변할 수 있다는 점을 인지해야합니다. 이 부분은 completion handler를 사용했을 때도 동일합니다. 하지만 async/await를 사용하는 것이 함수의 흐름을 보는 데 더 용이합니다. 함수가 정지된 중 다른 작업들이 실행될 수 있고, 심지어 원래 함수가 실행되던 스레드와 다른 스레드에서 함수가 재실행될 수도 있습니다.

Async/Await를 사용하면서 기억할 점

  1. 함수를 async라고 마크하는 것은 해당 함수가 실행 중 중지되는 것을 허가하는 것입니다. 함수가 중지되면, 호출한 함수도 중지됩니다. 그렇기 때문에 호출한 함수들도 비동기여야합니다.
  2. await 키워드를 사용해서 비동기 함수 중 어느 부분에서 실행이 중지될 수 있는지 표시합니다.
  3. 비동기 함수가 중지된 도중 스레드는 블락되지 않습니다. 시스템이 스레드를 위한 다른 작업을 스케줄링합니다.
  4. 비동기 함수가 재개되면 그 함수의 결과가 호출한 함수로 전달되고, 호출한 함수는 다음 코드를 이어 실행합니다.

Reference

https://developer.apple.com/videos/play/wwdc2021/10132/

--

--

Yoo Hwa Park
Yoo Hwa Park

Written by Yoo Hwa Park

0 Followers

iOS Developer, writing to learn.

No responses yet