스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Swift의 분산된 Actor 소개
분산 시스템의 개발을 간소화하는 Swift의 Actor 모델의 확장인 분산된 Actor를 살펴보겠습니다. 분산된 앱 및 시스템으로 작업할 때 분산된 Actor 격리 및 위치 투명성이 네트워킹, 직렬화 및 기타 전송의 우발적인 복잡성을 방지하는 데 어떻게 도움이 되는지 알아보겠습니다. 이 세션을 최대한 활용하려면 WWDC21의 ‘Protect mutable state with Swift actors(Swift Actor로 변이 가능한 상태 보호)'를 시청하시기 바랍니다.
리소스
- SE-0336: Distributed Actor Isolation
- SE-0344: Distributed Actor Runtime
- Swift Distributed Actors Cluster Library
- Swift Forums: Distributed Actors
- TicTacFish: Implementing a game using distributed actors
관련 비디오
WWDC22
WWDC21
WWDC19
-
다운로드
♪ 부드러운 힙합 음악 ♪ ♪ 안녕하세요! Konrad라고 합니다 Swift 팀의 엔지니어죠 ‘Swift 분산 행위자 만나기’에 오신 걸 환영합니다 이 세션에서는 Swift 동시성 기반 앱을 단일 프로세스 이상으로 활용하는 방법에 대해 알아봅니다 Swift 행위자는 동일한 프로세스에서 낮은 수준의 데이터 레이스로부터 사용자를 보호하도록 설계되었습니다 컴파일 시간 강제 실행 행위자 격리 검사를 통해 이를 수행합니다 분산 행위자는 동일한 개념 행위자 모델을 세분화하여 클러스터에 있는 여러 기기 또는 서버와 같은 여러 프로세스로 확장합니다 혹시 Swift 행위자를 아직 잘 모르신다면 WWDC 2021의 ’Swift 행위자로 가변 상태 보호’ 세션을 먼저 시청해 주세요 이번 세션에서 다룰 앱은 제가 최근에 개발 중인 tic-tac-toe 스타일 게임입니다 Tic Tac Fish죠! 여기서 재미있는 아이디어는 여러분이 플레이할 팀을 선택할 수 있다는 겁니다 이 팀은 여러분이 게임을 할 때 움직임을 표시하는 데 사용되는 이모티콘과 일치하죠 이후 여러분이 경기장에서 자신의 움직임을 표시할 때 플레이어 중 하나가 이길 때까지 여러분 팀의 이모티콘이 필드에 배치됩니다 지금은 봇 상대와 게임을 할 수 있는 오프라인 모드만 구현했지만 앱을 한 단계 업그레이드하기 위해 몇 가지 멀티플레이 모드를 도입하고 싶습니다 저는 이미 이 앱에서 행위자를 사용해 동시성을 관리하고 게임에 참여하는 플레이어를 모델링하고 있습니다 이런 플레이어 행위자를 다른 프로세스로 마이그레이션하는 데 무엇이 필요한지 또 분산 행위자가 이를 수행하는 데 어떤 도움을 주는지 살펴보죠 코드를 살펴보기 전에 한 걸음 물러서서 행위자들이 동시 및 분산 응용 프로그램을 구축하기에 적합한 이유를 시각화해 보겠습니다 WWDC 세션 내내 행위자에 대해 이야기할 때 '동시성의 바다'라는 용어를 사용하는 것을 들을 수 있죠 행위자에 대해 생각할 때 아주 적절한 정신적 모델이기 때문입니다 각 행위자는 동시성의 바다에 독립적으로 존재하는 섬이고 서로의 섬에 직접 액세스하는 대신 서로 메시지를 주고받습니다 Swift에서는 섬끼리 이런 메시지를 보내는 행위가 비동기 메서드 호출 및 async/await로 구현됩니다 이는 행위자 상태 격리와 결합하여 행위자 기반 프로그램이 일단 컴파일되면 컴파일러가 낮은 수준의 데이터 레이스에서 자유롭다는 사실을 보장하게 해줍니다 이제 동일한 개념 모델을 가져와 게임에 적용해 보겠습니다 분산 시스템으로 다시 구상되죠 각 기기, 클러스터의 노드 또는 운영 체제의 프로세스를 독립된 동시성의 바다로 여길 수 있습니다 더 작은 검은색 직사각형으로 여기에 표시돼 있죠 이 안에서는 정보를 꽤 쉽게 동기화할 수 있었습니다 이들은 여전히 동일한 메모리 공간을 공유하기 때문입니다 메시지 전달이라는 동일한 개념이 동시성 및 배포에 완벽하게 잘 작동하지만 이 모든 게 작동하도록 하려면 분산은 몇 가지 제약이 더 있습니다 여기서 분산 행위자가 등장합니다 분산 행위자를 사용하여 우리는 두 프로세스 사이에 채널을 구축하고 메시지를 보낼 수 있습니다 즉, Swift 행위자가 동시성 바다의 섬이라면 분산 행위자는 분산된 시스템의 광활한 바다에 있는 섬이라고 할 수 있죠 프로그래밍 모델에서 실제로 변한 것은 거의 없습니다 행위자는 여전히 상태를 격리하고 비동기식 메시지를 사용해야만 소통할 수 있습니다 같은 프로세스에 더 많은 분산 행위자를 참여시킬 수도 있습니다 모든 Intent와 목적에 있어 이들은 로컬 행위자만큼 유용하지만 필요할 때마다 원격 상호 작용에 참여할 준비가 되어 있다는 차이점이 있습니다 이렇게 분산 행위자와 상호 작용하는 방법을 바꾸지 않고도 잠재적으로 멀리 있을 수 있는 능력을 '위치 투명성'이라고 합니다 이는 분산 행위자가 어디에 있든 간에 동일한 방식으로 행위자와 상호 작용이 가능함을 의미합니다 이는 로컬 행위자에서 동일한 논리를 실행할 때 테스트에 있어 환상적일 뿐만 아니라 우리가 행위자를 그들이 있어야 할 곳으로 투명하게 이동시킬 수 있게 합니다 그들의 구현을 변경할 필요 없이요 좋아요, 이제 코드를 살펴보고 첫 번째 행위자를 분산 행위자로 전환할 준비가 된 것 같습니다 먼저 일반적인 게임 UI와 플레이어 행위자와 UI의 상호 작용 방식을 간단히 살펴보겠습니다 뷰는 꽤 표준적인 SwiftUI 코드입니다 게임 분야를 대표하는 몇 가지 텍스트와 버튼 요소가 있습니다 사용자가 GameCell을 클릭하면 플레이어 행위자에게 동작을 생성하고 UI를 구동하는 뷰 모델을 업데이트하도록 요청합니다 Swift 동시성 덕분에 이런 모든 업데이트는 스레드 안전하며 제대로 작동하죠 현재 사용자 입력을 대표하는 행위자가 오프라인 플레이어로 구현되어 있습니다 다음을 살펴보겠습니다 이 행위자는 게임 동작을 생성할 수 있는 일부 상태를 캡슐화합니다 구체적으로는 이미 얼마나 많이 움직였는지와 어떤 팀에서 뛰는지 추적할 필요가 있습니다 각 팀에는 각 움직임에 대해 선택할 수 있는 이모티콘이 여러 개 있으므로 움직임 번호를 사용하여 이모티콘 문자 ID를 선택합니다 또한 일단 움직임이 생성되면 모델을 업데이트해야 합니다 이 모델은 MainActor 격리 클래스이므로 이 모델의 변형은 스레드 안전합니다 하지만 사용자 MadeMove를 호출할 때는 'await'를 사용해야 합니다 마지막으로 오프라인 플레이어도 적이 움직일 때마다 호출되는 메서드를 선언합니다 여기서 해야 할 일은 뷰 모델의 업데이트입니다 그러면 게임 필드가 다시 활성화되어 인간 플레이어가 자신의 움직임을 선택할 수 있고 게임이 끝날 때까지 주기가 계속됩니다 우리의 봇 플레이어도 행위자를 사용하여 표현됩니다 오프라인 플레이어에 비해 구현이 상당히 간단합니다 뷰 모델 업데이트에 대해 걱정할 필요가 없기 때문에 그냥 GameState를 추적하고 게임 움직임만 생성하죠 봇 플레이어가 좀 더 간단하기 때문에 분산 행위자로 변환을 시작하기에 더 좋을 것 같습니다 좋아요, 이제 코드를 살펴보고 첫 번째 행위자를 분산 행위자로 전환할 준비가 된 것 같습니다 분산된 Tic Tac Fish 게임의 첫 번째 단계는 BotPlayer 유형을 분산 행위자로 변환하면서 동시에 여전히 로컬로만 사용하는 것입니다 Xcode를 열어서 어떻게 해야 할지 알아보죠 분산 행위자를 선언하려면 Swift 5.7에서 도입한 새 분산 모듈을 가져와야 합니다 이 모듈에는 분산 행위자를 선언하고 사용하는 데 필요한 모든 유형이 포함되어 있죠 이렇게 BotPlayer 행위자 선언 앞에 분산 키워드를 추가할 수 있습니다 이렇게 하면 행위자가 DistributedActor 프로토콜을 자동으로 준수하게 되며 여러 가지 추가 컴파일 시간 검사를 활성화하죠 이제 컴파일러가 어떤 오류를 수정하라고 할지 알아보겠습니다 컴파일러는 BotPlayer가 사용할 수 있는 ActorSystem을 선언하지 않는다고 우리에게 알려 줍니다 분산 행위자는 항상 원격 호출을 수행하는 데 필요한 모든 직렬화 및 네트워킹을 처리하는 일부 분산 행위자 시스템에 속하므로 이 행위자를 어떤 유형의 행위자 시스템과 함께 사용할 것인지 선언해야 합니다 현재로서는 원격 호스트에서 실제로 실행하지 않고도 봇 플레이어가 모든 분산 격리 검사를 통과하도록 하는 것이 유일한 목표이므로 분산 모듈과 함께 제공되는 LocalTesting DistributedActor 시스템을 사용할 수 있습니다 모듈 전체 DefaultDistributedActorSystem typealias를 선언하거나 특정 행위자의 본문에 있는 ActorSystem typealias를 선언하여 사용할 행위자 시스템에 대해 컴파일러에 말할 수 있습니다 후자가 더 구체적이니까 이 방법을 선택할게요
다음 오류는 이전에 제가 수동으로 구현한 'id' 속성과 관련 있습니다 두 플레이어 행위자가 따라야 하는 식별 가능 프로토콜을 준수하도록 마련한 속성이죠 이 오류는 ID 속성이 분산 행위자 합성 속성과 충돌하기 때문에 명시적으로 정의할 수 없음을 나타냅니다 ID는 분산 행위자의 중요한 부분입니다 이들은 행위자가 속한 전체 분산 행위자 시스템에서 해당 행위자를 고유하게 식별하는 데 사용됩니다 이들은 행위자가 초기화될 때 분산 행위자 시스템에 의해 할당되고 나중에 해당 시스템에서 관리됩니다 따라서 우리는 ID 속성을 수동으로 선언하거나 할당할 수 없습니다 행위자 시스템이 우리 대신 이를 수행합니다 즉, 간단히 이 작업을 행위자 시스템에 맡기고 수동으로 선언한 ID 속성을 제거할 수 있습니다 여기서 마지막으로 처리해야 할 오류는 분산 행위자의 이니셜라이저입니다 컴파일러는 actorSystem 속성이 사용 전에 초기화되지 않았다고 말합니다 이는 모든 분산 행위자의 일부인 또 다른 컴파일러 합성 속성입니다 우리가 사용하고자 하는 행위자 시스템의 유형을 선언해야 할 뿐만 아니라 일부 구체적인 행위자 시스템으로 합성된 actorSystem 속성을 초기화해야 합니다 일반적으로 여기서 올바른 작업은 이니셜라이저에서 행위자 시스템을 수락하고 해당 속성으로 전달하는 것입니다 이렇게 하면 테스트에서 다른 행위자 시스템 구현을 통과하여 유닛 테스트를 쉽게 수행할 수 있습니다 또한 새 봇 플레이어를 만들 때마다 인스턴스를 통과해야 하므로 지금 바로 실행해 보죠
좋습니다! 선언 측 오류를 모두 처리했습니다 하지만 아직 해결해야 할 호출 측 오류가 몇 가지 있습니다 잠재적인 원격 분산 행위자에 대해서는 분산 메서드만 호출할 수 있는 것 같습니다 이는 시스템에서 일부 행위자만 분산 행위자로 주석을 다는 것과 비슷하죠 분산 행위자의 모든 메서드가 반드시 원격으로 호출되도록 설계된 것은 아닙니다 호출자는 작은 도우미 기능 또는 호출자가 이미 인증되었다고 가정하는 기능을 가질 수 있습니다 그래서 Swift는 여러분에게 분산 API 표면에 대해 명시하라고 요청합니다 원격 호출자에게 노출해야 하니까요 고맙게도 이 문제는 분산 키워드를 그 기능들에 추가만 하면 쉽게 해결됩니다 makeMove메서드와 opponentMoved 메서드는 모두 원격으로 호출됩니다 분산형 키워드를 둘 다에 추가할게요
좋습니다! 이제 마지막으로 처리해야 할 일이 딱 하나 남았습니다 분산 메서드 호출은 네트워크 경계를 넘을 수 있으므로 모든 매개 변수와 반환 값이 행위자 시스템의 직렬화 요구 사항을 준수하는지 확인해야 합니다 우리의 경우, 행위자 시스템은 Swift의 기본 직렬화 메커니즘인 Codable을 사용하고 있습니다 컴파일러는 구체적으로 다음과 같이 말합니다 ‘결과 유형 GameMove가 직렬화 요구 사항 ‘Codable을 준수하지 않습니다’ GameMove 유형을 간단히 살펴보겠습니다 다행히도 필요한 적합성만 추가하면 Codable이 쉽게 가능한 깔끔하고 작은 데이터 유형으로 보입니다 Swift 컴파일러가 제게 필요한 Codable 구현을 합성할 겁니다 여기까지 하면 끝입니다! 제 게임이 예상대로 실행되는지 확인하면 되죠
좋아요, 물고기 팀에 1점! 봇 플레이어가 여전히 동일한 로컬 장치에서 실행 중이지만 우리는 이미 흥미로운 다음 단계를 위한 길을 닦았습니다 이 단계에서는 봇 플레이어가 새롭게 획득한 위치 투명성 기능의 혜택을 실제로 누릴 수 있습니다 여기서 사용할 수 있는 WebSocket 기반 샘플 행위자 시스템을 이미 준비해 뒀습니다 이 행위자 시스템을 활용하면 봇 플레이어를 서버 측 Swift 응용 프로그램으로 이동하여 모바일 게임에서 원격 참조를 분석할 수 있습니다 행위자의 경우 선언된 행위자 시스템만 LocalTesting DistributedActor 시스템에서 샘플 앱을 위해 준비했던 SampleWebSocketActorSystem으로 바꾸면 됩니다 나머지 행위자 코드는 그대로 유지됩니다 이제 원격 봇 플레이어 참조를 로컬에서 생성하는 대신 원격 봇 플레이어 참조를 분석하겠습니다 분산 행위자의 경우 '로컬'과 '원격'이라는 용어는 관점의 문제임을 명심할 필요가 있습니다 모든 원격 참조마다 분산 행위자 시스템의 다른 노드에 이에 상응하는 로컬 인스턴스가 존재합니다 분산 행위자의 로컬 인스턴스 생성은 다른 Swift 객체와 거의 동일한 방식으로 수행됩니다 이니셜라이저를 호출하죠 그러나 분산 행위자에 대한 원격 참조를 얻을 때는 약간 다른 패턴을 따릅니다 행위자를 만드는 대신 구체적인 행위자 시스템을 사용하여 행위자 ID를 분석하려고 시도하죠 정적 분석 방법을 사용하면 행위자 시스템에 해당 ID를 가진 행위자에 대한 기존 행위자 인스턴스를 제공하거나 해당 행위자에 의해 식별된 행위자에 대한 원격 참조를 반환하도록 요청할 수 있습니다 행위자 시스템은 식별자를 분석할 때 실제 원격 조회를 수행하지 않아야 합니다 분석 메서드는 비동기적이지 않으므로 빠르게 반환되어야 하며 네트워킹 또는 기타 차단 작업을 수행하지 않아야 합니다 ID가 유효해 보이고 유효한 원격 위치를 가리키는 것처럼 보이는 경우 시스템은 해당 행위자가 존재한다고 가정하고 원격 참조를 반환합니다 ID를 분석할 때 원격 시스템의 실제 인스턴스가 아직 존재하지 않을 수도 있다는 사실을 명심하세요! 예를 들어 여기서는 우리와 게임을 하는 데만 전념할 상대 봇 플레이어의 무작위 식별자를 만듭니다 이 봇은 아직 존재하지 않지만 이 ID에 지정된 첫 번째 메시지가 수신되면 서버 측 시스템에 생성될 것입니다 이제 서버 측 Swift 응용 프로그램으로 이동하죠 제가 준비한 샘플 WebSocket 행위자 시스템 덕분에 매우 쉽게 구현할 수 있습니다 첫째, WebSocket 행위자 시스템을 서버 모드로 생성하여 포트에 연결하는 대신 시스템을 바인딩하고 포트를 수신합니다 이제 시스템이 종료될 때까지 앱을 대기시킵니다 이어서 아직 어떤 행위자 인스턴스도 할당되지 않은 ID로 주소가 지정된 메시지를 수신할 때 어떻게든 온디맨드로 행위자를 생성하는 패턴을 처리해야 합니다 일반적으로 행위자 시스템은 수신 메시지를 수신하고 로컬 분산 행위자 인스턴스를 찾기 위해 수신자 ID를 분석하려고 시도합니다 그런 다음 배치된 행위자에서 원격 호출을 실행합니다 하지만 우리가 방금 논의했듯이 우리의 봇 플레이어 ID는 사실상 꾸며냈기 때문에 시스템은 그들에 대해 알 수 없을 뿐만 아니라 스스로 올바른 유형의 행위자를 만들 가능성도 낮습니다 고맙게도 우리의 샘플 행위자 시스템 구현은 우리를 위해 올바른 패턴을 준비해 뒀습니다 온디맨드 행위자 생성이죠 참고로 이것은 단지 패턴일 뿐이며 기본으로 제공되거나 분산 행위자 모듈이 제공되는 것은 아닙니다 그러나 행위자 시스템 구현이 얼마나 유연하고 강력할 수 있는지 보여주는 좋은 예입니다 시스템은 다양한 패턴을 제공하고 복잡한 작업을 쉽게 처리할 수 있게 해줍니다 이 패턴을 사용하여 행위자 시스템은 모든 수신 ID에 대한 로컬 행위자 분석을 시도합니다 그러나 기존 행위자를 찾는 데 실패하면 resolveCreateOnDemand를 시도합니다 ID를 구성하는 클라이언트 코드와 서버 코드의 일부를 모두 관리하고 있기 때문에 온디맨드로 필요한 행위자를 만들어 행위자 시스템을 지원할 수 있습니다 클라이언트에서 구성한 봇 식별자는 ActorIdentity에 태그를 추가하거나 일부 인식 가능한 이름을 사용하는 등 인식 가능한 이름 지정 체계를 사용하므로 이러한 ID를 탐지하여 아직 활성화되지 않은 모든 메시지에 대해 새 봇 상대를 만들 수 있습니다 이후 원격 호출이 기존 인스턴스를 분석하므로 지정된 첫 번째 메시지에 대해서만 새 봇 플레이어를 만듭니다 이게 전부입니다! 서버 구현이 완료되었으며 이제 원격 봇 플레이어로 게임을 즐길 수 있습니다 Swift run을 사용하여 명령줄에서 서버를 실행하거나 Xcode를 사용하고 서버 스킴을 선택한 다음 평소처럼 실행을 클릭하여 서버를 실행할 수도 있습니다 첫 번째 움직임을 마쳤으므로 봇 플레이어에서 생성한 원격 플레이어 참조에서 makeMove를 호출하여 동일한 작업을 수행하도록 요청합니다 그러면 서버 측 시스템에서 분석이 트리거됩니다 이 ID에 대한 기존 봇을 찾을 수 없으므로 온디맨드로 봇을 만들려고 시도하여 성공합니다 봇은 makeMove 호출을 받은 다음 GameMove를 생성하여 응답합니다 이미 꽤 훌륭하죠! 우리 봇 플레이어를 분산 행위자로 전환하기 위해 사전 작업을 해야 했지만 원격 시스템으로 실제로 옮기는 작업은 매우 간단했습니다 네트워킹이나 직렬화 구현 세부 사항을 다룰 필요도 전혀 없었습니다! 모든 어려운 일은 분산 행위자 시스템이 우리 대신 해결했죠 아직 사용할 수 있는 기능이 완벽하게 강화된 구현은 많지 않지만 이런 배포 용이성은 우리가 이 기능을 통해 달성하려는 목표입니다 다음으로 우리 게임에 진정한 멀티플레이어 환경을 구축하는 방법에 대해 알아보겠습니다 이전 예제에서는 클라이언트/서버 시나리오의 분산 행위자를 사용했는데 이는 기존에 작업한 다른 앱을 통해서 이미 익숙할 수도 있습니다 하지만 분산 행위자는 피어 투 피어 시스템에도 사용될 수 있습니다 전용 서버 구성 요소가 전혀 없는 경우에 해당하죠 이건 제가 게임에 대해 가졌던 다른 아이디어와 일치합니다 가끔 여행하다 보면 인터넷이 잘 안되는 곳에 도착하게 될 때가 있는데 지역 와이파이는 잘 터지죠 이런 상황에서도 같은 네트워크에 연결된 친구들에게 도전하고 함께 게임을 즐기고 싶습니다 그래서 저는 또 다른 행위자 시스템을 구현했는데 이번에는 네트워크 프레임워크에서 제공하는 로컬 네트워킹 기능을 사용했죠 이 강연에서는 이 행위자 시스템의 구현을 자세히 살피지 않습니다 WWDC 2019의 ’네트워킹 발전, 2부’를 시청해 보세요 이런 사용자 정의 프로토콜을 구현하는 방법을 배울 수 있죠 한 가지 짚고 넘어가자면 로컬 네트워크에 접속하면 매우 민감한 개인 정보가 노출될 수 있으므로 주의하여 사용하시기 바랍니다 이번에는 이미 다른 기기에 분산되어 있는 행위자를 다루게 되므로 이제 더 이상 이전 예제에서처럼 ID만 구성할 수는 없습니다 같이 게임을 하고자 하는 다른 기기에서 특정 행위자를 찾아야 합니다 이 문제는 분산 행위자에게만 해당하는 문제는 아니며 일반적으로 서비스 검색 메커니즘을 사용하여 해결됩니다 그러나 분산 행위자의 도메인에는 코드 전체에 걸쳐 강력한 API 유형을 고수할 수 있게 해주는 API 행위자 시스템의 공통적인 패턴과 스타일이 있습니다 이를 리셉션 패턴이라고 부릅니다 호텔과 비슷하게 행위자도 알려지고 남들과 만나려면 체크인을 해야 하기 때문이죠 모든 행위자 시스템은 자체 리셉셔니스트가 있으며 행위자 검색을 구현하기 위해 기본 운송 메커니즘에 가장 적합한 수단을 모두 사용할 수 있습니다 때로는 기존 서비스 검색 API에 의존하여 유형 안전 API를 그 위에 계층화하거나 가십 기반 메커니즘 또는 완전히 다른 무언가를 구현할 수도 있습니다 그러나 이것은 행위자 시스템 사용자의 관점에서 보는 구현 세부 사항입니다 우리가 신경 써야 할 단 하나는 우리 행위자가 검색 가능한지 확인하고 그들을 검색해야 할 때 특정 태그나 유형으로 행위자를 찾는 겁니다 SampleLocalNetworkActorSystem을 위해 제가 구현한 간단한 리셉셔니스트를 살펴보겠습니다 분산 행위자 시스템의 모든 리셉셔니스트가 행위자를 발견할 수 있도록 행위자의 체크인을 지원하는 기능이죠 이어서 특정 유형의 모든 행위자 목록을 얻고 그들이 해당 시스템에서 사용 가능해지면 태그를 지정할 수 있습니다 이 리셉셔니스트를 이용해 게임을 함께 하고 싶은 상대 행위자를 찾아봅시다 이전에 GameView는 뷰 이니셜라이저에서 직접 상대를 생성하거나 분석했습니다 네트워크에 상대가 나타날 때까지 비동기적으로 기다려야 하므로 더 이상 이 작업을 수행할 수 없습니다 이를 위해 우리가 상대를 찾는 동안 '상대를 찾는 중...’ 메시지가 표시되는 대전표 짜기 뷰를 도입해 보겠습니다 이 뷰가 나타나면 대전표 짜기를 시작합니다 대전표 짜기는 새로운 비구조화 작업으로 진행되며 여기서 우리는 로컬 행위자 시스템의 리셉셔니스트에게 상대 팀의 태그를 사용하여 태그된 모든 행위자의 목록을 요청합니다 우리가 물고기 팀에서 게임을 한다면 설치류 팀의 선수를 찾아야 하고 반대도 성립합니다 다음은 비동기 루프를 사용하여 들어오는 상대 행위자를 기다립니다 시스템이 플레이할 수 있는 상대가 있는 근처 기기를 발견하면 이 작업 루프가 다시 시작되죠 상대가 항상 게임을 할 준비가 되어 있다고 가정하고 즉시 모델을 저장하여 그들과 게임을 시작해 보죠 우리는 도우미 기능을 사용하여 누가 먼저 움직일지 결정하고 마지막으로 상대방에게 게임을 함께 시작하고 싶다고 말합니다 비동기 루프에서 벗어나려면 반드시 여기로 돌아와야 하며 중매 작업을 완료하려면 한 명의 상대만 있으면 됩니다 이 게임 플레이 모드를 사용하려면 OfflinePlayer 구현을 약간 변경해야 합니다 LocalNetworkPlayer로 부르도록 하죠 SampleLocalNetworkActorSystem를 사용할 겁니다 여기서 가장 흥미로운 점은 인간 플레이어를 대표하는 행위자의 makeMove 메서드가 이제 원격으로 호출된다는 거죠! 하지만 움직이는 것은 사실 인간 플레이어의 책임입니다 이 과제를 해결하기 위해 우리는 humanSelectedField 비동기 함수를 우리의 뷰 모델에 도입합니다 이 값은 인간 사용자가 필드 중 하나를 클릭할 때 트리거되는 @Published 값으로 제공됩니다 인간 플레이어가 필드를 클릭하면 makeMove 함수가 재개되고 우리는 실행된 GameMove를 원격 호출자에게 반환하여 원격 호출을 완료합니다 다시 말하지만 이게 전부입니다! 진정한 멀티플레이어 게임 모드를 다루기 위해 행위자의 구현을 약간 변경해야 했지만 시스템의 전체적인 디자인에서 실제로 변한 것은 전혀 없었습니다 또 가장 중요한 것은 게임 로직의 어떤 변경 사항도 우리가 로컬 네트워킹을 사용하게 되리라는 사실과 관련이 없다는 겁니다 우리는 플레이어 행위자에게 분산된 메서드를 호출하여 상대를 발견하고 그들과 게임을 합니다 이 게임 모드를 시연하려면 함께 플레이할 상대가 필요하니까 털이 보송보송한 제 조수 카피바라 캐플린에게 요청하죠 게임을 꽤 잘한대요!
좋아요, 꽤 똑똑하네요
상당히 잘해요 여기서 시도해 보죠 이런, 졌어요! 동물 친구야 이번엔 네가 이겼지만 다음 세션에서 또 플레이하자 도와줘서 고마워, 캐플린! 마지막으로 우리가 서로 다른 행위자 시스템을 결합하여 무엇을 달성할 수 있는지 알려드리겠습니다 예를 들어 WebSocket 시스템을 사용하여 기기 호스팅된 행위자 플레이어 행위자를 서버 측 로비 시스템에 등록하면 시스템은 그들을 쌍으로 연결하고 그들 사이의 분산 호출에 대한 프록시 역할을 합니다 기기 호스팅된 플레이어 행위자가 자신을 등록할 수 있는 GameLobby 행위자를 구현할 수도 있습니다 기기가 온라인 플레이 모드로 전환되면 리셉셔니스트를 사용하여 GameLobby를 발견하고 가입을 호출합니다 GameLobby는 이용 가능한 플레이어를 추적하고 플레이어 한 쌍이 식별되었을 때 게임 세션을 시작합니다 게임 세션은 게임의 동인 역할을 하며 움직임을 조사하고 서버에 저장된 게임 표현에 이를 표시합니다 게임이 완료되면 우리는 결과를 모아서 로비에 보고하면 됩니다 하지만 더 흥미로운 것은 이 디자인을 수평으로 확장할 수 있다는 거죠 물론 더 많은 게임 세션 행위자를 만들어 더 많은 게임을 하나의 서버에서 동시에 제공할 수 있습니다 하지만 분산 행위자 덕분에 심지어 다른 노드에서 게임 세션을 만들어 클러스터 전체에서 동시 게임 수의 로드 밸런싱을 할 수 있습니다 우리가 클러스터 행위자 시스템을 가지고 있다면요 사실 우리는 갖고 있죠! 이런 시나리오에서 사용할 수 있도록 기능이 풍부한 클러스터 행위자 시스템 라이브러리를 오픈 소스로 제공했습니다 이는 SwiftNIO를 사용하여 구현되고 서버 측 데이터 센터 클러스터링에 특화되어 있죠 장애 탐지를 위한 고급 기술을 적용하고 클러스터 전체 리셉셔니스트를 자체 구현하여 제공합니다 이 두 가지 모두 행위자 시스템의 고급 참조 구현이므로 한번 살펴보시기 바랍니다 강력한 서버 측 응용 프로그램이기도 합니다 이번 세션에서 배운 내용을 요약해 보겠습니다 먼저 분산 행위자에 대해 배웠고 추가적인 컴파일러 지원 행위자 격리 및 직렬화 검사를 제공하는 방법도 배웠습니다 그들이 위치 투명성을 가능하게 하는 방법과 그것을 이용해 행위자들이 호출자와 같은 프로세스에 위치할 필요가 없도록 만드는 방법도 배웠습니다 또한 분산 행위자를 사용하여 무엇을 구축할 수 있는지 영감을 얻기 위해 행위자 시스템 구현도 몇 가지 봤습니다 분산 행위자는 오로지 그들이 사용하는 행위자 시스템만큼만 강력합니다 참고로 여기 우리가 이 세션에서 봤던 행위자 시스템의 목록이 있습니다 기본적으로 Swift와 함께 제공되는 로컬 테스트 시스템과 두 가지 샘플 행위자 시스템으로 클라이언트/서버 스타일의 WebSocket 기반 시스템과 로컬 네트워킹 기반 시스템입니다 이런 시스템은 다소 불완전하며 분산 행위자를 사용하여 구축할 수 있는 것에 대한 영감으로 더 큰 역할을 합니다 이 세션과 연결된 샘플 코드 앱에서 해당 정보를 볼 수 있습니다 마지막으로 오픈 소스는 서버 측 클러스터링 구현 기능을 완벽하게 갖추고 있습니다 현재 베타 패키지로 제공되며 Swift 5.7과 함께 점차 완성될 예정입니다 분산 행위자에 대한 자세한 내용은 다음 리소스를 참조하세요 이 세션과 관련된 샘플 코드에는 Tic Tac Fish 게임의 모든 단계가 포함되어 있으므로 코드를 직접 자세히 살펴볼 수 있습니다 분산 행위자 언어 기능과 관련된 Swift 진화 제안은 작동 메커니즘을 매우 상세하게 설명합니다 또한 Swift 포럼에서 행위자 시스템 개발자와 사용자 모두를 위한 분산 행위자 카테고리를 찾을 수 있습니다 들어주셔서 감사합니다 여러분 앱에서 분산 행위자가 어떻게 사용될지 어서 보고 싶습니다! ♪
-
-
4:49 - actor OfflinePlayer
public actor OfflinePlayer: Identifiable { nonisolated public let id: ActorIdentity = .random let team: CharacterTeam let model: GameViewModel var movesMade: Int = 0 public init(team: CharacterTeam, model: GameViewModel) { self.team = team self.model = model } public func makeMove(at position: Int) async throws -> GameMove { let move = GameMove( playerID: id, position: position, team: team, teamCharacterID: team.characterID(for: movesMade)) await model.userMadeMove(move: move) movesMade += 1 return move } public func opponentMoved(_ move: GameMove) async throws { do { try await model.markOpponentMove(move) } catch { log("player", "Opponent made illegal move! \(move)") } } }
-
5:39 - actor BotPlayer
public actor BotPlayer: Identifiable { nonisolated public let id: ActorIdentity = .random var ai: RandomPlayerBotAI var gameState: GameState public init(team: CharacterTeam) { self.gameState = .init() self.ai = RandomPlayerBotAI(playerID: self.id, team: team) } public func makeMove() throws -> GameMove { return try ai.decideNextMove(given: &gameState) } public func opponentMoved(_ move: GameMove) async throws { try gameState.mark(move) } }
-
6:11 - distributed actor BotPlayer
import Distributed public distributed actor BotPlayer: Identifiable { typealias ActorSystem = LocalTestingDistributedActorSystem var ai: RandomPlayerBotAI var gameState: GameState public init(team: CharacterTeam, actorSystem: ActorSystem) { self.actorSystem = actorSystem // first, initialize the implicitly synthesized actor system property self.gameState = .init() self.ai = RandomPlayerBotAI(playerID: self.id, team: team) // use the synthesized `id` property } public distributed func makeMove() throws -> GameMove { return try ai.decideNextMove(given: &gameState) } public distributed func opponentMoved(_ move: GameMove) async throws { try gameState.mark(move) } }
-
12:08 - Resolving a remote BotPlayer
let sampleSystem: SampleWebSocketActorSystem let opponentID: BotPlayer.ID = .randomID(opponentFor: self.id) let bot = try BotPlayer.resolve(id: opponentID, using: sampleSystem) // resolve potentially remote bot player
-
13:35 - Server-side actor system app
import Distributed import TicTacFishShared /// Stand alone server-side swift application, running our SampleWebSocketActorSystem in server mode. @main struct Boot { static func main() { let system = try! SampleWebSocketActorSystem(mode: .serverOnly(host: "localhost", port: 8888)) system.registerOnDemandResolveHandler { id in // We create new BotPlayers "ad-hoc" as they are requested for. // Subsequent resolves are able to resolve the same instance. if system.isBotID(id) { return system.makeActorWithID(id) { OnlineBotPlayer(team: .rodents, actorSystem: system) } } return nil // unable to create-on-demand for given id } print("========================================================") print("=== TicTacFish Server Running on: ws://\(system.host):\(system.port) ==") print("========================================================") try await server.terminated // waits effectively forever (until we shut down the system) } }
-
20:02 - Receptionist listing
/// As we are playing for our `model.team` team, we try to find a player of the opposing team let opponentTeam = model.team == .fish ? CharacterTeam.rodents : CharacterTeam.fish /// The local network actor system provides a receptionist implementation that provides us an async sequence /// of discovered actors (past and new) let listing = await localNetworkSystem.receptionist.listing(of: OpponentPlayer.self, tag: opponentTeam.tag) for try await opponent in listing where opponent.id != self.player.id { log("matchmaking", "Found opponent: \(opponent)") model.foundOpponent(opponent, myself: self.player, informOpponent: true) // inside foundOpponent: // if informOpponent { // Task { // try await opponent.startGameWith(opponent: myself, startTurn: false) // } // } return // make sure to return here, we only need to discover a single opponent }
-
20:23 - distributed actor LocalNetworkPlayer
public distributed actor LocalNetworkPlayer: GamePlayer { public typealias ActorSystem = SampleLocalNetworkActorSystem let team: CharacterTeam let model: GameViewModel var movesMade: Int = 0 public init(team: CharacterTeam, model: GameViewModel, actorSystem: ActorSystem) { self.team = team self.model = model self.actorSystem = actorSystem } public distributed func makeMove() async -> GameMove { let field = await model.humanSelectedField() movesMade += 1 let move = GameMove( playerID: self.id, position: field, team: team, teamCharacterID: movesMade % 2) return move } public distributed func makeMove(at position: Int) async -> GameMove { let move = GameMove( playerID: id, position: position, team: team, teamCharacterID: movesMade % 2) log("player", "Player makes move: \(move)") _ = await model.userMadeMove(move: move) movesMade += 1 return move } public distributed func opponentMoved(_ move: GameMove) async throws { do { try await model.markOpponentMove(move) } catch { log("player", "Opponent made illegal move! \(move)") } } public distributed func startGameWith(opponent: OpponentPlayer, startTurn: Bool) async { log("local-network-player", "Start game with \(opponent.id), startTurn:\(startTurn)") await model.foundOpponent(opponent, myself: self, informOpponent: false) await model.waitForOpponentMove(shouldWaitForOpponentMove(myselfID: self.id, opponentID: opponent.id)) } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.