비동기 프로그래밍에서 가장 까다로운 적은 단연 데이터 레이스(Data Race)다.
여러 스레드가 동시에 같은 공유 자원에 접근하여 데이터를 수정하려 할 때 발생하는 이 문제는 예측 불가능한 버그와 크래시를 유발한다.
Swift 5.5는 이를 언어 차원에서 해결하기 위해 Actor라는 새로운 참조 타입을 도입했다.
클래스와 닮았지만 결정적인 차이가 있는 Actor의 실체를 파헤쳐 본다.
Actor는 클래스와 마찬가지로 참조 타입(Reference Type)이지만, 가장 큰 특징은 가변 상태(Mutable State)를 외부로부터 격리한다는 점이다.
Actor는 한 번에 하나의 작업만 자신의 상태에 접근할 수 있도록 보장한다.
즉, 여러 스레드에서 동시에 접근하려 해도 Actor 내부의 '사서'가 한 명씩 순서대로 처리해 주는 것과 같다.
이를 통해 별도의 Lock이나 Semaphore 없이도 완벽한 Thread-Safety를 달성한다.
클래스로 구현했다면 여러 스레드에서 동시에 increment()를 호출할 때 숫자가 꼬이겠지만, Actor는 이를 방지한다.
actor BankAccount {
private var balance: Int = 0
func deposit(amount: Int) {
balance += amount
}
func getBalance() -> Int {
return balance
}
}
Actor의 속성이나 메서드는 외부에서 접근할 때 반드시 비동기(async)로 처리해야 한다. 다른 작업이 진행 중일 때 기다려야 할 수도 있기 때문이다.
let account = BankAccount()
Task {
// 내부 상태를 변경하거나 읽을 때 await 필수
await account.deposit(amount: 1000)
let currentBalance = await account.getBalance()
print("현재 잔액: \(currentBalance)")
}
@MainActor): UI와 관련된 작업은 항상 메인 스레드에서 돌아가야 하므로, 시스템에서 제공하는 특별한 Global Actor인 @MainActor를 사용한다.await를 사용해야 한다.