(※ hackingWithSwift의 글을 번역한 것으로 아래 출처를 남겨두었습니다. 약간의 오역이 있을 수 있으니 지적해주시면 감사드리겠습니다.)
actor가 분산되어 동작할 수 있다. 즉, remote procedure call(RPC)을 통해서 프로퍼티를 읽고 쓰거나 네트워를 통해서 메소드를 호출할 수 있다.
상상하면 복잡한 문제 같지만, 다음 세 가지를 통해 좀 더 쉽게 다가올 것이다.
우리만의 actor 통신 시스템을 구축하는 것을 권장하기보다 Apple은 ready-made implementation(바로 사용할 수 있는 방법)을 제공한다. Apple은 결과적으로 관심을 끄는 완성적인 실행을 기대하며 Swift에서 분배된 모든 actor의 특징들은 우리가 사용하는 actor가 전달하는 것이 무엇인지에 대해 인식하기 어렵다고 말한다.
actor에서 distributed actor로 넘어가기 위해서 distributed actor, 그리고 필요에 따라 distributed func을 써야한다.
카드 트레이팅 시스템을 추적할 수 있는 코드가 있다고 가정해보자.
// use Apple's ClusterSystem transport
typealias DefaultDistributedActorSystem = ClusterSystem
distributed actor CardCollector {
var deck: Set<String>
init(deck: Set<String>) {
self.deck = deck
}
distributed func send(card selected: String, to person: CardCollector) async -> Bool {
guard deck.contains(selected) else { return false }
do {
try await person.transfer(card: selected)
deck.remove(selected)
return true
} catch {
return false
}
}
distributed func transfer(card: String) {
deck.insert(card)
}
}
distributed actor의 thorwing하는 특성 때문에, person.transfer(card:)가 throw되지 않는다면 deck.remove(selected)를 하는 것이 안전하다고 확신할 수 있다.
Swift의 목표는 actor에 대한 당신의 지식이 distributed actor로 쉽게 이전되는 것이지만 우리가 알아야 할 중요한 차이점이 있다.
모든 distributed actor는 'try'와 'await'과 함께 호출되어야 한다. 함수가 throw됨을 명시하지 않더라고 네트워크 호출이 빗나갈 실패 가능성이 있기 때문이다.
distribtued func에 정의되어 있는 모든 파라미터나 리턴 타입은 Codable과 같은 serialization 프로세스를 거쳐야 한다. 이는 컴파일 타임 내에 확인되며, remote actor로와 데이터를 주고받을 수 있음을 swift가 보장할 수 있다.
데이터 요청을 최소화하기 위해 actor API를 조정해야 한다. 즉 네트워크 호출을 여러 번 왔다 갔다하는 것을 피하기 위해, username, firstName, lastName을 읽는 경우 따로 3번씩 같은 함수를 호출하는 것보다 하나의 함수 호출로 3개를 읽을 수 있도록 해야 한다.
복잡한 result builder를 실행시키기 위한 과부하가 단순해졌는데, swift가 정규 표현식을 개선시킬 수 있었던 이유 중 하나이다. 또한, 이론적으로 variadic(매개변수의 개수가 가변적일 수 있음) generic를 추가할 필요없이 SwiftUI의 View개수 제한(10개)를 없앴다.
@resultBuilder
struct SimpleViewBuilderOld {
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View {
TupleView((c0, c1))
}
static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0: View, C1: View, C2: View {
TupleView((c0, c1, c2))
}
}
여기 두 가지 버전의 buildBlock함수가 있다. 하나는 2개의 View 파라미터를 또 다른 하나는 3개의 View 파라미터를 받는 함수이다.
buildBlock<C0, C1, C2>() 를 사용하면 4번째 View를 추가할 수 없게 되는데, 이 때문에 새롭게 buildPartialBlock()가 등장한 것이다.
이는 Sequence의 reduce() 고차함수와 비슷하게 동작한다(reduce는 초기값이 정해지고, 기존값과 다음값을 더해 나가면서 업데이트한다.)
그래서 새로운 result builder에서는 이와 마찬가지로 하나의 View를 받아서 다음 View와 어떻게 결합할 수 있는지를 보여준다.
@resultBuilder
struct SimpleViewBuilderNew {
static func buildPartialBlock<Content>(first content: Content) -> Content where Content: View {
content
}
static func buildPartialBlock<C0, C1>(accumulated: C0, next: C1) -> TupleView<(C0, C1)> where C0: View, C1: View {
TupleView((accumulated, next))
}
}
따라서 이 코드에서도 1개 혹은 2개의 View를 받는 변수가 있다하더라도, 축적될 수 있기 때문에 우리가 원하는만큼 추가할 수 있게 된다.
@SimpleViewBuilderNew func createTextNew() -> some View {
Text("1")
Text("2")
Text("3")
}
하지만, 결과는 항상 동일하지 않다: 아까 예제 코드에서는 TupleView<Text, Text, Text> 이렇게 리턴되는 반면, 현재 예제 코드에서는 TupleView<(TupleView<(Text, Text)>, Text)> 이렇게 중첩된 형태로 리턴될 수 있다. 다행히도 SwiftUI팀은 이렇게 되도록 의도했다면 예전에 겪었던 과부하인 10개의 buildPartialBlock()를 생성했어야 할것이다.
TIP: buildPartialBlock()은 어떠한 platform-specific 런타임에 반하는 Swift의 일부이다.
다양한 상황에서 프로토콜을 사용하여 generic 함수를 호출하는 것이 가능해졌다.
Numeric이라는 타입을 사용하는 generic함수가 있다고 가정했을 때, double(5)를 직접적으로 호출한다면 swift는 성능 면에서 효율적으로 Int타입의 인자를 받아들이는 버전을 만드는 등 함수를 구체화할 수 있었다.
func double<T: Numeric>(_ number: T) -> T {
number * 2
}
그런데 이번 업데이트에서 우리가 사용하는 데이터가 프로토콜을 따른다는 것만 안다는 것이 전부여도 함수를 호출할 수 있게끔 허용한다.
let first = 1
let second = 2.0
let third: Float = 3
let numbers: [any Numeric] = [first, second, third]
for number in numbers {
print(double(number))
}
swift는 이런 것을 existential type이라고 부른다: 우리가 박스 안에 앉아서 실제로 사용하는 데이터의 타입. swift는 그 박스 '안'에 있는 데이터를 호출한다고 암묵적으로 이해하는 것이다.
(출처: https://www.hackingwithswift.com/articles/249/whats-new-in-swift-5-7)