동시성 프로그래밍에서 가장 큰 골칫거리는 바로 데이터 레이스(data race)입니다.
Swift Concurrency는 이러한 문제를 구조적으로 해결하며, Swift 6에서는 더욱 엄격하고 안전하게 동작합니다.
여러 작업(Task, Thread)이 동시에 같은 데이터에 접근하거나 수정하면
서로 엉켜서 데이터가 손상(잘못된 값, 예측불가 결과)되는 현상을 말합니다.
Swift에서는 Task(작업)를 하나의 보트, Actor를 섬에 비유합니다.
Actor는 데이터의 안전한 컨테이너, 여러 Task가 동시에 접근하지 못하도록 완전히 격리합니다.
actor Island {
var food: [Fruit] = []
func add(food: Fruit) {
food.append(food)
}
}
데이터를 바꿀 때 한 번에, 중간에 끼어들지 않는 처리를 의미합니다.
Swift Actor는 내부적으로 원자성을 보장하려고 노력하지만
await로 작업이 잠시 쉬면 다른 Task가 들어와 상태가 변할 위험이 있습니다.
예시:
func depositPineapple(island: Island, pineapple: Fruit) async {
var food = await island.food
food.append(pineapple)
await island.add(food: pineapple)
}
await를 쓰는 순간, 다른 Task가 food를 건드릴 수 있으니 조심해야 합니다!Task가 여러 개일 때, 순서를 맞춰야 안전하게 처리할 수 있습니다.
let stream = AsyncStream<Fruit> { continuation in
// 이벤트를 비동기로 발생
}
for await fruit in stream {
await island.add(food: fruit)
}
Swift 5.7 이후로 Sendable 프로토콜이 도입되어
비동기 환경에서 안전하게 전달 가능한 타입만 Task/Actor 경계에서 쓸 수 있습니다.
struct Pineapple: Sendable {
let id: Int
}
func transferFruit(_ fruit: Pineapple) async {
// 안전하게 전달
}
또한, 컴파일러 수준으로 엄격한 체크 옵션(Minimal/Targeted/Complete)으로 경고와 에러를 잡아냅니다.
Actor 간 데이터 전달, 순서와 atomicity 주의!
actor Bank {
private var balance: Int = 0
func deposit(amount: Int) {
balance += amount
}
func withdraw(amount: Int) -> Bool {
guard balance >= amount else { return false }
balance -= amount
return true
}
}
func transfer(bank: Bank, amount: Int) async {
await bank.deposit(amount: amount)
}
Task {
await transfer(bank: myBank, amount: 100)
}
await로 상태 변경이 일어난다면, 불변성/atomicity 보장에 신경써야 함| 장점 | 한계 |
|---|---|
| 데이터 레이스 완벽 차단 | 격리 설계 신중 필요 |
| 안전한 동시성 모델 | 성능 최적화 추가 노력 |
| 컴파일러 체크로 예방 용이 | legacy 코드 적용 어려움 |
Swift의 동시성은 언어 차원에서 Data Race를 막아주는 강력한 도구입니다.
Task 격리, Actor 격리, Sendable 체크, atomicity 관리까지
안전한 동시성 앱, 서버, 서비스 개발에 필수며
Swift 6에서는 더욱 엄격하게 적용됩니다.