이번 포스팅에서도 Actor에 대해 배워봅니다. 이번 포스팅에서는 비동기적으로 접근하는 property를 동기적으로 접근할 수 있도록 해주는 nonisolated keyword에 대해서 알아봅니다.
actor SomeActor {
let id = UUID()
var num = 0
func increaseNum() {
num += 1
}
}
위 actor에서 id와 num은 각각 상수와 변수입니다. 각각 property에 접근할 때는 상수는 동기적으로 변수는 비동기적으로 접근해야 합니다.
print(actor.id)
print(await actor.num)
위 두개의 property는 stored property인데요. actor에는 stored 말고도 computed property도 존재합니다. 그렇다면 computed property는 동기적으로 접근할까요? 아니면 비동기적으로 접근할까요?
computed property는 기본적으로 가변성(mutable)을 띕니다. 즉 호출할 때마다 달라질 수 있다는 것이지요. 따라서 변수와 동일하게 비동기적으로 접근해야 합니다.
actor SomeActor {
let id = UUID()
var num = 0
var description: String {
"actor(id: \(id.uuidString)) has num of \(num)"
}
func increaseNum() {
num += 1
}
}
print(await actor.description)
하지만 불변하는 computed property가 있을 수도 있습니다. 아래 uuidString의 경우 computed property이지만 let으로 선언된 id만으로 구해진 값입니다. 따라서 절대로 변할 일이 없습니다.
actor SomeActor {
let id = UUID()
var num = 0
var uuidString: String {
id.uuidString
}
}
하지만 이런 경우에도 호출할 때는 무조건 비동기적으로 해야 합니다.
print(await actor.uuidString)
이렇게 모든 것을 비동기적으로 호출하는 것은 불편합니다… 위 uuidString의 경우에는 동기적으로 호출해도 아무런 문제가 없는데도 말이죠. 이런 경우 앞에 nonisolated를 붙여서 동기적으로 호출되도록 할 수 있습니다. 해당 property를 actor-isolated 하지 않게 만들어주는 것이죠.
actor SomeActor {
let id = UUID()
var num = 0
nonisolated var uuidString: String {
id.uuidString
}
}
print(actor.uuidString)
참고로 nonisolated는 computed property 뿐만 아니라 func에도 붙일 수 있습니다. 아래 method들은 동일한 코드를 가진 함수인데 nonisolated를 붙이면 동기적으로 호출이 가능해집니다.
actor SomeActor {
let id = UUID()
var num = 0
nonisolated var uuidString: String {
id.uuidString
}
func printActor1() {
print("actor")
}
nonisolated func printActor2() {
print("actor")
}
}
await actor.printActor1()
actor.printActor2() //👉 nonisolated는 동기적으로 호출 가능
무조건 nonisolated 키워드를 붙이면 동기적으로 호출이 가능해지는 것은 아닙니다. 컴파일러가 판단했을 때 동기적으로 호출할 경우 문제가 될 수 있는 computed property 혹은 method는 에러가 납니다.
아래 예시의 경우 computed property와 method 모두 mutable state인 num을 포함하고 있으므로 actor-isolated하게 사용해야만 합니다.
actor SomeActor {
let id = UUID()
var num = 0
//🚫 Actor-isolated property 'num' can not be referenced from a non-isolated context
nonisolated var description: String {
"actor(id: \(id.uuidString)) has num of \(num)"
}
//🚫 Actor-isolated property 'num' can not be referenced from a non-isolated context
nonisolated func increaseNum() {
num += 1
}
}
nonisolated는 각종 Protocol들을 채택할 때 편리하게 사용할 수 있습니다. 예를 들어 Identifiable은 id 값에 Hashable의 경우는 hash 함수에 동기적으로 접근할 수 있어야 준수할 수 있습니다.
각각 어떤 예시가 있는지 보도록 하겠습니다.
아래와 같은 actor를 만들었습니다. 그리고 Identifiable을 채택하기 위해서 id를 computed property로 만들었는데요. 아시다시피 해당 computed property는 actor-isolated입니다. 따라서 아래와 같은 에러가 뜹니다.
actor Product: Identifiable {
let name: String
let category: String
var price: Int
//🚫 Actor-isolated property 'id' cannot be used to satisfy nonisolated protocol requirement
var id: String {
"\(name)_\(category)"
}
init(name: String, category: String, price: Int) {
self.name = name
self.category = category
self.price = price
}
}
해석해보면 actor-isolated 속성인 id는 nonisolated인 프로토콜의 요구사항을 만족시키는데 사용될 수 없다는 것입니다. 따라서 id를 nonisolated로 만들 필요가 있습니다.
actor Product: Identifiable {
let name: String
let category: String
var price: Int
nonisolated var id: String {
"\(name)_\(category)"
}
init(name: String, category: String, price: Int) {
self.name = name
self.category = category
self.price = price
}
}
자 이렇게 하면 아래처럼 Identifiable을 요구하는 ForEach문에서 actor를 사용할 수 있습니다.
struct ContentView: View {
let products = [
Product(name: "iphone", category: "smart phone", price: 100),
Product(name: "mac", category: "desktop", price: 200),
Product(name: "mac", category: "laptop", price: 300)]
var body: some View {
ForEach(products) { product in
Text("Product: \(product.name)")
}
}
}
Hashable을 채택하기 위해서는 hash라는 함수를 구현해야 합니다. 아래처럼 구현해보도록 하겠습니다. (참고로 ==를 구현한 이유는 Hashable을 채택하기 위해서는 Equtable을 채택해야 하기 때문입니다.)
actor Product: Hashable {
let name: String
let category: String
var price: Int
func hash(into hasher: inout Hasher) {
hasher.combine("\(name)_\(category)")
}
static func == (lhs: Product, rhs: Product) -> Bool {
"\(lhs.name)_\(lhs.category)" == "\(rhs.name)_\(rhs.category)"
}
init(name: String, category: String, price: Int) {
self.name = name
self.category = category
self.price = price
}
}
역시 에러가 발생하는데요. 마찬가지로 내용은 actor-isolated한 hash 함수는 nonisolated인 프로토콜의 요구사항을 만족시키는데 사용할 수 없다는 것입니다. 마찬가지로 nonisolated 키워드를 활용합니다.
actor Product: Hashable {
let name: String
let category: String
var price: Int
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine("\(name)_\(category)")
}
static func == (lhs: Product, rhs: Product) -> Bool {
"\(lhs.name)_\(lhs.category)" == "\(rhs.name)_\(rhs.category)"
}
init(name: String, category: String, price: Int) {
self.name = name
self.category = category
self.price = price
}
}
에러는 사라지고 아래처럼 Hashable을 요구하는 Set에서 사용할 수 있습니다
let productSet = Set<Product>()
actor는 thread-safe를 위해서 actor-isolation이라는 안전 장치를 가지고 있는데요. 개발자 입장에서 해당 안전 장치는 유용하기도 하지만 때문에 불편한 상황도 자주 발생합니다.
오늘은 그 안전 장치를 우회하는 nonisolated 키워드에 대해서 알아봤습니다. actor 포스팅도 거의 끝나가네요. 잘 마무리해보도록 하겠습니다.
좋은 글 감사합니다!