Neoself의 기술 블로그

Protect mutable state with Swift actors 정리 본문

개발지식 정리/WWDC 정리

Protect mutable state with Swift actors 정리

Neoself 2025. 4. 3. 22:39

1. Actor

데이터를 안전하게 보관하는 상자와 같은 개념으로, 공유 mutable 상태를 보호하기 위한 동기화 매커니즘입니다.

자체 상태는 프로그램의 다른 영역과 격리되며, 액터 접근을 위해선 액터를 반드시 통과하며 이때, 액터의 동기화 메커니즘은 다른 코드가 액터의 상태에 동시에 접근하지 않도록 보장합니다.

속성, 메서드 초기화, 서브스크립트를 가질 수 있으며, 프로토콜을 준수하고 확장으로 보강될 수 있습니다.

저레벨에서 제공하는 직렬 DispatchQueue와 NSLock처럼 상호 배제를 제공하지만, 저레벨 동기화 매커니즘과 달리 엄격한 컴파일러 오류를 통해 동기화를 안정적으로 수행할 수 있도록 돕습니다.

 

1.1. 동작 원리

프로그램의 나머지 부분에서 인스턴스 데이터를 격리하고, 해당 인스턴스 데이터에 대한 동기화 접근을 보장

 

액터 외부 <-> 액터는 비동기적으로 상호작용

터가 사용 중이면 코드가 일시 중단(suspend)되어 실행 중인 CPU가 다른 유용한 작업을 수행할 수 있으며, 액터가 자유로워질 때 코드를 깨워 실행을 재개하여 호출이 액터에서 실행될 수 있도록 합니다.

 

*액터 내부에서 실행되는 동기식 코드 자체는 중단되지 않고 완전히 실행되기에 동시성 영향 고려가 불필요하다.

 

1.2. actor를 사용하여도 고려해야하는 상태관리 엣지 케이스

위 코드에서 설명하는 상황은, image()를 통해 1번째 downloadImage()가 자식 작업을 통해 실행되고 있는 도중, 동일한 url로 다른 이미지를 이중으로 다운로드하는 상황을 묘사합니다.

 

다운로드가 완료되기 전끼지는 캐싱이 되지 않기 때문에, 2개 다운로드 작업이 모두 실행될 수 있으며, 이로 인해 cache에는 의도했던 이미지 1이 아닌, 이미지 2가 다운로드 될 수 있습니다. 이는 상호배제를 제공하기 때문에 data race 자체는 아니지만, 데이터 일관성이 위배되는 상황입니다. 

 

이는 await 키워드로 인해 함수가 일시중단될 때, 다른 작업이 실행됨에 따라 함수 재개 시 프로그램의 상태가 변경되는 액터의 재진입 상황을 의미합니다.

때문에, 액터의 재진입 상황 및 전역 상태에 대한 가정을 주의깊게 확인해 일관성을 회복하는 것이 중요합니다. 설령 위 예시의 경우, 다운로드 완료 이후에 캐시를 확인하고, 캐시에 다른 이미지 존재 시 다운로드된 이미지를 폐기처리하는 식으로 내부 가변 속성에 대한 일관성을 회복시키면 됩니다.

1.3. 그래서 액터의 재진입이 뭐죠

액터를 구현하고 비동기 코드를 작성할 때 항상 재진입을 고려하여 설계하십시오. 코드에서 await는 세상이 계속 진행되어 가정을 무효화할 수 있음을 의미합니다.

 

액터는 종종 서로 또는 시스템의 다른 비동기 코드와 상호 작용합니다.

액터 재진입은 데드락을 방지하고 순방향 진행을 보장하지만, 각 await에서 여러분의 가정을 확인해야 합니다.

재진입에 대해 잘 설계하려면 동기 코드 내에서 액터 상태의 변경을 수행.

- actor A가 await 호출을 통해 액터 외부 코드나 actor B로 제어권을 넘겼다가, 다시 actor A의 다른 메서드를 호출하는 상황.

- actor A는 이미 첫번째 메서드 실행 중에 잠겨있었지만, 제어권을 넘긴 시점에서 일시적으로 잠금이 해제되어 두 번째 메서드 호출이 가능해집니다. 이로 인해 actor의 내부 상태가 첫번째 메서드 실행 도중에 변경될 수 있기에, 의도된 동작이 발생하지 않을 수 있습니다.

 

2. Sendable

값 타입, 액터 타입, 그리고 특정 조건 하의 클래스가 안전하게 공유될 수 있음을 명시하기 위한 프로토콜

값을 한 위치에서 다른 위치로 복사하고, 두 위치 모두 서로 간섭 없이 해당 값의 복사본을 안전하게 수정할 수 있다면 해당 유형은 Sendable이 될 수 있음

 

값: 그 자체로 독립적인 복사본을 생성하기에 서로 간섭하지 않아 Sendable을 기본적으로 준수합니다.

클래스: 멀티 스레드 환경에서 신중하게 구현되지 않는 한,  Sendable이 아닐 수 있기에 상태를 공유할 가능성이 존재합니다.

 

클로저:

클로저가 캡처하는 모든 것은 Sendable이어야 합니다. 그래야 클로저가 액터 경계를 넘어 Sendable이 아닌 타입을 이동시키는 데 사용될 수 없습니다.

클로저는 함수 내에서 정의되고, 나중에 호출 될 수 있도록 다른 함수에 전달될 수 있는 작은 함수로, 함수와 마찬가지로 액터 격리되거나 격리되지 않을 수 있습니다.

(좌) 액터 격리된 클로저, (우) 액터 격리되지 않은 클로저

(좌) 액터 내부에 클로저가 정의된 경우,해당 클로저가 다른 스레드로 빠질 수 없기에(액터 격리되기에) 안전합니다.

(우) Task.detached와 같이 분리된 작업을 생성할 경우, 액터가 수행하는 다른 작업과 동시에 클로저가 실행됨에 따라 액터 내부에 있을 수 없으며, 이는 데이터 경쟁을 트리거합니다.

3. nonisolated

이 메서드가 액터에 구문적으로 설명되어 있더라도 액터 외부에 있는 것으로 취급됨을 의미

액터 외부에 있는 것으로 취급되므로 액터의 변경 가능한 상태를 참조할 수 없습니다.(ex.booksOnLoan) 참조할 경우, 데이터 경쟁 허용

4. MainActor

메인 스레드를 의미하며, 메인액터 속성을 사용해 특정 코드를 메인 스레드에서 실행되도록 표시할 수 있습니다.

메인 액터 외부에서 호출하는 경우 await 해야하기에 호출이 메인 스레드에서 비동기적으로 수행됩니다.