Swift 동시성 사용하기 (Embracing Swift Concurrency) - WWDC25

동시성을 활용하면 응답성을 향상시킬 수 있음.
- Swift Concurrency 동시성 모델은 동시성을 사용하는 코드를 올바르게 작성하기 위해 설계되었음.
- 이를 통해 동시성 도입이 명시적으로 이루어지고 동시 작업에서 어떤 데이터가 공유되는지 명확해짐
- 컴파일 시점에 이를 확인하므로 안전함

많은 앱은 동시성을 아껴서 사용하고, 어떤 앱은 아예 동시성이 필요 없기도 함.
단일 스레드를 사용하는 것에 비해서 동시성은 앱을 복잡하게 만들 수 있어서 필요할 때 도입해야 함.

앱은 모든 코드를 메인 스레드에서 실행하여 시작해야 하며 단일 스레드로도 상당히 많은 코드를 작성할 수 있음.
메인 스레드는 앱이 UI 관련 이벤트를 수신하고 이에 대한 응답으로 UI를 업데이트 할 수 있는 영역임.
앱을 많이 개발할수록 코드를 메인스레드에서 실행하는 것 앱 성능 저하의 원인이 될 수 있음.
Swift Concurrency는 백그라운드 혹은 메인 스레드에서 실핼되는 동시 작업을 표현하기 위한 actor와 같은 도구를 제공함.
이 세션에서는 앱이 단일 스레드에서 동시성으로 전환되는 과정을 알아보고자 함.

- 단일 스레드 코드가 Swift 동시성과 어떻게 작동하는지 설명.
- 다음 네트워크 접근과 같이 대기 시간이 긴 작업을 처리하는 데 도움이 되는 비동기 작업 소개
- 작업을 백그라운드 스레드로 옮기기 위한 동시성을 소개
- 데이터 경쟁을 일으키지 않고 스레드 간에 데이터를 공유하는 방법
- 액터를 사용하며 메인 스레드에서 데이터를 이동

Swift는 @MainActor의 코드가 항상 메인 스레드에서만 실행되도록 보장하고, @MainActor 데이터는 항상 해당 스레드에서만 접근되도록 보장하며, 이런 코드는 MainActor에 isolated되어 있다고 말함.

Swift는 기본적으로 MainActor를 사용하여 메인 스레드 코드를 보호함.
Swift 6.2부터는 MainActor로 코드를 보호하는 것을 build 설정으로 결정 가능함.
Xcode 26으로 생성된 새 앱 프로젝트의 경우에는 기본적으로 활성화 됨.

Swift 6.2 @MainActor가 기본이 되므로 해당 세션에서는 이를 가정하고 진행.
해당 코드에는 스레드 전환이 일어나지 않으며, 이로인해 메인 스레드를 오래 점유해 문제가 발생할 수 있음.

asynchrous task 데이터를 기다릴 때 메인 스레드를 묶어두지 않고 처리할 수 있음.
await은 함수가 일시 중단될 수 있는 위치를 나타냄.
즉, 기다리는 이벤트가 발생할 때까지 현재 스레드에서 실행을 중지한다는 의미이며, 이벤트가 도착하면 실행을 재개할 수 있음.

Task는 다른 코드와 독립적으로 실행되며, 특정 작업을 처음부터 끝까지 수행하도록 생성되어야 함.

ㅊ
하나의 앱에는 여러가지 동시성이 존재할 수 있음. 각 작업은 백그라운드에서 수행되지만, 각 작업은 메인 스레드에서는 번갈아 가면서 각 작업을 순차적으로 실행해야 함.

단일 스레드를 번갈아 수행하는 작업을 Interleaving 이라고 부름.
이렇게하면 시 스템 리소스를 가장 효율적으로 사용하여 전반적인 성능이 향상됨.
스레드는 단일 작 업을 기다리면서 스레드를 idle 상태로 두는 대신 가능한 빨리 모든 작업에 대해 진행 시킬 수 있음.

URLSession은 이미 백그라운드에서 실행되도록 지원되고 있음.
하지만 decoing 작업이 너무 오래 걸려서 문제가 될 수도 있음.

앱이 반응하지 않는 문제가 발생했다면 메인스레드에서 너무 많은걸 처리하고 있음.
프로파일링을 통해 어떤 지점에서 많은 부분을 소모하는지 확인할 수 있음.

단일 스레드에서 더 빠르게 만들 수 없다면 동시성을 도입해야할 수도 있음.
또한, 시스템 CPU 코어를 더 많이 활용해 작업을 더 발리 완료하는데 사용할 수도 있음.

@MainActor에서 작성된 코드는 메인 스레드에서만 접근할 수 있는 모든 데이터와 코드에 자유롭게 접근할 수 있는데, 메인 스레드에서는 동시성이 없기 때문에 안전함
decodeIImage 메서드를 부하를 줄이기 위해 분산시키려면 호출되게 명시하려면 @concurrenct 속성을 해당 메서드에 적용하면 됨.

@concurrenct는 Swift에게 백그라운드에서 함수를 실행하라고 알려줌.
이를 사용할 경우 기존 Main 스레드이기에 접근할 수 있었던 데이터 접근에 대한 부분도 달라지게 됨.

MainActor와의 관계를 끊을 때 사용할 수 있는 몇가지 전략이 존재함.
- 어떤 경우에는 MainActor의 코드를 항상 MainActor에서 실행되는 호출자로 옮길 수 있음
- 작업이 동기적으로 진행되도록 하려는 경우 이 전략이 좋음
- await을 사용하여 동시성 코드에서 MainActor에 비동기적으로 접근할 수 있음.
- 코드가 MainActor에 있을 필요가 전혀 없다면 non-isolated 키워드를 추가하여 모든 액터와 분리할 수 있음.

디코드 이미지 코드를 이렇게 분리하여 첫번째 전략으로 접근할 수 있음.

- @concurrent 함수는 항상 백그라운드에서 실행하도록 전환함.
- nonisolated를 사용하면 어디서 실행될지 client가 선택할 수 있음.

하지만 언제나 concurrent를 사용하는 것이 항상 최상의 API 선택은 아님.
데이터를 디코딩하는 데 걸리는 시간은 데이터 크기에 따라 달다르며, 작은 사이즈의 경우에는 메인에서 해도 괜찮음.
경우에 따라서는 클라이언트가 작업을 오프로드(부하를 분산) 여부를 결정하게 하는것이 가장 좋음.
nonisolated 코드는 어디에서나 호출할 수 있기 때문에 매우 유연함.
즉, 호출한 MainActor에서 호출하면 MainActor이고 background actor에서 호출하면 background actor임.

작업을 백그라운드로 오프로드하면 시스템은 작업이 백그라운드에서 스레드에서 실행되도록 스케줄을 조정함.
동시 스레드 풀에는 시스템의 모든 백그라운드 스레드가 포함되며, 여기에는 서로 다른 많은 스레드가 포함될 수 있음.
코어가 많은 대형 시스템을 수록 스레드 풀에 더 많은 백그라운드 스레드가 생성되며, 스레드의 활용은 시스템이 알아서 함.
background 스레드 풀에서는 시작 스레드와 실행되고 있는 스레드가 다를 수 있음.

동시성이 있는 코드에서 변경 가능한 상태를 공유하는 것은 런타임에서 버그가 발생할 수 있어서 디버깅하기 어려운 걸로 악명이 높음.
Swift는 컴파일 타임에 이를 잡아내어서 도와줌.

- 메인 스레드에서 백그라운드에 존재하는 Fetch Image 호출
- url은 value type으로 메인 스레드에 존재하는 URL과 별도의 copy를 background에서 생성
- 메서드 호출 후 사용자가 URL을 바꾸더라도 백그라운드에 copy되어 들어갔기에 변경사항이 백그라운드에서 사용되는 값에 영향을 미치지 않음.
- 즉, URL과 같은 value type을 공유하는 것은 안전함
- 왜냐하면 이는 실제 객체를 공유하는 것이 아닌 copy를 공유하는 것이라서 서로 독립적이기 때문.

공유하기에 안전한 항상 타입을 Sendable 타입이라고 하며, Sendable은 프로토콜임.
Sendable을 conform하는 모든 타입은 안전하게 공유할 수 있음.
Array와 같은 컬렉션은 Sendable에 대한 where을 통해 조건부로 정의되어 있어서 Element가 Sendable일 때 Collection 자체도 Sendable임.
MainActor는 암묵적으로 Sendable이므로 명시적으로 작성할 필요 없음.

actor는 같은 한번에 하나의 작업에서만 접근 가능하도록 하여 non-Sendable state를 보호함.
actor는 값을 저장할 수 있고, 메서드에 보호된 상태에 대한 참조도 반환할 수 있음.

decodeImage는 인스턴스 메서드이므로 암묵적으로 self를 인수를 전달함.
여기서 self와 data라는 두개의 값이 MainActor 외부로 전달되며 image라는 참조값이 MainActor로 들어옴.
class와 같이 non-sendable 타입은 actor에 의해 보호되는 상태에 접근할 수 없음.

- sacleAndisplay 메서드는 메인 스레드에 새로운 이미지를 로드함.
- 그런 다음에 해당 함수를 동시성 풀(@concurrent를 명시)에서 실행되는 Task를 생성하고, 해당 이미지의 copy를 가져옴.
- 마지막으로 view.displayImage로 넘어가면서 메인 스레드가 이미지를 다시 표시함
- 이 지점에서 UI를 변경하는건 백그라운드에서 실행되어서 문제가 발생함.
- 백그라운드 스레드는 이미지를 변경하여 width와 height을 다르게 만들고 변경된 버전의 픽셀로 바꾸고 있음
- 동시에 메인 스레드에서는 기존의 너비와 높이를 기준으로 픽셀을 반복함.
- UI 오류가 발생할 수도 있고, 픽셀 배열의 범위를 벗어나 접근하려고 할 때 크래시가 발생할 가능성도 큼.

Swift Concurreny는 Sendable이 아닌 타입을 공유하려고 할 때 컴파일러 오류로 인해 데이터 경쟁을 방지함.
컴파일러는 @concurrent에서 실행되는 작업이 image를 캡처하고 있음을 나타냄
이를 수정하려면 동일한 객체를 동시에 수정하지 않도록 해야함.

이미지 스케일 작업이 완료될 때 까지 기다렸다가 표시하는 것도 방법임.
위 처럼 수정해 순서대로 진행되도록 바꿀 수 있음.

만약 MainThread에서 실행될 필요가 없다면 @concurrent를 외부로 빼내어 세 가지 작업을 순차적으로 진행할 수 있음.
이후 UI를 표시하기 위해 MainActor로 보내면서 이미지 객체를 캐싱하는 등의 방법으로 이미지에 대한 참조를 저장할 수 있음.

UI에 표시된 후에 이미지를 수정하고자 하면 컴파일러의 오류가 발생하게 됨.

해당 문제는 MainActor로 보내기 전에 이미지를 변경해서 해결할 수 있음.

데이터모델에 클래스를 사용하는 경우 MainActor에서 시작될 가능성이 높으므로, UI의 일부로 클래스가 표현될 수 있음.
하지만, 결국 background Thread에서 작업해야 한다고 결정하면, 격리되지 않도록 해야함.
하지만 거의 Sendable이 아닐 것임.
일부 모델은 메인 스레드에서 업데이트되고, 다른 모델들은 백그라운드에서 업데이트 되는 상황은 피하는 것이 좋음.
어떤 모델의 클래스의 상태를 non-Sendable로 할 경우 동시성의 세계에서 타입을 동시에 수정하는 것을 컴파일러 단에서 예방할 수 있음.
또한 클래스를 Sendable 클래스로 만드는 것은 일반적으로 lock과 같은 low-level 메커니즘을 사용해야 하므로 더 쉬움

클래스와 마찬가지로 클로저도 공유 가능한 상태를 생성할 수 있음.
여기서는 perform 메서드의 body 인자에 클로저가 존재하고, image 변수의 캡처라도 부름.
Sendable 클래스와 마찬가지로 공유 상태를 가진 클로저도 동시에 호출되지 않는 한 안전함.
동시에 공유해야하는 경우에만 함수의 타입을 Sendable로 만들기

Sendable Checking은 일부 데이터가 actor와 task 사이에서 데이터가 전달될 때 발생함.
앱에 버그를 일으키는 데이터 경쟁이 없는지 확인하기 위함.
일반적인 코드에서는 공유가능한 대부분의 타입은 Sendable임
클래스 또는 클로저처럼 안전하지 않은 변경 가능한 상태를 포함할 경우에는 순차적으로 처리해야 함.
하나의 task에서 다른 task로 작업을 전달하기 전에 현재 캡처한 task에서 객체에 대한 모든 수정을 이루어야 함.

비동기 작업은 Main에서 background로 옮기면 앱의 응답성을 향상시킬 수 있음.
비동기 작업이 메인 스레드에서 너무 자주 check in되는 원인이 MainActor에 많은 데이터가 있다는 것을 알게 되면, 백그라운드로 보내기 위해 Actor를 도입하는 것이 좋음.

시간이 지남에 따라 앱이 커질수록 MainActor에서 수행되는 상태의 양도 커짐.

background로 옮기면 좋지만, NetworkManager가 위치한 영역이 MainThread라서 Backgorund에서 메인 스레드로 자주 이동해야 함.
즉, 이로 인해서 여러 task가 MainActor에서 코드를 실행하려고 하는 경합 상황이 발생할 수 있음.
결국 개별 task는 빠르게 처리할 수 있지만 작업량이 많으면, UI오류가 발생할 수 있음.

networkManager도 개별 actor로 빼서 Main에서 접근할 때만 사용

- actor types은 데이터를 격리하여 한번에 하나씩만 접근 가능하도록 함.
- Actor types도 Sendable이므로 actor 객체를 자유롭게 공유할 수 있음.
- MainActor와 달리 프로그램에 여러개의 background Actor 객체가 존재할 수 있으며, 각각은 독립적임.

또한 Actor 자체는 MainActor처럼 MainThread 처리되는 단일 스레드가 아님.
일부 상태를 MainActor에서 Actor 객체로 옮기면 background 스레드에서 더 많은 코드를 실행할 수 있고, 메인스레드를 열어둔 채 UI응답성을 유지할 수 있음.

- MainActor에서 너무 많이 처리하는 경우에는 thread-hopping을 야기할 수 있어서 Actor를 사용하자.
- UI가 아닌 코드에 대한 부분을 Acotr로 분리하기.
- 대부분의 클래스는 Actor로 사용되지 않을 가능성이 높음
- UI에 대한건 반드시 MainActor에 남아 있어야 함.
- 모델 클래스는 일반적으로 UI가 있는 MainActor에 있거나, non-Sendable이므로 동시 접근이 많이 발생하지 않음.

몇 가지 빌드 권장사항을 소개
- Apporachable Concurrency를 사용하면 동시성 작업을 보다 쉽게 수행할 수 있는 기능을 제공
- Default Acotr Isolation은 UI와 상호작용하는 모듈의 경우 활성화
- Strict Concurrency Checking은 Swift Concurrecny를 잡아줌.
(참고)
https://www.youtube.com/watch?v=u2rYp8AMuSg