질문에 대한 답변입니다.
Swift 5.5 이전에는 동시성이 Grand Central Dispatch (GCD)와 수동 스레드 관리에 의존하여, 종종 보호되지 않은 공유 가변 상태로 인해 데이터 경합 및 메모리 손상이 발생했습니다. Swift는 격리 보장을 제공하기 위해 Actors와 함께 구조화된 동시성을 도입했지만, 컴파일러는 이러한 격리된 도메인 간에 전달되는 값들이 본질적으로 스레드 안전하도록 보장하는 메커니즘이 필요했습니다. 이를 위해 Sendable 프로토콜이 도입되었으며, 이는 형이 동시 경계를 넘어 안전하게 공유될 수 있도록 값 의미론 또는 유형 수준에서의 내부 동기화를 강제하여 마크합니다.
Actor가 자신의 격리 도메인 바깥에서 값을 수신할 때, 해당 값은 다른 실행 컨텍스트와 공유되는 참조 타입일 가능성이 있어 메모리 안전을 위반하는 동시에 변형이 이루어질 수 있습니다. 전통적인 접근 방식은 중요한 구역을 보호하기 위해 런타임 잠금이나 뮤텍스를 사용하지만, 이러한 방법은 오버헤드, 교착 상태 위험, 구현 중 인간 오류에 취약합니다. 문제는 Swift의 성능 특성과 타이포그래피를 유지하면서 정적으로 스레드 안전성을 검증하는 제로 비용 추상화를 설계하는 것이었습니다.
Swift의 컴파일러는 Actor 경계를 넘는 모든 유형이 Sendable 준수를 해야 하며, 런타임 오버헤드 없이 안전성을 검증하기 위해 정적 분석을 활용합니다. struct와 enum과 같은 값 타입은 값 의미론을 보여주고 공유 가변 상태를 방지하기 위해 복사 시 쓰기 최적화를 사용하기 때문에 묵시적으로 Sendable입니다. 참조 타입(class)의 경우, 컴파일러는 명시적인 Sendable 준수를 요구하여 클래스가 final이어야 하며 오직 Sendable 속성만 포함하도록 강제합니다. 이를 통해 동시에 액세스하더라도 손상되지 않는 불변 또는 내부 동기화 상태가 보장됩니다.
// 묵시적으로 Sendable인 struct struct UserData: Sendable { let id: UUID let score: Int } // 불변 상태의 명시적으로 Sendable인 final class final class Configuration: Sendable { let apiEndpoint: String let timeout: Duration init(endpoint: String, timeout: Duration) { self.apiEndpoint = endpoint self.timeout = timeout } } actor DataProcessor { func process(_ data: UserData) async { // 안전: UserData는 Sendable입니다. print("Processing \(data.id)") } }
실시간 금융 거래 애플리케이션을 설계하는 동안, 우리 팀은 여러 WebSocket 연결에서 시장 데이터를 집계하는 PriceFeedActor를 구현했으며, 이는 백그라운드 스레드에서 실행 중인 NetworkManager로부터 구문 분석된 JSON 페이로드를 수신해야 했습니다. 초기에는 고빈도 업데이트 중에 대량의 데이터 세트를 복사하지 않기 위해 참조 타입 MarketData 클래스를 사용했지만, Swift 컴파일러는 이 객체들이 Sendable 준수를 갖추지 못했기 때문에 Actor에 직접 전달하는 것을 막았습니다. 이로 인해 우리는 Actor의 격리 보장을 유지하면서 서브 밀리초 거래 결정을 위해 필요한 처리량을 희생하지 않도록 데이터 모델을 재설계해야 했습니다.
우리는 MarketData를 큰 바이트 버퍼에 대한 개인 저장소를 포함하는 struct으로 리팩토링하고, 변형이 발생할 때까지 기본 저장소를 공유하기 위해 ManagedBuffer를 통해 Swift의 복사 시 쓰기 메커니즘을 이용했습니다. 이 접근 방식은 자동으로 묵시적인 Sendable 준수를 제공하여 컴파일 타임 안전성을 보장하면서 읽기 중 많은 메모리 중복을 최소화했습니다. 그러나 수동 복사 시 쓰기 논리를 구현하는 복잡성은 유지 관리 오버헤드를 초래했으며, 자동 복사 동작이 핫 경로의 쓰기 작업 중에 예상치 못하게 발생할 경우 성능 저하 위험이 있었습니다.
우리는 MarketData 참조 타입을 유지했지만 이를 전적으로 let 상수와 깊게 불변의 Sendable 속성으로 재구성된 final class로 변환하여, 여러 Actors 간에 데이터 경합 없이 단일 읽기 전용 인스턴스를 공유할 수 있도록 했습니다. 이를 통해 대량의 데이터 세트에 대해 참조 의미론의 효율성을 유지하고 복사 오버헤드를 완전히 제거할 수 있었지만, 내부 클래스 변형 대신 Actor에 고립된 가변 상태를 사용하기 위해 캐싱 전략을 재구성해야 했습니다. 아키텍처의 변화는 가변 상태를 전용 Actors로 이동하기 위한 우리의 캐싱 레이어를 상당히 리팩토링해야 했으며, 코드 복잡성을 늘렸지만 엄격한 격리 보장을 보장했습니다.
즉시 재구성이 불가능한 레거시 Objective-C 연결 클래스를 위한 임시 조치로서, 우리는 @unchecked Sendable로 이들을 표시하여 컴파일러 경고를 억제하며 내부 잠금을 통해 스레드 안전성을 수동으로 검증했습니다. 이를 통해 새로운 Actor 모델로의 신속한 마이그레이션이 가능했지만, 사실상 Swift의 정적 보장을 비활성화하고 수동 동기화 논리에 오류가 있을 경우 런타임 데이터 경합 위험을 재발생시켰습니다. 따라서 우리는 이 접근 방식을 비판적인 로깅 인프라로만 제한하고, 안전성이 가장 중요한 생산 금융 데이터에는 사용하지 않기로 했습니다.
우리는 고빈도 스트리밍 데이터에 대해 최적화된 설계로 struct 접근 방식을 채택하고, 다수의 Actors에서 동시에 접근하는 정적 구성 객체에 대해 불변 class 접근 방식을 예약했습니다. 이 혼합 접근 방식은 스트레스 테스트 중에 감지된 모든 데이터 경합 충돌을 제거하여, 이전 GCD 기반 아키텍처에 비해 동시성 관련 버그 보고서를 94% 줄였습니다. 컴파일 타임 Sendable 검사로 인해 이전 수동 잠금 시스템에서 생산 중 간헐적인 충돌을 초래할 수 있었던 세 가지 잠재적 경합 조건이 발견되었습니다.
Sendable에 준수하는 타입이 비동기 Task에 전달된 클로저에 의해 캡처될 때 컴파일에 실패하는 이유는 무엇이며, 클로저에 대한 @Sendable 속성이 이 모호성을 어떻게 해결합니까?
타입이 Sendable일 수 있지만, Swift에서 클로저는 기본적으로 변수에 대한 참조로 캡처하므로 클로저가 다른 Actor로 전송된 후 캡처된 변수가 후속 변형을 허용할 수 있습니다. @Sendable 클로저 속성은 캡처를 Sendable 값으로 제한하고 클로저 자체가 동시 도메인을 안전하게 벗어나지 않도록 강제합니다. 이를 통해 클로저와 그 캡처된 모든 상태가 Actor 경계를 넘어 격리 보장을 유지하여 비동기 작업에서 가변 캡처 리스트를 통한 데이터 경합을 방지할 수 있습니다.
Swift 6의 엄격한 동시성 체크가 암시적으로 가져온 Objective-C 헤더에 미치는 영향은 무엇이며, Sendable 주석이 없는 레거시 프레임워크와의 지속적인 상호 운용성을 허용하는 메커니즘은 무엇입니까?
Swift 6은 대부분의 Objective-C 타입을 기본적으로 비 Sendable로 간주하는 엄격한 동시성 체크를 도입합니다. 개발자는 안전성 검사를 점진적으로 수용하기 위해 @preconcurrency 가져오기 문을 사용하거나, 수동으로 SWIFT_SENDABLE 매크로와 함께 Objective-C 헤더에 주석을 달아야 합니다. 이러한 주석은 컴파일러가 스레드 안전한 레거시 객체와 격리 경계가 필요한 객체를 구별할 수 있도록 하여, 순수 Swift 코드의 안전성을 손상시키지 않고 상호 운용성을 가능하게 합니다.
Actor 내의 비격리 메서드와 Sendable 타입 간의 근본적인 차이는 무엇이며, 가변 클래스 인스턴스에서 비격리 메서드를 호출할 때 정의되지 않은 동작이 발생하는 시점은 언제입니까?
비격리 메서드는 Actor의 데이터에 대한 동기적 접근을 격리 컨텍스트 외부에서 허용하지만, 호출자의 실행자에서 실행되며 Actor의 직렬 실행자가 아닙니다. 이는 해당 메서드가 가변 Actor 상태에 직접 접근하지 않아야 함을 필요로 하며, 만약 그렇게 할 경우 Actor의 격리 보장을 우회하게 됩니다. 비격리 메서드가 Sendable이 아닌 가변 참조 타입에 적용될 경우, 적절한 동기화 없이 공유 가변 상태에 접근하면 레이스 조건이 발생하여 메모리 손상 또는 정의되지 않은 동작으로 이어질 수 있습니다.