
오늘은 WWDC 2022 - Meet distributed actors in Swift에 대해서 학습해보고 정리한 것을 공유하겠습니다.
Swift Actor는 동일한 프로세스 내에서 저수준 데이터 경합으로부터 보호하도록 설계되었습니다. "동시성의 바다"에서 각 Actor는 고유한 섬이며, 서로의 섬에 직접 접근하는 대신 메시지를 교환합니다.

Distributed Actor는 동일한 개념적 Actor 모델을 여러 프로세스로 확장합니다:

AirDrop의 경우: 같은 Wi-Fi 환경이나 Bluetooth를 통해 각 기기의 프로세스가 통신하는 것이죠. 각 iPhone이나 Mac의 AirDrop 프로세스가 하나의 "분산 Actor"라고 생각할 수 있습니다.
닌텐도 게임기 통신: 포켓몬이나 동물의 숲에서 근처 기기와 데이터를 교환하는 것도 마찬가지입니다. 각 게임기의 프로세스가 로컬 네트워크를 통해 통신하는 분산 시스템이에요.

분산 Actor의 가장 중요한 특징은 위치 투명성입니다. Actor가 로컬에 있든 원격에 있든 동일한 방식으로 상호작용할 수 있습니다.
// 로컬이든 원격이든 똑같이 호출
let response = await actor.sendMessage("안녕!")
하지만 코드상으로는 동일하게 await actor.method()로 호출합니다. 이것이 바로 위치 투명성의 핵심입니다.
모든 분산 Actor는 Actor System에 속하며, 이 시스템이 원격 호출에 필요한 모든 직렬화 및 네트워킹을 처리합니다.
Actor System은 "분산 Actor들의 관리자" 역할을 합니다. 네트워크 환경에서 연결을 허용한 상태라면, 같은 Actor System에 등록된 모든 Actor들이 서로 통신할 수 있습니다.
// 예시: 로컬 네트워크에서 Actor System 생성
let actorSystem = SampleLocalNetworkActorSystem()
// 이 시스템에 연결된 모든 기기의 Actor들이 서로 통신 가능
WWDC 세션에서 소개된 리셉셔니스트(Receptionist) 패턴이 이 과정을 담당합니다:

각 분산 Actor는 전체 분산 시스템에서 고유한 ID를 가지며, 이를 통해 원격 Actor를 식별하고 통신합니다.
ID가 서로 식별되면 실제로 통신이 시작됩니다. WWDC 세션에서 설명한 과정은 다음과 같습니다:
resolve 메서드로 ID를 통해 Actor 찾기// 원격 Actor ID로 참조 해결
let remoteActor = try BotPlayer.resolve(id: actorID, using: actorSystem)
// 이 시점에서 실제 통신 시작
let move = await remoteActor.makeMove()
네트워크 경계를 넘는 모든 매개변수와 반환 값은 Actor System의 직렬화 요구사항(예: Codable)을 준수해야 합니다.
직렬화란 데이터를 주고받는 과정에서 JSON으로 바꾸는 것도 포함되는데, 왜 "직렬화"라고 부르는지 궁금했습니다.
직렬화(Serialization)는 "순서대로 줄 세우기"에서 나온 용어입니다:
역직렬화(Deserialization)는 그 반대 과정입니다:
// Swift에서 Codable을 통한 직렬화/역직렬화
struct GameMove: Codable {
let player: String
let position: Int
}
// 직렬화: 객체 → JSON 바이트
let move = GameMove(player: "Player1", position: 5)
let jsonData = try JSONEncoder().encode(move)
// 역직렬화: JSON 바이트 → 객체
let decodedMove = try JSONDecoder().decode(GameMove.self, from: jsonData)
클러스터링이 무엇인지, 그리고 이것이 채팅앱에 어떻게 적용될 수 있는지 궁금했습니다.
클러스터링(Clustering)이란 여러 서버를 하나의 시스템처럼 동작하게 하는 기술입니다. 부하를 여러 서버에 분산하여 처리 능력을 향상시키고, 한 서버가 다운되어도 다른 서버가 이어받아 안정성을 보장합니다.

채팅앱 적용 가능성에 대해서는, WWDC 세션에서 Apple이 SwiftNIO 기반 클러스터 라이브러리를 오픈소스로 공개했다고 언급했습니다.
흥미로운 점은 Distributed Actor가 소켓을 대체하는 게 아니라 추상화한다는 것입니다. 내부적으로는 여전히 소켓/WebSocket을 사용하지만, 개발자는 Actor 호출만 신경쓰면 됩니다.
즉, "소켓 없이"가 아니라 "소켓을 신경쓰지 않고" 개발할 수 있게 해주는 기술입니다!
이론을 넘어서, 실제로 Distributed Actor가 어떻게 동작하는지 두 가지 모델로 나누어 단계별로 정리해보겠습니다.
WWDC에서는 두 가지 주요 모델을 소개했습니다:
| 구분 | 클라이언트-서버 모델 | P2P 모델 |
|---|---|---|
| Actor System | SampleWebSocketActorSystem | SampleLocalNetworkActorSystem |
| 연결 방식 | WebSocket (인터넷) | Local Network (Wi-Fi) |
| Actor 생성 | 서버에서 온디맨드 생성 | 각 기기에서 직접 생성 |
| 발견 방식 | ID 기반 resolve | 리셉셔니스트 패턴 |
| 사용 사례 | 원격 게임 서버 | 근거리 P2P 게임 |
Before: 일반 Actor
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)
}
}
After: Distributed Actor
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 // 분산 액터 시스템 속성 초기화
self.gameState = .init()
self.ai = RandomPlayerBotAI(playerID: self.id, team: team) // 합성된 id 속성 사용
}
// distributed 키워드로 원격 호출 가능
public distributed func makeMove() throws -> GameMove {
return try ai.decideNextMove(given: &gameState)
}
public distributed func opponentMoved(_ move: GameMove) async throws {
try gameState.mark(move)
}
}
모든 Distributed Actor는 반드시 Actor System에 등록되어야 합니다.
서버 측 - Actor System 생성 및 활성화:
import Distributed
import TicTacFishShared
@main
struct Boot {
static func main() async throws {
// 1. Actor System 생성 및 활성화
let system = try SampleWebSocketActorSystem(mode: .serverOnly(host: "localhost", port: 8888))
// 2. 온디맨드(On-Demand) 액터 생성 핸들러 등록
// 온디맨드 = "필요할 때만" 생성한다는 의미
system.registerOnDemandResolveHandler { id in
// 클라이언트에서 특정 ID로 요청이 올 때만 BotPlayer 생성
if system.isBotID(id) {
return system.makeActorWithID(id) {
BotPlayer(team: .rodents, actorSystem: system) // 자동으로 시스템에 등록됨
}
}
return nil // 생성할 수 없는 ID
}
print("=== TicTacFish Server Running on: ws://\(system.host):\(system.port) ===")
// 3. 서버 시스템 활성화 상태 유지
try await system.terminated
}
}
클라이언트 측 - Actor System 생성 및 연결:
// 앱 시작 시 Actor System 생성
class GameApp {
private let actorSystem: SampleWebSocketActorSystem
init() async throws {
// 1. 클라이언트 모드로 Actor System 생성
self.actorSystem = try SampleWebSocketActorSystem(mode: .client(host: "localhost", port: 8888))
// 2. 서버에 연결 (내부적으로 WebSocket 연결)
try await actorSystem.connect()
print("서버에 연결됨!")
}
func startGame() async throws {
// 3. 이제 Distributed Actor 사용 가능
let opponentID: BotPlayer.ID = .randomID(opponentFor: self.playerID)
let bot = try BotPlayer.resolve(id: opponentID, using: actorSystem)
// 4. 첫 번째 호출에서 서버 측 온디맨드 생성 트리거
// 이 시점에서 서버가 BotPlayer를 "필요할 때" 생성함
let move = try await bot.makeMove()
}
}
// 클라이언트에서 원격 봇 플레이어 참조 해결
let sampleSystem: SampleWebSocketActorSystem
// 상대방 봇을 위한 임의의 ID 생성
let opponentID: BotPlayer.ID = .randomID(opponentFor: self.id)
// 원격 봇 플레이어 참조 해결 (아직 네트워크 통신 없음)
let bot = try BotPlayer.resolve(id: opponentID, using: sampleSystem)
// 첫 번째 분산 메서드 호출 시 실제 네트워크 통신 시작
do {
// 이 라인에서 서버의 온디맨드 생성이 트리거됨
let move = try await bot.makeMove()
print("서버 봇의 움직임: \(move)")
// 상대에게 내 움직임 알리기
try await bot.opponentMoved(myMove)
} catch {
print("분산 호출 실패: \(error)")
}
온디맨드 = "필요할 때만"
기존 방식 (Pre-created):
서버 시작 시 → 모든 BotPlayer 미리 생성 → 메모리 낭비
온디맨드 방식 (WWDC 권장):
서버 시작 → 대기 상태
클라이언트 요청 → 그때서야 BotPlayer 생성 → 효율적!
실제 동작 과정:
1. 클라이언트: await bot.makeMove() 호출
2. 서버: "아, 이 ID의 봇이 필요하구나!"
3. 서버: registerOnDemandResolveHandler 실행
4. 서버: 새로운 BotPlayer 생성
5. 서버: makeMove() 메서드 실행 후 결과 반환
// 게임 루프 - 클라이언트가 서버 봇과 대전
func playWithServerBot() async throws {
var gameFinished = false
while !gameFinished {
// 1. 플레이어 턴 - 로컬에서 움직임 생성
let myMove = createPlayerMove()
// 2. 서버 봇에게 상대 움직임 알리기
try await bot.opponentMoved(myMove)
// 3. 서버 봇의 다음 움직임 요청
let botMove = try await bot.makeMove()
// 4. 게임 상태 업데이트
updateGameBoard(with: botMove)
gameFinished = checkGameEnd()
}
}
P2P 환경에서는 인간 플레이어도 Distributed Actor가 됩니다:
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() // UI에서 사용자 입력 대기
movesMade += 1
let move = GameMove(
playerID: self.id,
position: field,
team: team,
teamCharacterID: movesMade % 2)
return 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)
}
}
class P2PGameApp {
private let localNetworkSystem: SampleLocalNetworkActorSystem
private var player: LocalNetworkPlayer!
init() async throws {
// 1. 로컬 네트워크 Actor System 생성
self.localNetworkSystem = try SampleLocalNetworkActorSystem()
// 2. 내 플레이어 Actor 생성 (자동으로 시스템에 등록됨)
self.player = LocalNetworkPlayer(
team: .fish,
model: gameViewModel,
actorSystem: localNetworkSystem
)
// 3. 리셉셔니스트에 체크인 (다른 기기에서 발견 가능하게 함)
await localNetworkSystem.receptionist.checkIn(player, tag: player.team.tag)
print("P2P 시스템 활성화 완료!")
}
}
func startMatchmaking() async throws {
// 상대 팀 결정 (내가 물고기 팀이면 상대는 설치류 팀)
let opponentTeam = player.team == .fish ? CharacterTeam.rodents : CharacterTeam.fish
// 리셉셔니스트를 통한 상대 발견
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)
return // 한 명의 상대만 찾으면 됨
}
}
// P2P 환경에서의 실제 게임 플로우
func startP2PGame(with opponent: OpponentPlayer) async {
// 상대에게 게임 시작 알림
try await opponent.startGameWith(opponent: self.player, startTurn: false)
// 게임 진행 - 턴 기반 분산 호출
while !gameFinished {
if isMyTurn {
// 내가 움직임을 만들고 상대에게 알림
let myMove = await self.player.makeMove()
try await opponent.opponentMoved(myMove)
} else {
// 상대의 움직임을 기다림 (상대가 나의 makeMove() 호출)
let opponentMove = await waitForOpponentMove()
updateGameBoard(with: opponentMove)
}
gameFinished = checkGameEnd()
isMyTurn.toggle()
}
}
// Actor System이 내부적으로 수행하는 작업들
class SampleWebSocketActorSystem {
// 1. 원격 호출 시 직렬화
func remoteCall() {
let message = RemoteCallMessage(
target: actorID,
method: "makeMove",
parameters: []
)
// JSON으로 직렬화
let jsonData = try JSONEncoder().encode(message)
// WebSocket으로 전송
webSocket.send(jsonData)
}
// 2. 응답 수신 시 역직렬화
func handleResponse(data: Data) {
let response = try JSONDecoder().decode(RemoteResponse.self, from: data)
// 기다리고 있던 호출에 응답 전달
waitingCalls[response.callID]?.resume(returning: response.result)
}
}


ActorSystem 인스턴스 생성await 호출에서 실제 네트워크 통신 시작"Distributed Actor를 실제로 사용해보자!"
Swift 5.7에서 도입된 Distributed Actor는 분산 컴퓨팅의 새로운 패러다임을 제시했습니다. 이론상으로는 네트워크 경계를 투명하게 넘나드는 Actor 호출이 가능하다는 매력적인 기술이었죠.
// 이런 게 가능하다고?
distributed actor MessengerActor {
distributed func sendMessage(_ text: String) async -> String
}
// 로컬이든 원격이든 똑같이 호출
let response = await actor.sendMessage("안녕!")
하지만 현실은... 생각보다 험난했습니다.

@MainActor
class ConversationViewModel: ObservableObject {
@Published var oppenentMessage: MessageModel?
@Published var myMessage: MessageModel?
private let messageActor = MessengerActor()
init() {
// AsyncStream으로 실시간 메시지 수신
Task {
for await message in await messageActor.startListening() {
await MainActor.run {
self.oppenentMessage = message
}
}
}
}
func sendMessage(_ text: String) {
myMessage = MessageModel(sender: "나", text: text)
Task {
await messageActor.sendMessage(text)
}
}
}
핵심 포인트:
actor MessengerActor {
private var messageStream: AsyncStream<MessageModel>.Continuation?
private var networkManager: NetworkManager?
func startListening() -> AsyncStream<MessageModel> {
return AsyncStream { continuation in
self.messageStream = continuation
Task { @MainActor in
let networkManager = NetworkManager { message in
Task {
await self.onMessageReceived(message)
}
}
await self.setNetworkManager(networkManager)
}
}
}
func sendMessage(_ text: String) async {
await MainActor.run {
networkManager?.sendMessage(text)
}
}
private func onMessageReceived(_ message: MessageModel) {
messageStream?.yield(message)
}
}
핵심 포인트:
@MainActor
class NetworkManager: NSObject, ObservableObject {
private let session: MCSession
private let advertiser: MCNearbyServiceAdvertiser
private let browser: MCNearbyServiceBrowser
private let onMessageReceived: (MessageModel) -> Void
init(onMessageReceived: @escaping (MessageModel) -> Void) {
let peerID = MCPeerID(displayName: UIDevice.current.name)
self.session = MCSession(peer: peerID, securityIdentity: nil,
encryptionPreference: .none)
self.advertiser = MCNearbyServiceAdvertiser(peer: peerID,
discoveryInfo: nil,
serviceType: "dmessenger")
self.browser = MCNearbyServiceBrowser(peer: peerID,
serviceType: "dmessenger")
self.onMessageReceived = onMessageReceived
super.init()
session.delegate = self
advertiser.delegate = self
browser.delegate = self
// 자동 기기 발견 및 연결
advertiser.startAdvertisingPeer()
browser.startBrowsingForPeers()
}
func sendMessage(_ text: String) {
guard !session.connectedPeers.isEmpty else { return }
let message = MessageModel(sender: session.myPeerID.displayName, text: text)
if let data = try? JSONEncoder().encode(message) {
try? session.send(data, toPeers: session.connectedPeers, with: .reliable)
}
}
}
distributed actor SimpleDistributedMessengerActor {
typealias ActorSystem = LocalTestingDistributedActorSystem
typealias SerializationRequirement = Codable
distributed func sendDistributedMessage(_ text: String) async throws -> String {
return "분산 응답: \(text)"
}
}
결과: 같은 앱 내에서만 작동. 실제 네트워킹 없음.
@MainActor
final class MultipeerDistributedActorSystem: NSObject, DistributedActorSystem {
typealias ActorID = MCPeerID
typealias SerializationRequirement = Codable
typealias InvocationEncoder = MultipeerInvocationEncoder
typealias InvocationDecoder = MultipeerInvocationDecoder
typealias ResultHandler = MultipeerResultHandler
// 50+ 메서드 구현 필요...
func remoteCall<Act, Err, Res>(...) async throws -> Res {
/* 복잡한 구현 */
}
func makeInvocationEncoder() -> InvocationEncoder { /* 구현 */ }
// ... 더 많은 메서드들
}
결과:
발견한 제약사항들:
<!-- Info.plist -->
<key>NSLocalNetworkUsageDescription</key>
<string>다른 기기와 메시지를 주고받기 위해 로컬 네트워크 접근이 필요합니다.</string>
<key>NSBonjourServices</key>
<array>
<string>_messenger._tcp</string>
</array>
// ❌ 문제: @State 사용 시 업데이트 안됨
@State var viewModel: ConversationViewModel
// ✅ 해결: @ObservedObject 사용
@ObservedObject var viewModel: ConversationViewModel
📱 ConversationViewModel 초기화 시작
🌍 NetworkManager 초기화 - 내 ID: iPad
📡 Advertising 시작...
🔍 Browsing 시작...
🔍 기기 발견: iPhone
📨 iPhone에게 초대 전송
✅ Actor 준비 완료
🔗 iPhone: ✅ 연결됨
📤 메시지 전송: 안녕하세요!
🤖 Actor: sendMessage 호출됨 - 안녕하세요!
📤 실제 네트워크 전송 시도: 안녕하세요!
👥 연결된 기기 수: 1
👤 연결된 기기: iPhone
✅ 네트워크 메시지 전송 성공
📩 iPhone에서 데이터 수신: 42 bytes
💬 메시지 디코딩 성공: MessageModel(sender: "iPhone", text: "안녕하세요! 잘 받았어요")
📩 메시지 수신: MessageModel(sender: "iPhone", text: "안녕하세요! 잘 받았어요")
✅ 완성된 기능들:
❌ 미완성 기능들:
전체 프로젝트는 GitHub에서 확인할 수 있습니다:
https://github.com/Peter1119/DistributedMessenger
핵심 파일들:
ConversationViewModel.swift: 메인 비즈니스 로직MessengerActor.swift: Actor 기반 메시지 처리NetworkManager.swift: MultipeerConnectivity 래퍼DistributedMessengerActor.swift: Distributed Actor 실험원래 목표였던 "Distributed Actor 활용"은 실패했지만, 그 과정에서 다음을 얻었습니다:
때로는 야심찬 목표를 추구하다가 예상치 못한 더 가치 있는 결과를 얻기도 합니다. Distributed Actor는 미래의 과제로 남겨두고, 완성된 P2P 메신저로 만족하기로 했습니다.