Data Race 문제
// ❌ Swift 5 - Data Race 가능
class Counter {
var count = 0
}
let counter = Counter()
Task {
counter.count += 1 // Thread A
}
Task {
counter.count += 1 // Thread B
}
// 결과: count가 1일 수도, 2일 수도 있음 (Data Race!)
Swift 6의 해결책: Sendable 프로토콜로 컴파일 타임에 검증
protocol Sendable {}
의미: "이 타입은 다른 스레드로 안전하게 전달할 수 있습니다"
Sendable이 자동으로 적용되는 타입들
// Value Types (자동 Sendable)
struct Point { // ✅ 자동으로 Sendable
let x: Int
let y: Int
}
enum Status { // ✅ 자동으로 Sendable
case success
case failure
}
// Immutable Reference Types
final class Config { // ✅ 모든 프로퍼티가 let이면 자동 Sendable
let apiKey: String
init(apiKey: String) {
self.apiKey = apiKey
}
}
Sendable이 자동으로 안 되는 경우
// Mutable Reference Type
class Counter { // ❌ var 프로퍼티가 있으면 Sendable 아님
var count = 0
}
// Reference를 포함한 Struct
struct ViewModel { // ❌ class 타입 포함
let counter: Counter // Counter가 Sendable 아님
}
언제 사용?
"컴파일러는 모르지만, 내가 thread-safe를 보장할 수 있을 때"
// ❌ 컴파일러 에러
class DataStorage: Sendable {
// Error: Stored property 'authKey' of 'Sendable'-conforming class
// 'DataStorage' is mutable
@KeychainStored(key: "auth_key")
var authKey: String?
}
// ✅ @unchecked로 수동 보장
class DataStorage: @unchecked Sendable {
@KeychainStored(key: "auth_key")
var authKey: String?
// PropertyWrapper 내부에서 Keychain API가 동기화 처리함
// 따라서 thread-safe를 수동으로 보장
}
실제 예시: PropertyWrapper로 thread-safety 보장
@propertyWrapper
struct KeychainStored {
var wrappedValue: String? {
get {
// Keychain API (Security.framework)
// 내부적으로 lock을 사용하여 동기화
return try? keychain.load(forKey: key)
}
set {
// 이것도 내부적으로 동기화됨
try? keychain.save(newValue, forKey: key)
}
}
}
왜 안전한가?
1. Keychain API (SecItemAdd, SecItemCopyMatching)는 Apple이 thread-safe 보장
2. UserDefaults도 Apple이 thread-safe 보장
3. 따라서 PropertyWrapper를 통한 접근은 안전
Actor Isolation 개념부터
@MainActor
class ViewModel {
var state = State() // MainActor에 격리됨
func update() {
state.isLoading = true // ✅ 항상 Main Thread
}
}
nonisolated란?
"이 프로퍼티/메서드는 Actor에 격리되지 않는다"
@MainActor
class ViewModel {
let id: String // Immutable이므로 어디서든 접근 가능
nonisolated var identifier: String {
return id // ✅ Actor 격리 없이 접근 가능
}
}
nonisolated(unsafe)란?
"격리 안 하는데, 안전성도 내가 책임진다"
@propertyWrapper
struct KeychainStored: Sendable {
private let key: String
// ❌ 컴파일 에러
private let keychain: KeychainHelper
// Error: KeychainHelper는 Sendable이 아님
// ✅ nonisolated(unsafe)로 해결
private nonisolated(unsafe) let keychain: KeychainHelper
// "KeychainHelper는 내부적으로 thread-safe니까 괜찮아"
}
실제 동작
// KeychainHelper는 Sendable이 아니지만
fileprivate final class KeychainHelper {
func save(_ value: String, forKey key: String) throws {
// Security framework 내부에서 동기화 처리
let status = SecItemAdd(query as CFDictionary, nil)
// Apple이 보장: 여러 스레드에서 호출해도 안전
}
}
// 따라서 nonisolated(unsafe)로 사용해도 안전
@propertyWrapper
struct KeychainStored: Sendable {
private nonisolated(unsafe) let keychain: KeychainHelper
}
| 개념 | 의미 | 언제 사용 | 예시 |
|---|---|---|---|
| Sendable | 컴파일러가 자동 검증 | Value type, immutable class | struct Point: Sendable |
| @unchecked Sendable | 수동으로 thread-safe 보장 | 컴파일러는 모르지만 내부적으로 안전 | class DataStorage: @unchecked Sendable |
| nonisolated(unsafe) | Actor 격리 해제 + 안전성 수동 보장 | Non-Sendable 타입을 Sendable 안에서 사용 | private nonisolated(unsafe) let keychain |
예시 1: DataStorage
// 왜 @unchecked Sendable?
final class DataStorage: @unchecked Sendable {
@KeychainStored(key: "auth_key")
var authKey: String? // ← mutable이지만 PropertyWrapper가 동기화
@UserDefault(key: "auto_login", defaultValue: false)
var isAutoLogin: Bool // ← mutable이지만 UserDefaults가 동기화
}
// 컴파일러 입장:
// "var 프로퍼티가 있는데 Sendable? 위험해!"
// → @unchecked 사용: "PropertyWrapper가 알아서 해줘"
예시 2: KeychainStored PropertyWrapper
@propertyWrapper
struct KeychainStored: Sendable { // ← PropertyWrapper 자체는 Sendable
private let key: String // ✅ Immutable, Sendable
// KeychainHelper는 Sendable 아님 (class이고 내부 state 있음)
// 하지만 Security framework가 동기화 처리
private nonisolated(unsafe) let keychain: KeychainHelper
var wrappedValue: String? {
get {
// 여러 스레드에서 동시 호출 가능
// Security framework가 내부에서 lock 처리
try? keychain.load(forKey: key)
}
set {
// 이것도 동기화됨
try? keychain.save(newValue, forKey: key)
}
}
}
예시 3: UserDefault PropertyWrapper
@propertyWrapper
struct UserDefault<T: Sendable>: Sendable {
// ↑ PropertyWrapper는 Sendable
// ↑ T도 Sendable이어야 함 (String, Int, Bool 등)
private let key: String
private let defaultValue: T // ✅ T: Sendable이므로 OK
// UserDefaults는 Sendable 아님 (class이고 내부 dictionary 있음)
// 하지만 Apple이 thread-safe 보장
private nonisolated(unsafe) let userDefaults: UserDefaults
var wrappedValue: T {
get {
// UserDefaults는 내부적으로 동기화됨
userDefaults.object(forKey: key) as? T ?? defaultValue
}
set {
// 이것도 동기화됨
userDefaults.set(newValue, forKey: key)
}
}
}
❌ 잘못된 사용 예시
// ❌ BAD - 실제로 thread-safe 아님
class UnsafeCounter: @unchecked Sendable {
var count = 0 // ← 전혀 동기화 안 됨!
func increment() {
count += 1 // ← Data Race 발생!
}
}
// ❌ BAD - 내부적으로 mutable state
@propertyWrapper
struct UnsafeWrapper: Sendable {
private nonisolated(unsafe) var cache: [String: Any] = [:]
// ↑ Dictionary는 thread-safe 아님!
var wrappedValue: Any {
get { cache["key"] ?? "" }
set { cache["key"] = newValue } // ← Data Race!
}
}
✅ 올바른 사용 예시
// ✅ GOOD - Apple framework가 동기화 보장
class DataStorage: @unchecked Sendable {
@KeychainStored(key: "auth_key") // ← Security framework
var authKey: String?
@UserDefault(key: "auto_login", defaultValue: false) // ← UserDefaults
var isAutoLogin: Bool
}
// ✅ GOOD - Lock으로 명시적 동기화
class ThreadSafeCounter: @unchecked Sendable {
private let lock = NSLock()
private var _count = 0
var count: Int {
get {
lock.lock()
defer { lock.unlock() }
return _count
}
set {
lock.lock()
defer { lock.unlock() }
_count = newValue
}
}
}
Sendable 체크리스트
// 1. Value Type → 자동 Sendable
struct Point { let x, y: Int } // ✅
// 2. Immutable Class → 자동 Sendable
final class Config { let key: String } // ✅
// 3. Mutable Class + Apple framework → @unchecked Sendable
class Storage: @unchecked Sendable {
@KeychainStored var key: String? // ✅ Keychain이 동기화
}
// 4. Non-Sendable을 Sendable 안에서 → nonisolated(unsafe)
struct Wrapper: Sendable {
nonisolated(unsafe) let helper: NonSendableHelper // ✅
}
기억할 것
Sendable: "다른 스레드로 안전하게 보낼 수 있다"
@unchecked Sendable: "내가 thread-safe를 보장한다" (책임은 개발자)
nonisolated(unsafe): "Actor 격리 안 하고, 내가 안전성 책임진다"
언제 사용해도 안전: Apple framework (Keychain, UserDefaults, CoreData 등)
절대 사용 금지: 직접 만든 mutable state without lock
// DataStorage 사용 시나리오
Task {
// Thread 1
DataStorage.shared.authKey = "key1"
print(DataStorage.shared.authKey) // "key1"
}
Task {
// Thread 2 (동시 실행)
DataStorage.shared.authKey = "key2"
print(DataStorage.shared.authKey) // "key2"
}
// 결과: Data Race 없음!
// 이유: Keychain API가 내부적으로 동기화
// → 항상 "key1" 또는 "key2" 중 하나만 읽힘
// → 절대 중간 상태나 크래시 없음