본문 바로가기
iOS/Swift

Swift의 분산된 Actor 소개

by 바등쪼 2023. 8. 22.

https://developer.apple.com/wwdc22/110356

 

Meet distributed actors in Swift - WWDC22 - Videos - Apple Developer

Discover distributed actors — an extension of Swift's actor model that simplifies development of distributed systems. We'll explore how...

developer.apple.com

 

  • Swift 동시성 기반 앱을 단일 프로세스 이상으로 활용하는 방법에 대해 알아보자!

 

Swift Actor는 동일한 프로세스에서 Row-level Data Race로부터 사용자를 보호하도록 설계되었다.

이것은 컴파일 타임에 actor isolation checks를 통해 수행된다.

 

Sea of Concurrency

Actor 관련 세션들을 보면 "동시성의 바다"라는 표현이 자주 사용된다.

Actor를 소개할 때 매우 적절한 비유이기 때문이다.

 

각 Actor는 동시성의 바다에 독립적으로 존재하는 섬이다. 서로의 섬에 직접 액세스하는 대신 서로 메시지를 주고받는다.

Swift에서는 섬끼리 이러한 메시지를 보내는 행위가 비동기 메서드 호출 및 async/await로 구현된다.

이는 Actor state isolation과 결합하여 Actor 기반 프로그램이 일단 컴파일되면 컴파일러가 낮은 레벨의 데이터 레이스에서 자유롭다는 사실을 보장하게 해준다.

 

이번 WWDC에서는 Tic Tac Toe 게임 앱을 예시로 들고 있습니다.

 

 

  • 각 기기, 클러스터의 노드 또는 운영 체제의 프로세스를 독립된 동시성의 바다로 여길 수 있다.
  • 위의 사진에서 남색 사각형 영역이 독립된 동시성의 바다를 의미한다.
  • 이 안에서는 정보를 꽤 쉽게 동기화할 수 있다. 이들은 동일한 메모리 공간을 공유하기 때문이다.
  • 메시지 전달이라는 동일한 개념이 동시성 및 distribution에 완벽하게 잘 작동하지만 이 모든 게 작동하도록 하려면 distribution은 몇 가지 제약이 더 있다.

  • 여기서 distributed actors가 등장한다.
  • 분산 액터를 사용하여 우리는 두 프로세스 사이에 채널을 구축하고 메시지를 보낼 수 있다. (위의 그림처럼)
  • 즉, Swift Actor가 동시성 바다의 섬이라면 분산 액터는 분산된 시스템의 광활한 바다에 있는 섬이라고 할 수 있다.
  • 프로그래밍 모델에서 실제로 바뀐 것은 거의 없다.
    • Actor는 여전히 상태를 격리하고 비동기식 메시지를 사용해야만 소통할 수 있다.
    • 같은 프로세스에 더 많은 분산 actor를 참여시킬 수도 있다.
  • 모든 Intent와 목적에 있어 이들은 로컬 Actor만큼 유용하지만 필요할 때마다 원격 상호 작용에 참여할 준비가 되어 있다는 차이점이 있다.

  • 이렇게 분산 액터와 상호 작용하는 방법을 바꾸지 않고도 잠재적으로 멀리 있을 수 있는 능력을 "Location transparency"라고 한다.
  • 이는 분산 액터가 어디에 있든 간에 동일한 방식으로 액터와 상호 작용이 가능함을 의미한다.
  • 로컬 액터에서 동일한 로직을 실행할 때 테스트에 있어 환상적이며 우리가 액터를 그들이 있어야 할 곳으로 투명하게 이동시킬 수 있게 한다. (구현을 변경할 필요 없이)

 

코드 예시

  • GameCell (뷰에 있는 네모 박스)를 클릭하면 플레이어 액터에게 움직임을 생성하고 UI를 구동하는 뷰 모델을 업데이트하도록 요청한다.
  • Swift Concurrency 덕분에 이런 모든 업데이트는 Thread Safe 하며 제대로 동작한다.
  • 현재는 사용자 입력을 대표하는 액터가 오프라인 플레이어로 구현되어 있다.

  • OfflinePlayer 액터를 살펴보자
  • 이 액터는 게임 동작을 생성할 수 있는 일부 상태를 캡슐화한다.
  • 구체적으로는 이미 얼마나 많이 움직였는지와 어떤 팀에서 뛰는지 추적할 필요가 있다.
  • 각 팀에는 각 움직임에 대해 선택할 수 있는 이모티콘이 여러 개 있으므로 움직임 번호를 사용하여 이모티콘 문자 ID를 선택한다.
  • 또한 일단 움직임(move)이 생성되면 모델을 업데이트해야 한다.
  • 이 모델은 Main actor isolated class 이므로 이 모델의 mutation은 스레드 안전하다.
  • 하지만 userMadeMove를 호출할 때는 await를 사용해야 한다.

  • opponentMoved는 적이 움직일 때마다 호출되는 메서드이다.
  • 뷰모델의 업데이트를 담당한다.
  • 그러면 게임 필드가 다시 활성화되어 인간 플레이어가 자신의 움직임을 선택할 수 있고 게임이 끝날 때까지 주기가 계속된다.

  • 우리의 봇 플레이어도 액터를 사용하여 표현된다.
  • 오프라인 플레이어에 비해 구현이 상당히 간단하다.
  • 뷰모델 업데이트에 대해 걱정할 필요가 없기 때문에 그냥 GameState를 추적하고 게임 움직임만 생성한다.
  • 봇 플레이어가 좀 더 간단하기 때문에 분산 액터로 변환을 시작하기에 더 좋을 것 같다. 

 

 

봇 플레이어를 Distributed Actor로 변환

봇 플레이어를 분산 액터로 변환하고 여전히 로컬에서만 사용해 보자

분산 액터로 변환한 BotPlayer

  1. Distributed 모듈을 import 한다.
  2. BotPlayer 액터 선언 앞에 distributed 키워드를 추가한다.
  3. 이렇게 하면 액터가 DistributedActor 프로토콜을 자동으로 준수하게 되며 여러 가지 추가 컴파일 타임 검사를 활성화한다.
  4. 여기까지 하면 ActorSystem을 선언하기 않았다고 컴파일러가 경고를 뱉는다.
  5. 분산 액터는 항상 원격 호출을 수행하는 데 필요한 모든 직렬화 및 네트워킹을 처리하는 일부 분산 액터 시스템에 속하므로 이 액터를 어떤 유형의 액터 시스템과 함께 사용할 것인지 선언해야 한다.
  6. 현재 예시에서는 원격 호스트에서 실제로 실행하지 않고도 봇 플레이어가 모든 분산 격리 검사를 통과하도록 하는 것이 유일한 목표이기 때문에 Distributed 모듈에서 제공하는 LocalTestingDistributedActorSystem을 사용할 수 있다.
  7. typealias를 사용하여 ActorSystem을 LocalTestingDistributedActorSystem로 지정한다.
  8. 여기까지 하면 기존에 nonisolated로 선언한 id에서 에러가 발생한다.
  9. ID 속성이 distributed actor synthesized 속성과 충돌하기 때문에 명시적으로 정의할 수 없다는 에러이다.
  10. ID는 분산 액터의 중요한 부분으로 액터가 속한 전체 액터 시스템에서 해당 액터를 고유하게 식별하는 데 사용된다.
  11. 이들은 액터가 초기화될 때 분산 액터 시스템에 의해 할당되고 해당 시스템에서 관리된다.
  12. 따라서 우리는 수동을 ID를 선언하거나 할당할 수 없다.
  13. 그래서 선언되어 있던 ID 속성을 제거한다.
  14. 여기까지 하면 self.actorSystem이 초기화되지 않았다는 에러가 발생한다.
  15. 이는 모든 분산 액터가 가지고 있는 또 다른 compiler synthesized 속성이다.
  16. 이 에러 해결을 위해 생성자의 파라미터로 actorSystem을 받아서 self.actorSystem에 주입해 준다.
  17. 여기까지 하면 에러는 모두 사라지지만 botPlayer를 호출하는 쪽의 코드에서 에러가 발생한다.
  18. 잠재적인 원격 분산 액터에 대해서는 분산 메서드만 호출할 수 있기 때문이다.
  19. 분산 액터의 모든 메서드가 반드시 원격으로 호출되도록 설계된 것은 아니다.
  20. 이 문제는 distributed 키워드를 원격으로(외부에서) 호출하는 메서드인 makeMove와 opponentMove에 추가하면 해결된다.
  21. 여기까지만 하면 GameMove가 Codable을 채택하지 않아서 에러가 발생한다.
  22. 분산 메서드 호출은 네트워크 경계를 넘을 수 있으므로 모든 매개 변수와 반환 값이 액터 시스템의 직렬화 요구 사항을 준수해야 한다.
  23. GameMove 구조체에 Codable을 채택하면 된다.

Codable을 채택시킨 모습

 

 

 

봇 플레이어를 Server-side 봇 플레이어로 변환하기

Location transparency로 인해 비교적 쉽게 구현 가능하다.

이 예제에서는 이미 준비된 WebSocket 기반 샘플 액터 시스템을 활용한다.

  • BotPlayer 구현체에서는 ActorSystem의 타입만 SampleWebSocketSystem으로 바꾸면 된다.

분산 액터의 경우 'local'과 'remote'라는 용어는 관점의 문제임을 명심해야 한다.

모든 원격 참조마다 분산 액터 시스템의 다른 노드에 이에 상응하는 로컬 인스턴스가 존재한다.

 

분산 액터의 로컬 인스턴스 생성은 다른 Swift 객체와 거의 동일한 방식으로 수행된다. ➡️ 이니셜라이저를 호출

 

그러나 분산 액터에 대한 원격 참조를 얻을 때는 약간 다른 패턴을 따른다.

액터를 만드는 대신 구체적인 액터 시스템을 사용하여 액터 ID를 분석하려고 시도한다.

정적 분석 방법을 사용하면 액터 시스템에 해당 ID를 가진 액터에 대한 기존 액터 인스턴스를 제공하거나 해당 액터에 의해 식별된 액터에 대한 원격 참조를 반환하도록 요청할 수 있다.

 

액터 시스템은 식별자를 분석할 때 실제 원격 조회를 수행하지 않아야 한다. 분석 메서드는 비동기적이지 않으므로 빠르게 반환되어야 하며 네트워킹 또는 기타 블로킹 작업을 수행하지 않아야 한다.

 

ID가 유효해 보이고 유효한 원격 위치를 가리키는 것처럼 보이는 경우 시스템은 해당 액터가 존재한다고 가정하고 원격 참조를 반환한다.

ID를 분석할 때 원격 시스템의 실제 인스턴스가 아직 존재하지 않을 수도 있다는 사실을 명심하자!!

위의 예시에서 무작위 식별자를 만들고 있는데 이 봇은 아직 존재하지 않지만 이 ID에 지정된 첫 번째 메시지가 수신되면 서버 측 시스템에 생성될 것이다.

 

서버 쪽 구현

  • WebSocketActorSystem을 서버 모드로 생성
  • 포트에 연결하는 대신 시스템을 바인딩하고 포트를 수신
  • 시스템이 종료될 때까지 앱을 대기시킨다.
  • 이어서 아직 어떤 액터 인스턴스도 할당되지 않은 ID로 주소가 지정된 메시지를 수신할 때 어떻게든 온디멘드로 행위자를 생성하는 패턴을 처리해야 한다.

  • 일반적으로 액터 시스템은 수신 메시지를 수신하고 로컬 분산 액터 인스턴스를 찾기 위해 수신자 ID를 분석하려고 시도한다.
  • 하지만 이전에 언급한 것처럼 우리의 봇 플레이어 ID는 사실상 꾸며냈기 때문에 시스템은 이것에 대해 알 수 없다.
  • 따라서 SampleActorSystemem의 구현체에 이를 위한 적절한 패턴을 준비해 뒀다고 가정한다.
  • 온디맨드 액터 생성이다.
  • 이 패턴을 사용하여 액터 시스템은 모든 수신 ID에 대해 로컬 액터 분석을 시도하고 이것에 실패하면 resolveCreateOnDemand를 시도한다.

아래는 실제로 로컬 서버를 연결하여 실행해  보는 장면이다.

 

 


Play with friends (멀티 플레이어 환경 구축)

Peer-to-peer: local networking

멀티 플레이어 모드의 2가지 형태

이 세션에서는 local networking을 하는 것 자체를 구현하는 방법은 설명하지 않는다. 링크를 참고하면 좋을 것 같다.

(이미 로컬 네트워크가 연결되어 있다고 가정)

  • 이번에는 이미 다른 기기에 분산되어 있는 액터를 다루게 되므로 더 이상 이전 예제에서처럼 ID만 구성할 수는 없다.
  • 같이 게임을 하고자 하는 다른 기기에서 특정 액터를 찾아야 한다.
  • 이 문제는 분산 액터에만 해당되는 문제는 아니며 일반적으로 서비스 검색 메커니즘을 사용하여 해결할 수 있다.
  • 그러나 분산 액터의 도메인에는 코드 전체에 걸쳐 강력한 API 유형을 고수할 수 있게 해주는 API 액터 시스템의 공통적인 패턴과 스타일이 있다.
  • 이를 리셉션 패턴(Receptionist Pattern)이라고 한다.

 

  • 호텔과 비슷하게 액터가 알려진다. (남들과 만나려면 체크인을 해야 하기 때문)
  • 모든 액터 시스템은 자체 Receptionist가 있으며 액터 검색을 구현하기 위해 기본 운송 메커니즘에 가장 적합한 수단을 모두 사용할 수 있다.
  • 때로는 기존 서비스 검색 API에 의존하여 타입 안전 API를 그 위에 계층화하거나 가십 기반 매커니즘 또는 완전히 다른 무언가를 구현할 수도 있다. -> 이러한 것들은 사용자의 관점에서 보는 구현 세부 사항이므로 우리는 신경 쓸 필요 없다.
  • 우리가 신경 써야 하는 단 하나는 우리 액터가 검색 가능한지 확인하고 그들을 검색해야 할 때 특정 태그나 타입으로 액터를 찾는 것이다.

SampleLocalNetworkActorSystem을 위해 발표자가 구현한 간단한 리셉셔니스트는 위와 같다.

  • 분산 액터 시스템의 모든 리셉셔니스트가 액터를 발견할 수 있도록 액터의 체크인을 지원한다.
  • 이어서 특정 타입의 모든 액터 목록을 얻고 그들이 해당 시스템에서 사용 가능해지면 태그를 지정할 수 있다.

 

이 리셉셔니스트를 이용해 게임을 함께 하고 싶은 상대 행위자를 찾아보자!

 

이전에 GameView는 뷰 생성자에서 직접 상대를 생성하거나 분석했다. 이제는 네트워크에 상대가 나타날 때까지 비동기적으로 기다려야 하기 때문에 이전처럼 구현할 수 없다.

 

따라서, 상대를 찾는 동안 ProgressView를 보여주도록 한다. 그동안 startMatchMaking을 수행한다.

  • 로컬 액터 시스템의 리셉셔니스트에게 상대 팀의 태그를 사용하여 태그 된 모든 액터의 목록을 요청한다.
  • 그다음은 비동기 반복문을 사용하여 들어오는 상대 행위자를 기다린다.
  • 시스템이 플레이 할 수 있는 상대가 있는 근처 기기를 발견하면 이 작업 루프가 다시 시작된다.

게임을 할 상대가 준비가 되어 있다고 가정해 보자

 

이 게임 모드를 사용하려면 OfflinePlayer의 구현을 약간 변경해야 한다.

OfflinePlayer를 LocalNetworkPlayer로 바꾸어 보자!

  • 액터 시스템으로 SampleLocalNetworkActorSystem을 사용한다.
  • 가장 큰 차이점은 인간 플레이어를 대표하는 액터의 makeMove 메서드가 이제 원격으로 호출된다는 점이다. ➡️ distributed 키워드 추가
  • 하지만 움직이는 것은 인간 플레이어의 책임이다.
  • 이 과제를 해결하기 위해 우리는 humanSelectedField 비동기 함수를 우리의 뷰모델에 도입한다.
  • 이 값은 인간 사용자가 필드 중 하나를 클릭할 때 트리거되는 @Published 값으로 제공된다.
  • 인간 플레이어가 필드를 클릭하면 makeMove 함수가 재개되고 우리는 실행된 GameMove를 원격으로 호출자에게 반환하여 원격 호출을 완료한다.

 

여기까지가 멀티플레이 모드를 위해 변경해야 하는 사항의 전부이다..!! (라고 하지만 네트워크 구현체는 이 영상에서 다루지 않아서 그쪽이 더 큰 문제 아닌가 싶은데...? 아무튼...액터 쪽의 구현 사항은 이게 끝인가 보네요)

 

멀티플레이어 모드를 위해 액터의 구현을 약간 변경해야 했지만 전체적인 디자인에서 실제로 바뀐 것은 전혀 없다.

즉, 게임 로직에 변화를 주지 않아도 구현이 가능했다!

 

 

Online: clustered server-side lobby system

웹 소켓을 이용해 기기 호스팅된 플레이어 액터를 서버 측 로비 시스템에 등록하면 시스템은 그들을 쌍으로 연결하고 그들 사이의 분산 호출에 대한 프록시 역할을 한다.

  • 기기가 온라인 플레이 모드로 전환되면 리셉셔니스트를 사용하여 GameLobby를 발견하고 가입을 호출한다.
  • GameLobby는 이용 가능한 플레이어를 추적하고 플레이어 한 쌍이 식별되면 게임을 시작한다.
  • 게임 세션은 게임의 driver 역할을 하며 움직임을 조사하고 서버에 저장된 게임의 reperesentation에 이를 표시한다.
  • 게임이 완료되면 결과를 모아서 로비에 보고하면 된다.

  • 더 흥미로운 것은 이 디자인을 수평으로 확장할 수 있다는 점이다.
  • 더 많은 게임 세션 액터를 만들어 더 많은 게임을 하나의 서버에서 동시에 제공할 수 있다.

  • 우리가 클러스터 액터 시스템을 가지고 있다면 분산 액터 덕분에 심지어 다른 노드에서 게임 세션을 만들어 클러스터 전체에서 동시 게임의 수의 로드 밸런싱을 할 수 있다.

이런 시나리오에서 사용할 수 있는 클러스터 액터 시스템 라이브러리를 오픈 소스로 제공한다. (SwiftNio로 구현)

 

 

 

요약

 

 

 

 

'iOS > Swift' 카테고리의 다른 글

Swift concurrency: Behind the scenes  (0) 2023.09.13
Protect mutable state with Swift actors  (0) 2023.08.31
Adopting Swift Packages in Xcode  (0) 2023.06.14
Creating Swift Packages  (1) 2023.06.07
Getting to Know Swift Package Manager  (0) 2023.05.28

댓글