저번에 Sendable
에 대해 공부하면서 새로 등장한 타입, Actor
에 대해 포스팅한다고 했습니다!
그 약속 지키러 왔습니다! 후훗~
공부하면서 느낀 부분인데, Actor
요 녀석! 아주 요물입니다!
그러면 차근차근 알아보러 같이 떠나시죠!
먼저 Actor
가 어떤 타입인지 알아보기 전에, Apple이 왜 만들었을까?를 파헤쳐보는 것이 좋을 거 같습니다.
이 질문에 대한 답변은 WWDC 2021 - Protect mutable state with Swift actors에서 자세히 다루고 있습니다.
관심 있으신 분들은 보시고, Actor
에 대해 공부하시는 걸 추천드립니다.
Swift에는 프로그램 전체에서 공유되는 변경 가능한 상태를 선언하는 메커니즘을 제공하는 클래스가 포함되어 있습니다. 그러나 클래스는 동시 프로그램 내에서 올바르게 사용하기가 매우 어렵기로 악명 높으며, 데이터 레이스를 피하기 위해 오류가 발생하기 쉬운 수동 동기화가 필요합니다. 우리는 공유 변경 가능 상태를 사용하는 동시에 데이터 레이스 및 기타 일반적인 동시성 버그에 대한 정적 감지 기능을 제공하고자 합니다.
Sendable을 class 타입에 어떻게 준수해야하는지 다들 기억 나시나요?
생각 안 나시는 분들은 Sendable 다시 읽고 와주십쇼!
결론부터 말하자면, 제약이 엄청 많았습니다.
저는 참조 타입의 변동성을 컴파일러가 알 수 없어, 저런 제약이 걸려있다라고 생각했지만 반은 맞고 반은 틀렸네요.
클래스는 변경 가능한 상태라 데이터 레이스를 피할 수 없어 이러한 제약이 걸려 있었군요!
그렇다면 참조 타입은 클래스 밖에 없는데, 참조 타입으로는 데이터 레이스를 해결하기 힘들까요?
그래서 등장한 것이 Actor
타입입니다!
Actor
는 공유된 변경 가능 상태에 대한 동기화 메커니즘입니다.Actor
에는 자체 상태가 있으며 해당 상태는 프로그램의 나머지 부분과 격리됩니다. 해당 상태에 접근하는 유일한 방법은Actor
를 통과하는 것입니다. 그리고Actor
를 통과할 때마다Actor
의 동기화 메커니즘은 다른 코드가Actor
의 상태에 동시에 접근하지 않도록 보장합니다. 이는 잠금 또는 직렬 디스패치 큐를 수동으로 사용하여 얻는 것과 동일한 상호 배제 속성을 제공 하지만Actor
의 경우 Swift에서 기본적으로 제공합니다. 동기화를 수행해주지 않으면 Swift가 컴파일러 오류를 생성하므로 동기화 수행을 잊어서는 안 됩니다.
Actor
타입의 특징은 인스턴스 데이터를 프로그램의 나머지 부분과 분리하고 해당 데이터에 대한 동기화된 접근을 보장한다는 것입니다.
어떻게 가능할까요?
아래 인용문과 예제 코드는 swift-evolution > proposals > 0306-actors.md을 참고하였습니다.
Actor
격리는 액터가 변경 가능한 상태를 보호하는 방법입니다.Actor
의 경우 이 보호를 위한 기본 메커니즘은 저장된 인스턴스 속성에만 자체적으로 직접 액세스할 수 있도록 허용하는 것입니다. 예를 들어, 다음은 한 계좌에서 다른 계좌로 돈을 이체하는 방법입니다.
actor BankAccount {
let accountNumber: Int
var balance: Double
init(accountNumber: Int, initialDeposit: Double) {
self.accountNumber = accountNumber
self.balance = initialDeposit
}
}
extension BankAccount {
enum BankError: Error {
case insufficientFunds
}
func transfer(amount: Double, to other: BankAccount) throws {
if amount > balance {
throw BankError.insufficientFunds
}
print("Transferring \(amount) from \(accountNumber) to \(other.accountNumber)")
balance = balance - amount
other.balance = other.balance + amount // error: actor-isolated property 'balance' can only be referenced on 'self'
}
}
BankAccount가 클래스인 경우
transfer(amount:to:)
메서드는 형식이 잘 구성되어 있지만 외부 잠금 메커니즘 없이 동시 코드에서 데이터 레이스가 발생할 수 있습니다.
Actor
의 경우other.balance
를 참조하려고 하면 컴파일러 오류가 발생합니다. 왜냐하면balance
는 자체에서만 참조될 수 있기 때문입니다. 오류 메시지에는balance
가Actor
격리되어 있다는 점, 즉balance
가 연결되거나 "격리된" 특정Actor
내에서만 직접 접근할 수 있음을 나타냅니다. 이 경우self
가 참조하는BankAccount
인스턴스입니다. 저장 및 계산된 인스턴스 프로퍼티(예:balance
), 인스턴스 메서드(예:transfer(amount:to:)
) 및 인스턴스 첨자를 포함하여Actor
인스턴스에 대한 모든 선언은 기본적으로 모두Actor
격리됩니다.Actor
분리 선언은 동일한 행위자 인스턴스(self
)의 다른Actor
분리 선언을 자유롭게 참조할 수 있습니다.Actor
분리되지 않은 선언은 분리되지 않으며Actor
분리 선언에 동기적으로 접근할 수 없습니다.
정리하자면, Actor의 인스턴스 프로퍼티는 그 자체에서만 참조될 수 있기 때문에 외부에서 참조할 경우 컴파일러 오류가 발생합니다. 이러한 특징을 swift에서는 isolation(격리)라고 표현합니다.
Actor
의 내부 동기화 메커니즘은 하나의 increment()
가 완료된 후에 다음 increment()
가 실행되도록 해줍니다. 따라서 실제로 실행해보면 하나가 먼저 처리되고 그 다음이 처리됩니다.
격리 기능으로 인한 것은 이해했지만, 정확히 어떤 원리로 가능한 것인지 아직은 의문이 생깁니다. 위의 WWDC를 보면서 그 의문이 조금 해결되었습니다.
외부에서 Actor와 상호작용할 때마다 비동기로 처리됩니다. 만약 Actor가 사용 중이라면 사용 중인 CPU가 Actor에 접근 중인 코드를 일시 정지하고 다른 작업을 수행합니다. 이후에 Actor의 사용이 끝나면 정지해둔 코드를 다시 실행해서 Actor에 접근합니다. 위 코드에서 await 키워드는 Actor에 대한 비동기 호출이 일시 정지될 수도 있음을 나타냅니다.
(이 부분은 해석이 잘 안 되어서 [WWDC 2021] Protect mutable state with Swift actors 블로그를 참고했습니다.)
오... 제 뇌피셜이긴 한데, 컴파일러가 Actor의 사용을 추적하여 작업 순서를 정해주는 느낌이 듭니다. 그러면 당연히 비동기 작업이 잠시 일시정지 될 수도 있겠군요!
다음 내용들은 Actor
에 protocol
을 어떻게 적용하는지, closure
와 함수를 다루는 내용들도 있고.. 문서의 경우, Cross-actor
를 다루고 있습니다. 관심 있으신 분들은 위의 링크를 눌러 공부해보셔도 좋을 거 같습니다👍
일단 여기까지 Actor
에 대해 간단히 알아보았습니다.
앞서 말씀드린 내용들도 같이 다뤄보고 싶었지만... 이번주는 취업 준비로 시간이 많이 없었네요🥺 죄송합니다🙇♀️
Actor
는 공유된 변경 가능 상태에 대한 동기화 메커니즘입니다.Actor
격리 기능은 액터가 변경 가능한 상태를 보호하여 데이터 레이스를 방지해줍니다.
너무 잘 읽었습니다 !! ㅎㅎ