apple/WWDC

Elevating an app with Swift concurrency (Swift 동시성으로 앱 수준 높이기)

lgvv 2025. 10. 19. 01:35

Elevating an app with Swift concurrency (Swift 동시성으로 앱 수준 높이기)

 

 

 

Asynchronous code

: 단일 스레드에서 시작해서 필요에 따라 비동기 코드 도입

Parallelism

: 앱 성능 개선을 위해 자원이 많이 소모되는 작업을 오프로드하고 병렬로 실행

Data-race safety

: 일반적인 데이터 경쟁 안전 시나리오와 해결을 위한 접근 방식도 알아봄

Structured concurrency

: 구조화된 동시성을 살펴보고 동시성 코드를 더 세밀하게 조정하는 TaskGroup 등도 사용

 

 

Asynchronous code

 

 

Xcode에 몇가지 기능이 추가되어 동시성을 더 쉽게 도입할 수 있음.

: Approachable Concurrency를 YES로 설정

: Default Actor Isolation을 MainActor로 설정

: Strict Concurrency Checking을 Complete로 설정

 

Xcode 26에서 기본으로 활성화된 기능임

 

 

전체적인 흐름은 위와 같음.

 

 

photoProcessor는 리소스 소모가 큰 두개의 작업을 수행

: 스티커 추출 및 색상 계산

: Swift의 동시성 기능으로 리소스 소모가 큰 작업을 원활하게 진행

 

 

 

StickerViewModel

: selection: Photo Lib에서 사용되는 모델

: await으로 loadTransferable을 호출하면 해당 작업이 완료될 때까지 실행이 일시 중단됨.

: loadPhoto가 중단된 동안 Transferable 프레임워크가 백그라운드에서 loadTransferable을 실행함

 

 

 

Concurrency 코드가 성능을 높이는 방법에 대해서 이해하보고자 함.

: loadTransferable가 완료되면 loadPhoto가 실행을 재개함.

: 이후에 MainThread에서 이미지를 업데이트 함.

: laodPhoto가 suspend된 동안에 MainThread는 UI 이벤트 및 다른 작업을 실행할 수 있음.

: await 키워드는 다른 작업이 발생할 수 있는 지점을 나타냄.

 

데이터로부터 기본 이미지를 PhotoPorocessor를 대신 사용하여 가공해서 넣고자 하는데, 과정에서 행이 발생하고 이를 분석

 

 

이미지 처리에는 많은 자원이 들어갈 것으로 예상하여 Instruments로 프로파일링해 확인.

: 추적 결과 앱에 Severe Hang이 발생

: 우측 하단의 Heaviest Stack Trace를 살펴보면 사진 프로세서가 리소스를 많이 쓰는 작업을 10초 이상 수행하는 메인스레드를 차단함.

 

 

 

메인스레드에서 앱이 실행하는 작업으로는 

: loadTransferable를 실행하고 이를 Background Thread로 오프로드해서 실행함.

: Transform the image가 메인스레드에서 구동되어서 스크롤 제스처에 응답 등의 UI 업데이트를 받지 못해서 앱의 사용자 경험이 저하됨.

 

 

Parallelism

 

 

Transform the image는 3가지 작업으로 구성됨

: Get the raw image, Process image, Update the image로 UI와 상호작용해서 원본 이미지를 가져오고 업데이트 함.

: 따라서 Get the raw image와 Update the image는 백그라운드로 오프로드 할 수 없음.

: 하지만 Process image는 백그라운드로 오프로드 가능하면 이렇게 할 경우 메인 스레드가 리소스를 많이 소모하는 이미지 처리 작업 중에도 다른 이벤트에 응답할 수 있음.

 

 

PhotoProcessor 구조체를 살펴보고 구현 방법에 대해서 알아볼 예정.

: Xcode26 설정에 의하여 코드는 기본적으로 MainActor 모드에 있어 PhotoProcessor도 @MainAcotr에 연결됨

: 모든 메서드도 MainActor에서 실행되어야 함 (컴파일 타임에 보장)

: extractSticker와 extractColor는 MainActor 외부에서 실행 가능함을 표시해야 하는데 이때 전체 PhotoProcessor를 nonisolated로 표시할 수 있음. (Swift 6.1에 새롭게 도입)

: (Default Isolation이 MainActor로 변경되면서 기존에 MainActor를 명시적으로 표시해서 사용하는 방식과 반대로 변경)

 

 

PhotoProcessor에서 process 함수에 @concurrenct라는 새로운 속성을 적용하고, async를 새롭게 적용함.

: 이렇게 작성할 경우 Swift는 항상 해당 메서드 실행 시 백그라운드 스레드로 전환함.

: 이번에는 nonisolated를 적용하여 PhtoroProcessor를 MainActor에서 분리하고 concurrenct 코드를 호출하게 함.

: PhotoProcessor에 nonisolated를 적용하면, 여기까지 할 경우 스크롤 지연 현상이 해결

 

 

앱의 반응성 유지가 사용자 경험을 개선하는 유일한 요소는 아님.

: 작업을 Main Thread에서 백그라운드 스레드로 옮기면 사용자가 결과를 얻는데 오래걸려서 오히려 사용자 경험을 악화시킬 수도 있음.

: Process image를 백그라운드 스레드로 옮겼지만 여전히 완료하는데 오래 걸림.

 

Process image를 최적화해서 더 빠르게 완료하는 방법을 알아보고자 함.

: Process image는 Extract sticker와 Extract colors가 있는데, 이런 작업은 서로 독립적이므로 async let을 활용해 각 작업을 병렬로 실행 가능.

: 이렇게 할 경우 백그라운드 스레드를 관리하는 concurrent thread pool이 백그라운드 스레드에서 한번에 실행되도록 함.

: 이로인해 휴대폰의 코어를 최대한으로 활용 가능.

 

Data-race safety

 

 

색상 추출 작업은 ColorExtractor와 현재 동일한 인스턴스를 공유해서 동시 접근이 발생할 수 있음.

: Swift 6는 컴파일 시점에 동시성 버그를 찾아내 원천 방지

 

 

변경 가능한 상태를 concurrent code 간에 공유해야 하는지를 먼저 생각해보기.

: 대부분의 경우 그냥 공유하지 않으면 됨.

: 상태를 공유해야 하는 경우가 존재하는데 데이터를 Sendable 만들어 전달하기

: 이 중 어떤것도 맞지 않다면, @MainActor 등의 Actor로 분리하는걸 고려하기.

 

 

해당 예제에서는 메서드의 지역 변수로 만들어서 외부 접근을 차단하여 안전하게 데이터 경합을 처리

 

동시성 코드 최적화는 항상 Instruments를 활용한 데이터에 기반이 되어야 함.

: 동시성을 도입하지 않고 코드를 최적화할 수 있다면 그 방법을 항상 우선시 해야 함.

 

View에서 사용하는 코드
구현된 코드

 

잠깐 사이드로 SwiftUI에서 VisualEffect 코드를 살펴볼 예정.

: 성능을 위해 @MainActor로 오프로드 함.

: VisualEffect는 @Sendable 클로저로 나중에 백그라운드에서 실행됨.

: SwiftUI에서 VisualEffect는 selection이 바뀔때마다 VisualEffect를 다시 호출하기 때문에 클로저의 캡처 리스트로 copy를 만들 수 있음.

 

 

캡처 리스트를 통해 복사본에서 동작하도록 데이터 경쟁 없이 코드를 실행할 수 있음.

 

 

Data의 값이 Sendable이어서 데이터 경쟁이 발생하지 않음.

: Sendable 값은 concurrent code에서 공유하지 않도록 막음.

: Data는 Copy-On-Write(COW)를 구현하고 있어서 수정된 경우에만 복사됨.

 

 

Structured concurrency

 

기존에 사용한 async let을 사용하는 방법은 작업의 수를 미리 알고 있어야 함.

 

 

TaskGroup을 사용하는 경우에 작업의 수를 미리 알 필요가 없음.

: TaskGroup과 같은 API를 사용하면 앱이 수행해야 하는 비동기 작업을 더 세밀하게 제어할 수 있음.

 

 

 

TaskGroup을 사용하면 child task와 결과를 더 세밀하게 제어할 수 있음.

: TaskGroup을 사용하면 원하는 만큼 child task를 병렬로 시작할 수 있음.

: 각 child task는 완료에 걸리는 시간이 제각각이라서 서로 다른 순서로 완료될 수 있음.

: 해당 예제에서는 딕셔너리에 저장하고 있어서 완료되는 순서는 상관 없음.

: 주의할 점은 완료된 순서이지 시작한 순서가 아닌것에 유의

 

 

TaskGroup은 AsyncSequence를 따르기 때문에 완료된 순서대로 결과를 반복하여 딕셔너리에 저장할 수 있음.

: 마지막으로 전체 그룹이 child task를 끝내기를 기다림.

 

 

 

 

(링크)
https://www.youtube.com/watch?v=UoFK7uY9MnY