Actor는 Swift 5.5+에서 도입된 타입으로, 언어 수준에서 데이터 경합 문제를 해결합니다. 수동 잠금 및 DispatchQueue 대기열과 달리, actor는 자동으로 상태를 격리하고 액세스를 동기화합니다. 즉, 두 스레드가 동시에 actor 타입의 동일한 속성을 수정할 수 없으므로 개발자의 추가 노력 없이 경합 상태가 제거됩니다. 이 메커니즘은 컴파일러가 actor의 속성 및 메서드에 대한 액세스를 추적하는 actor 격리 개념을 기반으로 합니다. Apple, 2025에 따르면, actor 모델은 Swift 동시성 시스템의 근본적인 부분입니다.
핵심 사항
Actor는 클래스와 유사한 참조 타입이지만 데이터 경합에 대한 자동 보호 기능이 있습니다. Swift 5.5에서 async/await 및 Task와 함께 동시성 시스템의 일부로 도입되었습니다. Actor는 명시적 동기화 없이 여러 스레드에서 동시에 가변 상태를 읽거나 쓰지 않음을 보장합니다.
Actor를 선언하려면 actor 키워드를 사용하고 그 뒤에 멤버가 포함된 중괄호를 사용합니다. 구문적으로 actor는 클래스와 유사하지만 동작이 근본적으로 다릅니다.
actor BankAccount {
private var balance: Double
init(initialBalance: Double) {
self.balance = initialBalance
}
func deposit(amount: Double) {
balance += amount
}
func getBalance() -> Double {
return balance
}
}
컴파일러는 모든 actor 속성과 메서드를 자동으로 격리하여 actor 컨텍스트 내에서만 액세스 가능하도록 합니다. actor 외부에서 balance에 액세스하려고 하면 호출이 async로 표시되지 않은 경우 컴파일러가 오류를 생성합니다.
데이터 격리는 actor의 핵심 개념입니다. Actor는 actor 실행기 메커니즘을 통해 상태에 대한 상호 배타적 액세스를 보장합니다. 각 actor는 격리된 멤버에 대한 모든 액세스를 순차적으로 처리하는 자체 실행기를 가지고 있습니다.
Actor 외부의 코드가 해당 메서드를 호출하면 호출이 actor 실행기 대기열에 배치됩니다. Actor는 한 번에 하나의 작업만 실행하여 경합이 없음을 보장합니다. 두 스레드가 동시에 deposit을 호출하면 두 번째 호출은 첫 번째 호출이 완료될 때까지 기다립니다.
let account = BankAccount(initialBalance: 1000.0)
// 비동기 호출 — actor 외부에서 필요
await account.deposit(amount: 500.0)
let currentBalance = await account.getBalance()
Actor 메서드에 대한 모든 호출에는 await가 필요합니다. actor가 다른 작업으로 바쁠 수 있기 때문입니다. 이것은 버그가 아니라 경합을 방지하기 위한 의도적인 설계입니다. Swift는 암시적으로 actor 속성의 getter와 setter를 비동기로 만들므로 속성을 읽는 데도 await가 필요합니다.
Actor와 클래스는 모두 참조 타입이지만 멀티스레드 환경에서의 동작은 크게 다릅니다. 클래스는 경합에 대한 자동 보호를 제공하지 않는 반면, actor는 타입 시스템을 통해 컴파일러 수준에서 이를 내장합니다.
| 특성 | Actor | Class |
|---|---|---|
| 경합 보호 | 자동, 컴파일러 수준 | 수동 동기화 필요 |
| 속성 액세스 | 외부에서 await를 통해서만 | 직접, 동기화 없음 |
| 상속 | 다른 actor에서만 | 표준 클래스 기반 |
| 프로토콜 준수 | 프로토콜 준수 가능 | 표준 |
| 성능 | 격리로 인한 낮은 오버헤드 | 동기화 없이 더 빠름 |
Actor는 다른 actor에서만 상속받을 수 있으며 클래스에서는 상속받을 수 없습니다. 이는 의도적인 것으로, 클래스에는 actor 격리 메커니즘이 없고 타입을 혼합하면 안전 보장이 깨지기 때문입니다.
actor SavingsAccount: BankAccount {
func applyInterest(rate: Double) {
let interest = balance * rate
balance += interest
}
}
비동기 호출은 외부 코드에서 actor와 상호 작용하는 메커니즘입니다. Actor가 상태를 격리하므로 외부에서 멤버에 액세스하려면 await가 필요합니다. 이를 통해 Swift는 호출 코드가 스레드를 차단하지 않고 actor가 다른 요청을 처리할 수 있음을 보장합니다.
선언된 actor 타입 외에도 Swift는 전역 actor — @MainActor 속성을 지원합니다. 이는 클래스, 속성 또는 메서드를 기본 스레드에서 실행 가능한 것으로 표시합니다. 이는 iOS 애플리케이션의 UI 코드에 특히 유용합니다.
@MainActor
class ViewModel: ObservableObject {
@Published var title: String = ""
func updateTitle() {
// 이 코드는 기본 스레드에서 실행되는 것이 보장됩니다
title = "New Title"
}
}
@MainActor를 사용하면 수동으로 DispatchQueue.main.async를 호출할 필요가 없어 코드가 더 깔끔하고 안전해집니다. 컴파일러는 기본 스레드로의 전환이 올바르게 발생하는지 확인합니다.
Nonisolated는 actor의 메서드 또는 계산된 속성을 격리되지 않은 것으로 표시할 수 있는 키워드입니다. 이러한 멤버는 actor 격리 상태에 액세스할 수 없지만 actor 외부에서 await 없이 호출할 수 있습니다.
Nonisolated 메서드는 actor의 가변 상태에 의존하지 않는 계산에 유용합니다. 예를 들어 formatBalance 메서드는 balance에 직접 액세스하지 않고 전달된 값의 형식만 지정합니다. 이러한 메서드는 nonisolated로 만들어도 안전합니다.
actor BankAccount {
private var balance: Double = 0
nonisolated func formatBalance(amount: Double) -> String {
return "$\(amount)"
}
}
Nonisolated 멤버는 동기적으로 실행되며 await가 필요하지 않습니다. 그러나 actor 격리 속성을 직접 읽을 수는 없습니다. Nonisolated 메서드가 actor의 값이 필요한 경우 매개변수로 전달되어야 합니다.
Reentrancy는 비동기 호출을 기다리는 동안 actor에 재진입을 허용하는 메커니즘입니다. Reentrancy가 없으면 한 메서드가 다른 메서드를 기다리고 그 메서드가 다시 첫 번째 메서드를 기다리는 경우 actor가 영원히 교착 상태에 빠질 수 있습니다.
Actor 내부의 코드가 await를 실행하면 actor는 현재 작업을 일시 중단하고 대기열에 있는 다른 작업을 처리할 수 있습니다. Await가 완료되면 작업이 재개됩니다. 이렇게 하면 교착 상태가 방지되지만 주의가 필요합니다. await 지점 사이에서 actor 상태가 변경될 수 있습니다.
actor DataProcessor {
var cache: [Int: String] = [:]
func process(id: Int) async -> String {
if let cached = cache[id] {
return cached
}
// await — 재진입 지점
let result = await fetchData(id: id)
// await 후 캐시가 변경되었을 수 있습니다 — 다시 확인하세요
cache[id] = result
return result
}
}
개발자는 재진입을 고려하고 await 지점 이후에 actor 상태를 확인해야 합니다. 일반적인 실수는 actor 격리가 비동기 일시 중단 지점을 통해 지속된다고 가정하는 것입니다. 실제로는 await와 다음 명령문 사이에 상태가 예상과 다를 수 있습니다.
자주 묻는 질문
Actor는 자동으로 데이터 경합으로부터 상태를 격리하며 외부 액세스에 await가 필요합니다. 클래스는 이러한 보호를 제공하지 않습니다 — 개발자는 잠금 또는 대기열을 통한 동기화를 담당합니다. Actor는 actor에서만 상속받고, 클래스는 클래스에서 상속받습니다.
네, actor는 다른 actor에서 상속받을 수 있습니다. 하위 클래스는 부모의 모든 격리된 속성과 메서드를 상속받습니다. Actor는 클래스에서 상속받을 수 없습니다. 클래스는 컴파일러 수준에서 actor 격리 메커니즘이 없기 때문입니다.
actor-isolated 컨텍스트는 actor의 가변 상태에 대한 직접 액세스가 허용되는 코드 영역입니다. 격리된 것으로 표시된 (기본값) actor 메서드 내에서는 await 없이 속성을 읽고 쓸 수 있습니다. 컴파일러가 격리 경계를 확인합니다.
Actor의 데이터는 Sendable 타입을 반환하는 비동기 메서드를 통해 전달되거나, 매개변수로 값을 받는 nonisolated 메서드를 통해 전달됩니다. Sendable 구조체로 actor 상태의 스냅샷을 반환하는 async 속성을 만들 수도 있습니다.
네, actor는 프로토콜을 준수할 수 있습니다. 프로토콜에 격리 요구 사항(actor-isolated)이 포함된 경우 자동으로 actor 격리가 됩니다. 프로토콜의 비동기 메서드의 경우, isolated 마커를 사용하여 특정 actor에서 호출되어야 함을 지정할 수 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.