325 lines
7.1 KiB
Markdown
325 lines
7.1 KiB
Markdown
---
|
|
id: ios-strict-concurrency
|
|
title: Swift 6 Strict Concurrency — Sendable / Isolation
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [ios, swift, concurrency, vibe-coding]
|
|
tech_stack: { language: "Swift 6", applicable_to: ["iOS"] }
|
|
applied_in: []
|
|
aliases: [Swift 6, strict concurrency, Sendable, isolation, data race, complete checking]
|
|
---
|
|
|
|
# Swift 6 Strict Concurrency
|
|
|
|
> Swift 6 = data race 컴파일 시 잡음. **`Sendable` 강제, isolation 명시**. 점진 도입 (`@preconcurrency`). 큰 코드는 마이그레이션 작업.
|
|
|
|
## 📖 핵심 개념
|
|
- Strict concurrency: 모든 cross-actor 가 검증.
|
|
- Sendable: thread-safe 표시.
|
|
- Isolation: 어떤 actor / context.
|
|
- @preconcurrency: 옛 코드 호환 가뭄.
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### 활성화
|
|
```swift
|
|
// Package.swift
|
|
.target(
|
|
name: "MyApp",
|
|
swiftSettings: [
|
|
.enableExperimentalFeature("StrictConcurrency"),
|
|
]
|
|
)
|
|
|
|
// 또는 Xcode → Build Settings → Strict Concurrency Checking → Complete
|
|
```
|
|
|
|
```swift
|
|
// Swift 6 mode
|
|
swiftLanguageVersions: [.v6]
|
|
```
|
|
|
|
### Sendable 자동
|
|
```swift
|
|
// Struct 가 모든 field Sendable 면 자동 Sendable
|
|
struct User: Sendable {
|
|
let id: UUID
|
|
let email: String
|
|
}
|
|
|
|
// Class — 명시 필요
|
|
final class Config: Sendable {
|
|
let theme: String
|
|
init(theme: String) { self.theme = theme }
|
|
}
|
|
|
|
// Mutable class = Sendable X (자동)
|
|
class MutableState { // ❌ 다른 actor 로 못 보냄
|
|
var x = 0
|
|
}
|
|
```
|
|
|
|
### @MainActor
|
|
```swift
|
|
@MainActor
|
|
class ViewModel: ObservableObject {
|
|
@Published var users: [User] = []
|
|
|
|
func load() async {
|
|
let fetched = await api.fetchUsers() // off main
|
|
users = fetched // back to main
|
|
}
|
|
}
|
|
```
|
|
|
|
→ 자동 main thread.
|
|
|
|
### Actor isolation
|
|
```swift
|
|
actor BankAccount {
|
|
private var balance: Decimal = 0
|
|
|
|
func deposit(_ amount: Decimal) {
|
|
balance += amount
|
|
}
|
|
|
|
var current: Decimal { balance }
|
|
}
|
|
|
|
let acc = BankAccount()
|
|
await acc.deposit(100) // await 필요 (cross-actor)
|
|
let bal = await acc.current
|
|
```
|
|
|
|
### Isolation 명시
|
|
```swift
|
|
@MainActor
|
|
func updateUI() { ... }
|
|
|
|
@globalActor actor DatabaseActor {
|
|
static let shared = DatabaseActor()
|
|
}
|
|
|
|
@DatabaseActor
|
|
func saveToDB() { ... }
|
|
```
|
|
|
|
### nonisolated
|
|
```swift
|
|
actor Logger {
|
|
private var logs: [String] = []
|
|
func add(_ msg: String) { logs.append(msg) }
|
|
|
|
nonisolated var name: String { "AppLogger" } // 동기 read OK
|
|
}
|
|
```
|
|
|
|
### Cross-actor 호출
|
|
```swift
|
|
@MainActor func ui() {}
|
|
actor DB { func write() {} }
|
|
|
|
let db = DB()
|
|
|
|
@MainActor
|
|
func handle() async {
|
|
await db.write() // cross-actor — await + Sendable args
|
|
}
|
|
```
|
|
|
|
### Sending parameters (Swift 6 의 새 feature)
|
|
```swift
|
|
// Sendable X 인 객체도 일회성 전달 OK
|
|
actor Worker {
|
|
func process(_ data: sending NonSendable) {
|
|
// data 는 caller 가 더 사용 X (move-like)
|
|
}
|
|
}
|
|
```
|
|
|
|
→ Class 가 mutable 도 한 번만 보내면 OK.
|
|
|
|
### @preconcurrency (legacy)
|
|
```swift
|
|
// 옛 lib import
|
|
import @preconcurrency Foundation
|
|
|
|
// 또는 specific
|
|
@preconcurrency import OldLib
|
|
```
|
|
|
|
→ Sendable warning 무시 (옛 lib 가 안 표시).
|
|
|
|
### Async let + Sendable
|
|
```swift
|
|
async let user = api.fetchUser(id) // Sendable result
|
|
async let orders = api.fetchOrders(uid)
|
|
let dashboard = try await Dashboard(user: user, orders: orders)
|
|
```
|
|
|
|
### TaskGroup — Sendable result
|
|
```swift
|
|
func loadAll() async throws -> [URL: Data] {
|
|
try await withThrowingTaskGroup(of: (URL, Data).self) { group in
|
|
for url in urls {
|
|
group.addTask {
|
|
let (data, _) = try await URLSession.shared.data(from: url)
|
|
return (url, data) // tuple 가 Sendable
|
|
}
|
|
}
|
|
var result: [URL: Data] = [:]
|
|
for try await (url, data) in group {
|
|
result[url] = data
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
```
|
|
|
|
### Inheritance 와 Sendable
|
|
```swift
|
|
class Base: Sendable {
|
|
let name: String
|
|
init(name: String) { self.name = name }
|
|
}
|
|
|
|
final class Sub: Base, @unchecked Sendable { // 명시 필요
|
|
// ...
|
|
}
|
|
|
|
// final 안 + Sendable = 어려움 — final 권장.
|
|
```
|
|
|
|
### @unchecked Sendable (lock 직접 관리)
|
|
```swift
|
|
final class ThreadSafeCache: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var storage: [String: String] = [:]
|
|
|
|
func set(_ key: String, _ value: String) {
|
|
lock.lock(); defer { lock.unlock() }
|
|
storage[key] = value
|
|
}
|
|
}
|
|
```
|
|
|
|
→ 검사 우회 — 책임 너에게.
|
|
|
|
### Migration 점진
|
|
```swift
|
|
// Phase 1: Strict concurrency = Minimal (default)
|
|
// Phase 2: Targeted (특정 module)
|
|
// Phase 3: Complete
|
|
|
|
// Module 별
|
|
.target(
|
|
name: "Core",
|
|
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]
|
|
)
|
|
```
|
|
|
|
### Common errors + fix
|
|
```swift
|
|
// Error: "Sending non-Sendable value"
|
|
class MyData { var x = 0 }
|
|
Task { await actor.use(MyData()) } // ❌
|
|
|
|
// Fix 1: struct
|
|
struct MyData: Sendable { var x = 0 }
|
|
|
|
// Fix 2: final class + Sendable
|
|
final class MyData: Sendable {
|
|
let x: Int
|
|
init(x: Int) { self.x = x }
|
|
}
|
|
|
|
// Fix 3: sending (Swift 6)
|
|
actor MyActor {
|
|
func use(_ data: sending MyData) { ... }
|
|
}
|
|
```
|
|
|
|
```swift
|
|
// Error: "Cannot use mutable state from another isolation"
|
|
@MainActor var counter = 0
|
|
|
|
Task {
|
|
counter += 1 // ❌ — main actor 외
|
|
}
|
|
|
|
// Fix
|
|
Task { @MainActor in
|
|
counter += 1
|
|
}
|
|
|
|
// 또는
|
|
await MainActor.run { counter += 1 }
|
|
```
|
|
|
|
### Async sequence isolation
|
|
```swift
|
|
@MainActor
|
|
class Stream {
|
|
func observe() async {
|
|
for await value in someAsyncStream { // 다른 actor 가 yield
|
|
// value 가 Sendable 인지 검증
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Performance impact
|
|
```
|
|
거의 없음 — 컴파일 시 검사.
|
|
일부 boxing 추가 가능 (cross-actor sending).
|
|
```
|
|
|
|
### Tooling
|
|
```
|
|
Xcode: warning / error 표시.
|
|
swift -strict-concurrency=complete: CLI.
|
|
```
|
|
|
|
### Adoption advice
|
|
```
|
|
1. 새 프로젝트 = Swift 6 mode + complete.
|
|
2. 기존 = minimal → targeted → complete 점진.
|
|
3. 한 module 씩 + @preconcurrency 로 의존.
|
|
4. Sendable struct 우선 (class 보다).
|
|
5. @MainActor / actor isolation 명시.
|
|
```
|
|
|
|
## 🤔 의사결정 기준
|
|
| 상황 | 추천 |
|
|
|---|---|
|
|
| 새 프로젝트 | Swift 6 + Complete |
|
|
| 기존 큰 codebase | 점진 (minimal → complete) |
|
|
| Old library 의존 | @preconcurrency |
|
|
| UI / ViewModel | @MainActor |
|
|
| Mutable shared state | actor |
|
|
| Pure data | struct + Sendable |
|
|
|
|
## ❌ 안티패턴
|
|
- **@unchecked Sendable 남발**: data race 검사 무효.
|
|
- **모든 거 @MainActor**: contention.
|
|
- **Class 가 모든 곳 — Sendable 어려움**: struct 우선.
|
|
- **Cross-actor 무거운 데이터 매번**: copy 비용. id 만 전달.
|
|
- **nonisolated + mutable**: race.
|
|
- **@preconcurrency 영원 유지**: 점진 마이그.
|
|
- **`Task.detached` 무절제**: priority / context 잃음.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- Swift 6 = Strict concurrency default.
|
|
- Sendable struct + actor + @MainActor 3종.
|
|
- @preconcurrency 점진 마이그.
|
|
- @unchecked 마지막 카드.
|
|
|
|
## 🔗 관련 문서
|
|
- [[iOS_Swift_Concurrency_Actor_Patterns]]
|
|
- [[iOS_Swift_Concurrency_async_await]]
|
|
- [[iOS_Swift_Macros]]
|