Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

일왓록(日What錄)

[Swift][WWDC] Meet async/await in Swift(WWDC21) 정리 해보자 본문

iOS/Swift

[Swift][WWDC] Meet async/await in Swift(WWDC21) 정리 해보자

일왓 2023. 12. 27. 00:31

 

Meet async/await in Swift - WWDC21 - Videos - Apple Developer

Swift now supports asynchronous functions — a pattern commonly known as async/await. Discover how the new syntax can make your code...

developer.apple.com

 

 ⚠️ 해당 글은 필자가 WWDC를 보면서 공부할 목적으로 정리한 글입니다. 잘못된 번역 및 잘못된 해석이 있을 수 있으니, 잘 못된 부분이 있다면 지적해주시면 감사하겠습니다.

- 필자가 이해 및 해석하여 추가 설명을 한 경우는 괄호 및 Italic로 표시 하였으니, 참고 바랍니다.


 

동기와 비동기의 차이

  • preparingThumnail(of:) & preparingThumnail(of: completionHandler: ): 이미지를 원하는 사이즈의 이미지로 변환하는 메서드 : 변환하는 과정에 많은 시간이 소모됨.
  • Synchronous(동기)의 경우, preparingThumbnail(of:) 메서드의 동작이 완료될 때 까지 다른 동작을 하지 못함. 메서드의 실행이 끝나고 다시 Caller(메서드를 호출한 원 함수)로 복귀
  • Asynchronous(비동기)의 경우, preparingThumbnail(of: completionHandler:) 메서드의 동작이 완료될 까지 Thread는 다른 동작을 수행함. 메서드의 동작이 완료된 경우, Caller에서 전달한 completionHandler를 실행 시켜 Caller가 하고자 했던 동작 실행(사실상, 원 함수의 동작)

 

async / await 의 필요성

 

  • 서버로부터 이미지를 받아와 썸네일 크기로 변환시킨 후 화면에 띄우는 앱을 예제로 생각해보자.
  • 우측 사진과 같은 프로세스를 진행하는데, 색칠한 2가지의 메서드의 동작에 많은 시간이 소모됨(높은 비용이 소비됨).
  • 그렇기 때문에 비동기적 동작을 통해서 이 동작을 진행해야함 .

  • 위 동작을 진행하는 코드는 다음과 같다.
  • 하지만 왼쪽의 경우에는 completionHandler가 두가지 인자에 어떤 값을 넘기냐에 따라 매번 대응해야하는 문제가 발생.
    • 그뿐만 아니라, 이를 대응하는 과정이 안전하지 않음 (like... 개발자의 휴먼 에러)
  • 이를 해결하고자 오른쪽과 같은 Result Type을 도입. result type의 인자만 대응하면 됨.
  • 왼쪽에 비해서 코드가 안정적으로 변했지만, 여전히 코드가 길고 깔끔하진 않음.
  • 비동기 코드를 대응하는 이러한 코드들을 좀더 깔끔하고 안정성있게 대응하기 위해 Async/Await를 도입.

 

Async/Await의 사용

  • 먼저 fetchThumbnail의 id: String 인자는 그대로 받고 반환값 이전에 completionHandler 대신해 async 키워드를  throws 앞에 넣자.
    • 이 함수는 작업 성공시 UIImage를 반환하고 작업중에 에러가 발생할 경우 throws를 통해 에러를 방출할 것이다.
    • 만약 함수 작업 중간에 error를 방출하지 않는다면 화살표 앞에 async를 붙일 수 도 있다.
  • thumbnailURLRequest의 경우 동기 함수이기 때문에 실행이 완료될 때 까지 다른 코드를 실행하지 못한다.
  • 하지만 URLSession.shared.data 메서드의 경우 비동기 함수이다. 
    • 비동기이기 때문에 이 메서드를 처리하는 동안에는 이를 담당하는 쓰레드는 해당 실행 메서드를 중단 시키고 다른 코드를 실행시킬 수 있다.

  •  해당 메서드가 "throws"를 하기 때문에 메서드 앞에 "try"는  넣어 이를 대응해야한다.
  • 그 뿐만 아니라  해당 메서드에는 "async" 키워드가 있기 때문에 이를 대응하기 위해 "await"를 사용해야한다.

 

  • 결과적으로 "async throws" 키워드가 있는 코드를 대응하기 위해서 "try await"를 사용해야하는 것이다.
  • 해당 메서드가 종료되면(data의 다운로드가 끝나면) 비동기 실행에 의해 다른 코드가 실행되던 쓰레드에  중단 시켰던 함수로 돌아오면서 바로 실행시킨다.
  • 해당 메서드(data(for:))가 성공적으로 실행되었을 때는 반환 값을, 실행 도중 에러가 발생했을 때는 에러를 return 할 것인데, 에러가 발생했을 경우에는 fetchThumbnail(for:)의 "throws"에 의해 발생한 에러를 그대로 return 한다.
    • 해당 메서드(data(for:))가 성공적으로 반환되었을 때의 동작은 이전에 completionHandler를 전달하는 방식과 유사하다.

  • 그런 다음, 데이터를 이미지로 형태로 변환시키고 썸네일 형식으로 렌더링 하는 thumbnail 속성에 접근하는데, 이 과정 또한 비동기 과정이다.
    • 함수 뿐만 아니라 프로퍼티(Property)도 비동기가 될 수 있다. 
    • 이미지를 썸네일 크기로 렌더링 하는 동안에 Thread는 다른 동작을 수행하고 수행이 완료되면 다시 fetchThumbnail(for:)로 돌아온다.
    • 이미지가 제대로 렌더링 되지 못하여 nil이 반환되면 optional binding(guard let)에 의해 에러를 방출한다.
  • 만약 이미지가 제대로 형성되어, nil이 아니라면 optional binding을 통과하여 thumbnail을 반환한다.

 

결과적으로 completionHandler를 사용하던 이전 방식에 비해 코드가 훨씬 간결해지고 의도를 분명히 확인 할 수도 있게 되었다.

 

 

  • 이 코드는 SDK에 있는 Property가 아닌 직접 구현한 코드이다.
  • 함수만 async를 할 수있는 것은 아니다. Property에도 async를 사용할 수있다. 단, 2가지 조건이 필요하다.
    • 명시적 getter를 사용해야한다. (get을 생략할 수 없다.)
      • Swift 5.5부터 getter 또한 throw가 가능하다.
      • 함수에서 사용하는 것처럼 async throw 형식으로 사용하면 된다. 
    • 읽기 전용으로만 작성해야한다. (== setter를 정의할 수 없다.)

  • 함수, 프로퍼티, 생성자 어느 곳이든 async를 사용할 수 있다.
  • 심지어는 반복문에도 사용이 가능하다.
    • 요소 하나하나당 비동기가 작동 되는 방식

 

동작 방식

 

  • 일반 함수의 경우에는 함수가 호출되면 함수가 실행중인 Thread의 제어권을 해당 함수(호출된 함수)에게 넘긴다.
    • 해당 함수는 해당 제어권을 가지고 있다가 에러 또는 결과 값을 반환함으로써 함수를 종료 하면서 제어권을 포기한다.
  • 비동기 함수가 실행이 되면, 쓰레드의 제어 통제권을 시스템에게 넘기면서 함수의 실행이 중단(suspending)된다. 그러면서 쓰레드는 다른 일을 수행한다.
    • 시스템은 어느 시점에 중단된 비동기 함수를 빨리 끝내는 것이 우선이라고 판단하는 시점이 오는데, 그 시점에 함수를 재 실행한다.
    • 재실행된 함수는 쓰레드 제어 통제권을 다시 받고 동작을 이어나갈 수 있다.
  • async/await 표시가 되어있다고 해서 무조건적으로 실행이 중단되지는 않는다. -> 실행이 중단(suspending)되지 않는 경우도 있다.

 

 

  • 해당 코드를 예시로 들어보자. 
  • fetchThumbnail(for:) 함수가 URLSession.shared.data(for:) 메서드를 부르게 되면 해당 data 메서드는 실행을 중단하면서 시스템에 Thread 제어권을 넘기게 되고, 자신의 실행에 대한 스케쥴링을 요청한다.
  • 만약 fetchThumbnail(for:)이 불린 다음 게시글의 좋아요 버튼을 눌렸다고 상황을 가정한다면, 이에 대한 반응 동작을 중단된 메서드가 있는 Thread에서 실행할 수 있다.
    • (해당 설명 그림은 Thread의 타임라인으로써 보여준 앞선 설명과는 다르게 FIFO 실행 구조의 Thread를 기준으로 설명되어 data(for:) 메서드 앞에 해당 Task가 할당 되었다.)
  • urgentlLikePost(id:) 동작이 끝나고 다른 동작을 더 수행할 수도 있고, 바로 data(for:) 메서드를 재실행 할 수 도 있다.
  • 어찌됐든, 결국 data(for:) 메서드의 재 실행 되어 실행이 완료되면 fetchThumbnail(for:) 함수로 되돌아간다.

 

  • 메서드의 실행이 중단되는 상황에서 다양한 동작이 이루어지기 때문에 앱 상태에 대한 많은 변화가 일어나는 부분에 대해서도 주의가 필요하다.
 

Protect mutable state with Swift actors - WWDC21 - Videos - Apple Developer

Data races occur when two separate threads concurrently access the same mutable state. They are trivial to construct, but are notoriously...

developer.apple.com

 

 

정리

 

  • async 키워드는 함수 실행의 중단을 가능하게 한다.
    • async 함수를 부르는 caller 또한 실행이 중단되게 되므로, caller도 async를 붙여야 된다.
  • async 키워드를 통해 중단되는 함수를 알려주기 위해 await 키워드가 사용된다.
  • 함수가 중단되는 동안 다른 동작이 스케쥴링되어 실행된다
  • async 함수가 재 실행되면 async 함수에서 반환된 결과가 원래 함수로 다시 유입되고 중단된 시점인 await 키워드 이후부터 다시 실행된다.

 

 

Migrate Own Project

앞선 예시에서 fetchThumbnail(for:) 함수를 통해 예시를 보여줬던 것 처럼 이번엔 Testing에서 migration을 통해 동시성을 적용하는 방법에 대해서 알아보자.

 

Testing

 

  • async/await를 적용하기 전까지는 XCTestExpectation을 사용하여 fetchThunmbnail을 실행한 결과를 기다리는 코드를 작성했었다.
  • 하지만 async/await를 통해 좀더 직관적이고 간결한 코드를 작성할 수 있다.

 

 

SwiftUI

 

먼저 위와 같은 코드에서 completionHandler를 지우고 우리고 배운 방식대로 try await를 작성해보자.

 

그 상태에서 빌드를 진행하면 다음과 같이 컴파일 에러가 발생한다.

에러의 이유는 async 컨텍스트가 아닌 sync 컨텍스트의 onAppear 에서는 async 함수를 동작시킬 수 없다는것.

  • 즉, async는 async 컨택스트 내에서만 사용 가능하다는 뜻.

  • 해결 방법은 Task 함수를 사용하는 것이다.
  • Task 클로져 안에 있는 작업들을 패키징 global dispatch queue와 같은 즉시 실행 가능한 Thread에 실행될 수 있도록 시스템에 보내진다.

 

  • 자세한 내용은 왼쪽의 wwdc에서 확인하자.

 

 

 

 

 

Async APIs in the SDK

  • SDK내의 Async를 지원하는 API들이다.

 

  • 기존에 CompletionHandler를 통해 제공되던 API가 오른쪽과 같이 async/await를 통해 마이그레이션을 진행할 수 있게 되었다.
  • 이외에도 비동기 동작을 completionHandler 형태로 전달하는 delegate에도 async/await를 지원한다.

 

 

 

  • 자세한 내용은 해당 wwdc에서 확인하자.

 

 

 

 

 

 

Async alternatives and continuaions

지금까지는 Swift가 제공하는 async/await를 보여주었지만 사용자가 직접 async/await를 구현해서 사용해야하는 경우도 있을 것이다.

  • 해당 예제를 보도록 하자. 이 함수는 앱의 전반적으로 많이 쓰이는 함수이기 때문에 async/await를 적용하면 큰 이득을 얻을 듯 하여 이를 적용해보고자 한다.

 

  • 위 예제를 그림으로 그려보면 다음과 같다. await/resume이 없다는 것과 하단이 system이 아닌 Core Data인 것만 제외하면 앞서 보인 그림들과 많이 유사해 보인다. 
    • 위와 같이 앞서 계속 보인 이와 같은 패턴을 Continuation이라고 한다.
  • 그렇다면 getPersistentPosts 내에서 managedObjectContext.excute()(Core Data에 접근)의 결과를 기다리면서 그 실행이 끝나면 다음에는 어떤 동작을 할 것인지 클로저 형태로 전달하고, 그 실행이 끝났을 때 클로저로 전달한 실행이 이루어지는 과정을 continuation으로 묶어주면 된다.

 

  • 그러기 위해서는 error를 던질수 있는 Swift의비동기 함수인 withCheckedThrowingContinuation를 사용하면 된다.
    • 만약 에러를 발생할 일이 없다면 withCheckedContinuation을 사용하면 된다.
  • 위 함수들을 통해 중단된 함수를 재개할 수 있게 하는 Continuation(클로저 인자)에 접근 할 수 있게 되고, 이 continuation의 resume 메서드를 통해 completion handler의 결과 값을 전달 할 수 있다.
    • 그 뿐만 아니라, 이 함수를 통해, persistentPosts()의 결과값을 기다리는 모든 호출의 중단을 해제하기 위한 누락된 링크를 제공 해준다.(대강 중단 때문에 끊어질 수 있는 상위 함수들에 대한 연결을 유지 시켜준다는 것 같음...)

 

  • 여기서 주의해야할 점이 있다.

  • continuation의 resume은 모든 경로에 덜도 말고 더도 말고 한번만 실행되어야 한다.
  • 만약 contiuation의 resume이 실행되지 않는 경로가 있다면 Swift는 경고를 표시할 것이다.
    • 중단 현상이 재개 되지 않게 되기 때문...

 

  • 반대로 continuation이 여러번 불리어지는 경우에는 더 큰 문제가 발생하기 때문에, 런타임 에러를 발생시킨다
    • 프로그램 데이터에 손상이 갈 수 있기 때문이라고 한다.

 

  • delegate callback을 통해 적절한 시점에 앱에 알려주는 이벤트 기반의 api에도 async/await를 사용할 수 있다.
    • continuation을 저장할 변수를 만들고 그 변수를 적절한 시점에 resume() 해주어 구현하면 된다.

 

  • 더 자세한 continuation을 포함한 low-level 수준의 concurrency에 알고 싶다면  Swift concurrency: Behind the scenes를 확인하자.

 

  • Async/await code를 통해 비동기 코드를 좀더 안전하고 간결하게 사용할 수 있게 되었다.
  • 많은 api들에서 async 를 지원하기 시작했다.

 


이로써 WWDC21 - Meet Async/await를 정리해보았다.

 

21년도까지는 아직 한글을 지원하지 않아서 스크립트를 보면서 번역 및 정리를 하다보니 생각보다 정리에 많은 시간이 들어갔지만, 많은 공부가 되었던 것 같다..!

 

이번 영상은 간단한 async/await에 대한 개요 및 사용에 대한 방법을 알려주는 세션이라, 좀더 자세한 부분을 공부하기 위해서는 다른 세션들을 많이 보아야할 것 같다.

 

무언가, system이 쓰레드의 스케쥴링에 대한 관여가 있는 부분이다 보니 뭔가 흥미로웠던 주제였다.

 

다른 세션들도 또 봐야겠다.

 

정리를 할지는 모르겠지만...🥲