[iOS - WWDC] Meet async/await in Swift #1

spring·2022년 6월 28일
2

WWDC Study

목록 보기
2/4
post-thumbnail

WWDC 스터디 이번 주제는 async/await!

영상보다 내용이 적을수도,, 간단하게 적어보겠습니다

동기/비동기 코드를 제대로 작성해본 적 없는 나로써는 처음엔 좀 헷갈리는 주제였다.

먼저 동기와 비동기에 대해 알아보자!

동기 (synchronous) : 동시에 일어난다는 뜻, 요청과 결과가 동시에 일어난다. → 요청을 하면 시간이 얼마가 걸리든 요청한 자리에서 결과가 주어져야 함 (설계가 간단하고 직관적이지만 아무것도 못하고 대기해야 하므로 비효율적일 수도 있다.)

비동기 (asynchronous) : 동시에 일어나지 않는다는 뜻, 요청과 결과가 동시에 일어나지 않는다. → 결과가 주어지는데 시간이 걸리더라도 그 시간동안 다른 작업을 할 수 있으므로 자원을 효율적으로 사용할 수 있음


위와 같은 이유로 비동기 프로그래밍은 많은 사람들에게 일상적인 활동이다. 그만큼 복잡하고 정확하지 않은 비동기 코드를 작성할 때도 많다.

예시 1

UIKit의 UIImage에서 썸네일을 형성하는 기능 (fetchThumbnail)
여기서는 fetchThumbnail 이 아래 두 함수를 호출한다고 가정

  • 동기 : preparingThumbnail
    호출 → 스레드 차단 + 해당 함수가 완료되기를 기다림 = 완료될 때 까지 다른 작업 수행 불가

  • 비동기 : prepareThumbnail
    호출 → 스레드가 자유롭게 다른 작업을 수행할 수 있음
    ** 비동기 버전의 함수가 작업을 완료하면 completionHandler를 호출하여 알려준다.

비동기 함수가 작업이 완료됐음을 알리는 방법에는 여러가지가 있다. (completion handler, delegate callbacks)

✅ 비동기 함수를 호출하면?
1. 스레드가 빠르게 차단 해제된다.
2. 작업을 시작한다.
= 비동기 함수가 실행되는 동안 스레드는 다른 작업을 수행할 수 있음!
(아마도, 함수 호출하면서 차단된 스레드를 바로 해제해서 다른 작업도 수행할 수 있게 하는 것이 아닐까... 확실하게 아시는 분은 댓글 부탁드립니다..🥲)


예시 2

item list가 있고 각 행에는 서버에 저장된 이미지의 썸네일이 표시된다.
해당 list에 표시할 썸네일을 준비해야 할 때 fetchThumbnail 메소드 호출, 일련의 단계를 통해 문자열을 UIImage로 변환

  1. thumbnailURLRequest 메소드 : 문자열에서 URLRequest 생성
  2. URLSessiondataTask 메소드 : 해당 request에 대한 데이터를 가져옴
  3. UIImage(data:) : 해당 데이터에서 이미지를 생성
  4. UIImageprepareThumbnail 메소드 : 원본 이미지에서 썸네일을 랜더링 (비동기 함수!)

위 작업 과정은 각각 이전 작업의 결과에 따라 다르므로 순서대로 수행해야 함!
작업들 중 일부는 값을 빠르게 반환 (ex. String → URLRequest, data → UIImage)

따라서 함수가 발생하는 스레드에서 실행하고 이러한 작업이 동기적으로 호출되도록 하는 것이 좋음

But,
URLRequest → data, UIImage → UIImage와 같은 작업들은 시간이 좀 걸림
그렇기 때문에 비동기 함수를 제공하는 것임

즉, 순서대로 수행해야 하는 작업 중 시간이 오래 걸리는 작업도 포함되어 있을 수 있으므로 비동기 함수도 필요한 것

+ Completion Handler

함수는 인수로 첫번째 작업에 대한 입력인 String과 호출자에게 출력을 다시 제공하는 데 사용되는 completion handler를 사용한다.

fetchThumbnail이 호출되면 먼저 thumbnailURLRequest를 호출한다.
(thumbnailURLRequest = 동기 함수 = completion handler 필요 없음)

다음으로 URLSession 인스턴스에서 dataTask를 호출, 해당 URLRequestcompletion handler를 전달

비동기 작업을 시작하기 위해 재개(resume)되어야 하는 URLSessionDataTask를 동기적으로 생성

fetchThumbnail이 반환되고 스레드는 다른 작업을 자유롭게 수행할 수 있음

✅ 정리
1. fetchThumbnail 호출
2. thumbnailURLRequest 호출
3. dataTask 호출하여 URLRequestcompletion handler 전달
4. dataTask를 시작시켜 주기 위해 resume() 메소드 사용
** 이 때 기본적으로 모든 작업은 일시정지 상태임, dataTask를 실행시켜 주기 위함

앞서 말했듯 이미지를 다운로드하는 데에는 시간이 걸리지만 데이터가 스트리밍되기를 기다리는 스레드를 차단시키고 싶지도 않기 때문에 매우 중요한 작업임!

이 과정의 결과로는 이미지 다운로드 성공 or 문제 발생

어느 쪽이든 요청은 완료되고 dataTask에 전달된 completion handler가 데이터, 응답, 오류와 같은 몇 가지 optional value와 함께 호출됨

** 문제가 발생하면 당연히 오류를 전달 //completion(nil, error)

모든 것이 해결되면 UIImage(data:)를 사용하여 데이터를 이미지로 변환

이미지가 생성되면 UIKitprepareThumbnail 메소드를 호출하여 completion handler를 전달

이 작업이 완료되는 동안 스레드는 차단 해제되어 다른 작업을 수행할 수 있다.

썸네일 준비에 성공하면 이미지와 함께 completion handler 호출
그렇지 않으면 nil 호출
//completion(thumbnail, nil)

하지만 이 방법에는 아주 큰 문제점이 있음!!

fetchThumbnail의 호출자는 fetchThumbnail이 실패하더라도 작업이 완료되면 알림을 받을 것임
But,
guard let return을 작성하는 데 너무 익숙해서 completion handler를 2번이나 까먹고 작성하지 않음

  • 데이터 → 이미지 변환 //UIImage(data: data!)
  • 썸네일을 준비하는 곳 //prepareThumbnail(of: CGSize~~~)
    위 두 부분에서 completion 호출 없이 return만 있음.

따라서 작업을 실패하더라도 fetchThumbnail의 호출자에게 알림이 전송되지 않고 행도 업데이트되지 않음
그렇기 때문에 무슨 일이 있어도 호출자에게 알리는 것이 매우 중요함!!

(↑ 누락된 completion 추가해줌)


Swift는 함수를 통해 실행이 어떻게 진행되든 값이 반환되지 않으면 오류가 발생하도록 함

하지만 여기서는 Swift의 일반적인 error handling 메커니즘을 사용할 수 없음
문제가 발생하면 completion handler내에서 오류를 throw할 수 없기 때문임
즉, Swift가 우리의 작업을 확인할 수 없다는 뜻

Swift에게 fetchThumbnails와 같은 completion handlerclosure에 불과함

클로저에 불과하다?
= 클로저는 명시적인 호출이 필요하므로 항상 호출되도록 강제할 방법이 없다!

그래서 방금 두 개의 guard 문에서 그냥 return 했을 때 컴파일 오류가 발생하지 않았던 것임. 강제할 방법이 없다는 것.
= completion handler가 호출되는지 확인하는 것은 사용자에게 달렸다.

✅ 정리!

  • 함수들 중 2개는 동기식, 나머지 2개는 completion handler 사용하여 비동기식
  • 미묘한 버그가 침투할 수 있는 5개의 기회가 포함된 약 20줄의 코드를 완성
  • 원했던 것은 4가지 작업을 순서대로 수행하는 것이지만 이번에 작성된 코드는 따르기도 어렵고 제대로 하기도 어려울 뿐더러 의도를 흐리게 함

조금 더 안전하게 만들 수 있는 방법?

  • 표준 라이브러리의 result type 사용 : .failure(error)

** 이 방법은 안전하긴 하지만 식을 추가하는 바람에 코드가 더 못생기고 길어짐

그렇다면 async/await를 사용하자!

함수는 여전히 문자열을 인수로 사용하지만 지난 번과 달리 completion handler는 전달하지 않음
대신! 비동기식 함수임 (async 키워드와 함께)

함수를 비동기로 표시할 때 async 키워드의 위치

  • throws 바로 앞
  • 함수가 throw하지 않을 때에는 -> (화살표)

이렇게 함수를 async로 표시하면 함수와 함수의 특징이 간단해짐
성공적으로 썸네일이 준비되면 썸네일이 반환되고
그렇지 않고 오류가 발생하면 그냥 던져짐

✅ async/await 작업 과정

  1. fetchThumbnail 호출
  2. 이전과 마찬가지, thumbnailURLRequest 호출
    이 함수는 동기식, 스레드가 차단된 채 작업을 수행
  3. URLSession.shared에서 data(for: request)를 호출, 데이터 다운로드 시작함

data vs dataTask

  • 둘 다 비동기식
  • async/await 지원 여부 차이
  • dataawaitable!
    → 따라서 호출된 후에 스레드를 차단 해제하여 빠르게 일시 정지됨, 그러면 다른 스레드는 다른 작업을 자유롭게 수행할 수 있음 (데이터를 받아오는 동안 data 메소드 일시 정지, 스레드는 다른 작업을 수행하는 것)
  1. 이때, data 메소드가 throws로 표시되어 있기 때문에 try 키워드 필요함

    이전 버전에서 오류를 확인하고 명시적으로 completion handler를 호출해야 했던 것 기억나시나요? 지금은 try 키워드로 모두 요약됨!

throw로 표시된 함수를 호출하려면 try 필요
마찬가지로,
async로 표시된 함수를 호출하려면 await 필요


여러 비동기 함수 호출이 있어도 try, await는 한번만 작성하면 됨!

즉, 함수 호출은 try await을 붙이자!
(키워드 순서 : async throws - try await , 차례대로 오는 것이 아니니 헷갈리지 말기!)

  1. 결국 데이터 다운로드가 완료되면 data 메소드는 재개되고 fetchThumbnail로 돌아감
  • 이 시점에서 data 메소드가 return or throw하는 오류값이 유입됨

  • 오류 발생 시 fetchThumbnail이 오류를 발생시키고 그렇지 않으면 data와 response 변수가 정의됨

    = 이전 버전의 dataTask 메소드에 전달된 completion handler가 호출되었을 때 발생한 것과 유사함

두 버전 모두 URLSession의 비동기 함수에서 발생하는 값과 오류가 유입되었는데 현재 버전은 훨씬 간단함!
왜냐면 우리가 의미하는 바를 정확히 말해주기 때문임
요청을 하고 반환된 값을 변수에 할당하여 사용할 수 있게 하고 문제가 발생하면 오류를 발생시킴

  1. 다운로드한 데이터에서 UIImage 생성 시도, 성공하면 썸네일 프로퍼티에 액세스하여 해당 이미지에 대한 썸네일이 렌더링됨

    이때 썸네일이 렌더링되는 동안 다른 작업을 자유롭게 수행할 수 있음!
    (썸네일 프로퍼티가 재개되어 fetchThumbnail로 돌아갈 때까지)

  2. 썸네일이 렌더링되면 fetchThumbnail이 이를 반환, 그렇지 않으면 오류 발생
    ** 전체코드 ↓

    completion handler 버전과 달리 썸네일이 렌더링되지 않으면 Swift는 여기에 오류가 발생하거나 값을 반환하도록 함!

✅ 정리

  • 위 함수는 completion handler 버전이 이전에 수행한 작업을 정확히 수행
  • but, 20줄의 코드 대신 6줄만 있음
  • 모두 직선 코드 = 동기적인 코드와 똑같이 생겼음 = 순서대로 수행해야 하는 4가지 작업이 차례로 나열됨
  • Swift는 문제가 발생하면 return or throw하여 함수가 완료되면 항상 호출자에게 알리고 있음 (try를 사용하여 반환, 동기식 코드는 throw 사용함)

async/await로 비동기 코드를 변환하여 더 안전하고 짧게, 의도를 잘 반영할 수 있도록 하는 방법의 한 예시일뿐!


fetchThumbnail이 구현되는 방법에 대한 몇 가지 세부 사항

  • 함수를 호출하지는 않더라도 썸네일 렌더링을 시작하는 표현식이 await로 표시됨
    why? 썸네일 프로퍼티가 비동기임!

    즉, 함수가 아니더라도 비동기식일 수 있음.
    properties 뿐만 아니라 initializers도 마찬가지!

사실 썸네일 프로퍼티는 SDK의 일부가 아닌 직접 추가한 것임
UIImage의 extension에서 정의

CGSize를 형성하고 .byPreparingThumbnail(ofSize:)에 전달한 결과를 기다림
(= await)

몇 가지 주의할 점

  1. 명시적인 getter
    property를 async(비동기)로 표시하는데 필요함
    Swift5.5부터 property getters도 throw할 수 있음 (비동기 함수 문법과 마찬가지로 async throw로 작성)
  2. property에는 setter가 없음
    읽기만 하는 프로퍼티만 비동기식일 수 있음!
    (아무래도... 쓸때도 비동기식이면 데이터가 좀 잘못되지 않을까 싶은데 이게 맞나요?)

await 키워드에 대해 더 알아보자

함수, 프로퍼티, 생성자(initializer)에서
스레드를 차단 해제할 수 있는 위치를 나타내기 위해
표현식에서 await 사용 가능

await 키워드를 사용할 수 있는 또 다른 장소는 for문

이 부분은 잘 이해가 안 가서... 그냥 사용할 수 있구나~ 정도만 하고 넘어가겠습니다.. 혹시 잘 아시는 분은 댓글 부탁드립니다~!


여기까지 비동기 코드 예시와 async/await 사용 예시를 간단하다면 간단하게 살펴보았습니다!

※ 약간.. 제멋대로 이해하고 적어본 거라.. 혹시 잘못된 부분이나 오타가 있다면 알려주시고 헷갈리는 부분은 언제든지 같이 이야기 나눠보아요! 🤗
※ 너무 길어질 것 같아서 영상의 뒷부분은 2번째 포스팅 참고해주시면 감사하겠습니다.🤓
※ 현재 포스팅은 대략 15:02 까지의 내용입니다.

🎥 WWDC 영상 링크 : https://developer.apple.com/videos/play/wwdc2021/10132/

profile
CS 4th 💻

0개의 댓글