Sendable은 스레드 간 전송에 안전한 타입을 표시하는 Swift 프로토콜입니다. 동시성(actor, async/await, Task)을 다룰 때 Swift 컴파일러는 격리된 컨텍스트 간에 전송되는 모든 데이터가 Sendable을 준수하도록 요구합니다. 이는 데이터 경합을 초래할 수 있는 안전하지 않은 타입의 우발적 전송을 방지합니다. 프로토콜은 계약으로 작동합니다. Sendable을 준수하는 타입은 내부에 동기화되지 않은 상태가 없음을 보장합니다. WWDC 2021에 따르면, Sendable 프로토콜은 안전한 멀티스레드 아키텍처를 설계할 때 필수 요소입니다.
핵심 요점
Sendable은 Swift 표준 라이브러리(SE-0302)의 프로토콜로, 격리 도메인 간 전송에 안전한 타입을 표시합니다. 격리 도메인이란 actor, Task 또는 @MainActor 컨텍스트를 의미합니다. Sendable에는 요구사항이 없습니다 — 안전 마커로 작동합니다.
Swift 5.5 이전에는 개발자가 DispatchQueue를 통해 모든 객체를 스레드 간에 전송할 수 있었고, 컴파일러는 이것이 안전한지 확인하지 않았습니다. Sendable은 이 격차를 해소합니다. 이제 컴파일러 자체가 스레드 간 전송을 추적하고 안전하지 않은 전송을 차단합니다. 이는 동시 코드를 실행 전부터 더 신뢰할 수 있게 만듭니다.
struct UserProfile: Sendable {
let name: String
let age: Int
}
class NonSendableClass {
var counter: Int = 0
}
예제에서 UserProfile은 Sendable 타입의 상수 속성을 가진 구조체이므로 스레드 간에 안전하게 전송할 수 있습니다. NonSendableClass는 await을 통해 또는 Task로 전송하려고 하면 컴파일 오류가 발생합니다.
Swift의 Sendable 타입은 세 가지 범주로 나뉩니다: Sendable 속성을 가진 값 타입, 불변 상태의 final 클래스, @Sendable로 표시된 함수/클로저입니다. 컴파일러는 값 타입에 대해 자동으로 Sendable 준수를 추론합니다.
Struct, Enum 및 튜플은 모든 속성과 연관 값이 Sendable인 경우 자동으로 Sendable이 됩니다. 이는 보수적인 접근 방식입니다. 하나의 속성이라도 Sendable을 준수하지 않으면 전체 타입이 안전한 것으로 인식되지 않습니다.
| 타입 | Sendable | 조건 |
|---|---|---|
| Struct | 예(명시적 선언 없음) | 모든 속성이 Sendable |
| Enum | 예(명시적 선언 없음) | 모든 연관 값이 Sendable |
| Final class | 예(명시적 선언 있음) | 모든 let 속성이 Sendable, var 없음 |
| Non-final class | 아니오 | 상속으로 인해 Sendable 불가 |
모든 Swift 내장 타입 — Int, String, Double, Bool, Optional, Array, Dictionary, Set — 은 Sendable을 준수합니다. 이는 타입 구성을 기본적으로 안전하게 만듭니다. 개발자는 사용자 정의 클래스에만 주의하면 됩니다.
@unchecked Sendable은 클래스가 컴파일러 검사를 우회하여 명시적으로 자신을 Sendable로 선언할 수 있는 메커니즘입니다. 개발자는 해당 클래스의 스레드 안전성에 대한 책임을 집니다. 이는 Objective-C 브리지 및 최적화된 구조에 유용합니다.
@unchecked Sendable은 클래스가 내부적으로 잠금 또는 원자적 연산을 통해 안전성을 보장하지만 컴파일러가 정적으로 검증할 수 없는 경우에 사용됩니다. 예를 들어 os_unfair_lock 또는 pthread_mutex_t가 있는 클래스 — 그 스레드 안전성은 코드에 의해 보장되지만 Swift는 이를 인식하지 못합니다.
final class AtomicCounter: @unchecked Sendable {
private var value: Int = 0
private let lock = NSLock()
func increment() {
lock.lock()
value += 1
lock.unlock()
}
}
@unchecked Sendable은 주의해서 사용해야 합니다. 이는 다른 개발자에게 명시적인 신호입니다: "확인했습니다, 안전합니다." 구현 오류는 찾기 어려운 경합을 초래할 수 있습니다. @unchecked를 사용하기 전에 타입을 정말 값 타입으로 다시 작성할 수 없는지 확인하세요.
Actor와 Sendable은 동전의 양면입니다. Actor는 상태를 격리하지만 외부 세계와 데이터를 교환하려면 Sendable 타입을 반환해야 합니다. Actor 메서드가 Sendable이 아닌 타입을 반환하면 컴파일러가 경고 또는 오류를 표시합니다.
Actor가 외부 코드로 데이터를 보낼 때 이 데이터는 격리 경계를 교차합니다. Sendable은 수신자가 Actor 외부에서 데이터를 안전하게 사용할 수 있음을 보장합니다. Actor 자체는 격리된 상태로 유지됩니다 — 내부 상태가 노출되지 않습니다.
struct AccountSnapshot: Sendable {
let id: UUID
let balance: Double
let lastUpdated: Date
}
actor BankActor {
private var balance: Double = 0
func snapshot() async -> AccountSnapshot {
return AccountSnapshot(
id: UUID(),
balance: balance,
lastUpdated: Date()
)
}
}
AccountSnapshot은 Sendable 타입의 let 속성만 포함하는 Sendable 구조체입니다. 이 접근 방식은 Actor에서 데이터를 추출하기 위한 모범 사례입니다. 상태 스냅샷은 값으로 전달되며 Actor는 상태에 대한 제어를 잃지 않습니다.
@Sendable은 클로저가 가변 방식으로 Sendable이 아닌 데이터를 캡처하지 않음을 보장하는 함수 및 클로저용 속성입니다. 클로저가 Task나 Actor 메서드로 전송될 때 Sendable이어야 합니다.
컴파일러는 @Sendable 클로저가 클래스에 대한 가변 참조를 캡처하지 않는지 확인합니다. Sendable 타입의 let 속성 캡처는 허용됩니다. 참조 타입의 var 변수를 캡처하면 클로저가 변형과 동시에 실행될 수 있으므로 오류가 발생합니다.
func performAsync(operation: @Sendable () -> Void) {
Task {
await operation()
}
}
let constant = "Safe"
var counter = 0
// counter — var 캡처는 counter를 변경하여 컴파일 오류 발생
performAsync { // ❌ 캡처된 변수의 변경
print(constant)
}
규칙은 간단합니다. @Sendable 클로저는 그 자체가 Sendable이고 외부에서 변형되지 않는 데이터만 캡처할 수 있습니다. 클래스의 경우, 클래스가 Sendable로 표시되지 않은 경우 약한 참조 weak self를 캡처하는 것이 허용됩니다. 이는 고전적인 순환 참조와 경합 조건을 방지합니다.
자주 묻는 질문
Sendable은 컴파일러에게 "이 타입은 스레드 간에 안전하게 전송할 수 있습니다"라고 알리는 마커입니다. 구조체와 상수는 일반적으로 기본적으로 Sendable이지만, 클래스는 명시적으로 지정하지 않는 한 그렇지 않습니다.
Sendable 속성을 가진 값 타입(struct, enum), 불변 상태의 final 클래스, 그리고 모든 기본 Swift 타입(Int, String, Double, Bool, Array, Dictionary, Optional)이 자동으로 Sendable을 준수합니다.
@unchecked Sendable은 컴파일러의 정적 검사 없이 클래스를 Sendable로 선언하는 방법입니다. 개발자는 종종 잠금이나 원자적 연산을 통해 스스로 안전성을 보장합니다.
Actor는 상태를 격리하지만 외부 코드에 데이터를 반환할 때 이 데이터는 격리 경계를 교차합니다. Sendable은 수신자가 Actor 외부에서 이 데이터를 사용할 때 경합을 겪지 않음을 보장합니다.
모든 속성이 Sendable 타입의 상수인 final 클래스에 Sendable 프로토콜을 추가합니다. 클래스가 잠금을 사용하는 경우 @unchecked Sendable을 사용할 수 있지만 주의가 필요합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.