Distributed Actor 도전기: 이론에서 실전까지

피터·2025년 9월 29일

Concurrency

목록 보기
10/10
post-thumbnail

오늘은 WWDC 2022 - Meet distributed actors in Swift에 대해서 학습해보고 정리한 것을 공유하겠습니다.

동시성의 바다에서 분산 시스템으로

Swift Actor는 동일한 프로세스 내에서 저수준 데이터 경합으로부터 보호하도록 설계되었습니다. "동시성의 바다"에서 각 Actor는 고유한 섬이며, 서로의 섬에 직접 접근하는 대신 메시지를 교환합니다.

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

어디에 사용되는 것일까?

AirDrop의 경우: 같은 Wi-Fi 환경이나 Bluetooth를 통해 각 기기의 프로세스가 통신하는 것이죠. 각 iPhone이나 Mac의 AirDrop 프로세스가 하나의 "분산 Actor"라고 생각할 수 있습니다.

닌텐도 게임기 통신: 포켓몬이나 동물의 숲에서 근처 기기와 데이터를 교환하는 것도 마찬가지입니다. 각 게임기의 프로세스가 로컬 네트워크를 통해 통신하는 분산 시스템이에요.

핵심 개념들

1. 위치 투명성 (Location Transparency)

분산 Actor의 가장 중요한 특징은 위치 투명성입니다. Actor가 로컬에 있든 원격에 있든 동일한 방식으로 상호작용할 수 있습니다.

// 로컬이든 원격이든 똑같이 호출
let response = await actor.sendMessage("안녕!")
  • 일반 Actor: 같은 앱 내에서만 호출 가능
  • Distributed Actor: 네트워크를 넘어서 호출 가능

하지만 코드상으로는 동일하게 await actor.method()로 호출합니다. 이것이 바로 위치 투명성의 핵심입니다.

2. Actor System

모든 분산 Actor는 Actor System에 속하며, 이 시스템이 원격 호출에 필요한 모든 직렬화 및 네트워킹을 처리합니다.

Actor System은 "분산 Actor들의 관리자" 역할을 합니다. 네트워크 환경에서 연결을 허용한 상태라면, 같은 Actor System에 등록된 모든 Actor들이 서로 통신할 수 있습니다.

// 예시: 로컬 네트워크에서 Actor System 생성
let actorSystem = SampleLocalNetworkActorSystem()

// 이 시스템에 연결된 모든 기기의 Actor들이 서로 통신 가능

WWDC 세션에서 소개된 리셉셔니스트(Receptionist) 패턴이 이 과정을 담당합니다:

  • Actor들이 시스템에 "체크인"하여 발견 가능해짐
  • 같은 시스템에 속한 Actor들끼리 통신 가능
  • 호텔 리셉션처럼 "누가 어디에 있는지" 관리

3. ID 기반 식별

각 분산 Actor는 전체 분산 시스템에서 고유한 ID를 가지며, 이를 통해 원격 Actor를 식별하고 통신합니다.

ID가 서로 식별되면 실제로 통신이 시작됩니다. WWDC 세션에서 설명한 과정은 다음과 같습니다:

  1. Actor 생성 시 ID 할당: Actor System이 자동으로 고유 ID 부여
  2. 원격 참조 해결: resolve 메서드로 ID를 통해 Actor 찾기
  3. 통신 시작: 첫 번째 메시지 전송 시 실제 연결 시도
// 원격 Actor ID로 참조 해결
let remoteActor = try BotPlayer.resolve(id: actorID, using: actorSystem)

// 이 시점에서 실제 통신 시작
let move = await remoteActor.makeMove()

4. 직렬화 요구사항

네트워크 경계를 넘는 모든 매개변수와 반환 값은 Actor System의 직렬화 요구사항(예: Codable)을 준수해야 합니다.

직렬화란 데이터를 주고받는 과정에서 JSON으로 바꾸는 것도 포함되는데, 왜 "직렬화"라고 부르는지 궁금했습니다.

직렬화(Serialization)는 "순서대로 줄 세우기"에서 나온 용어입니다:

  • Serial = 순서대로, 연속적으로
  • 메모리의 복잡한 객체를 일렬로 나열된 바이트 스트림으로 변환
  • 네트워크로 전송하거나 파일에 저장 가능한 형태로 변환

역직렬화(Deserialization)는 그 반대 과정입니다:

  • 바이트 스트림을 다시 원래 객체로 복원
  • De- 접두사는 "반대로"라는 의미
// 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)

활용 시나리오

  1. 클라이언트-서버 구조: 서버의 Actor와 클라이언트의 Actor 간 통신
  2. P2P 네트워킹: 중앙 서버 없이 기기 간 직접 통신
  3. 클러스터링: 서버 클러스터 내에서 부하 분산
  4. 마이크로서비스: 서비스 간 타입 안전한 통신

클러스터링이 무엇인지, 그리고 이것이 채팅앱에 어떻게 적용될 수 있는지 궁금했습니다.

클러스터링(Clustering)이란 여러 서버를 하나의 시스템처럼 동작하게 하는 기술입니다. 부하를 여러 서버에 분산하여 처리 능력을 향상시키고, 한 서버가 다운되어도 다른 서버가 이어받아 안정성을 보장합니다.

채팅앱 적용 가능성에 대해서는, WWDC 세션에서 Apple이 SwiftNIO 기반 클러스터 라이브러리를 오픈소스로 공개했다고 언급했습니다.

흥미로운 점은 Distributed Actor가 소켓을 대체하는 게 아니라 추상화한다는 것입니다. 내부적으로는 여전히 소켓/WebSocket을 사용하지만, 개발자는 Actor 호출만 신경쓰면 됩니다.

즉, "소켓 없이"가 아니라 "소켓을 신경쓰지 않고" 개발할 수 있게 해주는 기술입니다!


🔄 Distributed Actor 동작 과정 가이드

이론을 넘어서, 실제로 Distributed Actor가 어떻게 동작하는지 두 가지 모델로 나누어 단계별로 정리해보겠습니다.

WWDC에서는 두 가지 주요 모델을 소개했습니다:

📊 두 가지 모델 비교

구분클라이언트-서버 모델P2P 모델
Actor SystemSampleWebSocketActorSystemSampleLocalNetworkActorSystem
연결 방식WebSocket (인터넷)Local Network (Wi-Fi)
Actor 생성서버에서 온디맨드 생성각 기기에서 직접 생성
발견 방식ID 기반 resolve리셉셔니스트 패턴
사용 사례원격 게임 서버근거리 P2P 게임

🌐 모델 1: 클라이언트-서버 (WebSocket)

1단계: 일반 Actor에서 Distributed Actor로 변환

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)
    }
}

2단계: Actor System 생성 및 활성화

모든 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()
    }
}

3단계: 클라이언트에서 원격 봇 ID 해결

// 클라이언트에서 원격 봇 플레이어 참조 해결
let sampleSystem: SampleWebSocketActorSystem

// 상대방 봇을 위한 임의의 ID 생성
let opponentID: BotPlayer.ID = .randomID(opponentFor: self.id)

// 원격 봇 플레이어 참조 해결 (아직 네트워크 통신 없음)
let bot = try BotPlayer.resolve(id: opponentID, using: sampleSystem)

4단계: 실제 분산 호출 시작 (온디맨드 생성)

// 첫 번째 분산 메서드 호출 시 실제 네트워크 통신 시작
do {
    // 이 라인에서 서버의 온디맨드 생성이 트리거됨
    let move = try await bot.makeMove()
    print("서버 봇의 움직임: \(move)")

    // 상대에게 내 움직임 알리기
    try await bot.opponentMoved(myMove)

} catch {
    print("분산 호출 실패: \(error)")
}

💡 온디맨드(On-Demand) 생성이란?

온디맨드 = "필요할 때만"

기존 방식 (Pre-created):

서버 시작 시 → 모든 BotPlayer 미리 생성 → 메모리 낭비

온디맨드 방식 (WWDC 권장):

서버 시작 → 대기 상태
클라이언트 요청 → 그때서야 BotPlayer 생성 → 효율적!

실제 동작 과정:
1. 클라이언트: await bot.makeMove() 호출
2. 서버: "아, 이 ID의 봇이 필요하구나!"
3. 서버: registerOnDemandResolveHandler 실행
4. 서버: 새로운 BotPlayer 생성
5. 서버: makeMove() 메서드 실행 후 결과 반환

5단계: 클라이언트-서버 게임 진행

// 게임 루프 - 클라이언트가 서버 봇과 대전
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()
    }
}

🔗 모델 2: P2P (Local Network)

1단계: LocalNetworkPlayer 구현

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)
    }
}

2단계: P2P Actor System 생성 및 리셉셔니스트 등록

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 시스템 활성화 완료!")
    }
}

3단계: 리셉셔니스트를 통한 상대 발견

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 // 한 명의 상대만 찾으면 됨
    }
}

4단계: P2P 게임 진행

// 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의 역할)

// 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)
    }
}

🌊 두 모델 전체 플로우 요약

모델 1: 클라이언트-서버 (WebSocket)

모델 2: P2P (Local Network)

🔑 핵심 포인트: Actor System 라이프사이클

  1. 생성: 앱 시작 시 ActorSystem 인스턴스 생성
  2. 활성화: 서버는 포트 바인딩, 클라이언트는 서버 연결
  3. 등록: Distributed Actor 생성 시 자동으로 시스템에 등록
  4. 발견: 리셉셔니스트 패턴으로 상대 Actor 발견
  5. 통신: 첫 번째 await 호출에서 실제 네트워크 통신 시작
  6. 종료: 앱 종료 시 시스템 정리

🎯 프로젝트 시작: 야심찬 목표

"Distributed Actor를 실제로 사용해보자!"

Swift 5.7에서 도입된 Distributed Actor는 분산 컴퓨팅의 새로운 패러다임을 제시했습니다. 이론상으로는 네트워크 경계를 투명하게 넘나드는 Actor 호출이 가능하다는 매력적인 기술이었죠.

// 이런 게 가능하다고?
distributed actor MessengerActor {
    distributed func sendMessage(_ text: String) async -> String
}

// 로컬이든 원격이든 똑같이 호출
let response = await actor.sendMessage("안녕!")

하지만 현실은... 생각보다 험난했습니다.


📱 실제로 구현한 것: MultipeerConnectivity P2P 메신저

최종 아키텍처

핵심 컴포넌트

1. ConversationViewModel: UI와 비즈니스 로직 연결

@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)
        }
    }
}

핵심 포인트:

  • @ObservedObject vs @State: UI 업데이트 반응성의 차이 발견
  • AsyncStream을 통한 실시간 메시지 스트림 처리
  • MainActor로 UI 스레드 안전성 보장

2. MessengerActor: 동시성 안전한 메시지 처리

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)
    }
}

핵심 포인트:

  • Actor를 통한 race condition 방지
  • AsyncStream.Continuation으로 이벤트 기반 메시지 전달
  • MainActor 격리를 통한 UI 컴포넌트 안전한 접근

3. NetworkManager: MultipeerConnectivity 래퍼

@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 도전과 실패

시도 1: LocalTestingDistributedActorSystem

distributed actor SimpleDistributedMessengerActor {
    typealias ActorSystem = LocalTestingDistributedActorSystem
    typealias SerializationRequirement = Codable

    distributed func sendDistributedMessage(_ text: String) async throws -> String {
        return "분산 응답: \(text)"
    }
}

결과: 같은 앱 내에서만 작동. 실제 네트워킹 없음.

시도 2: 커스텀 MultipeerDistributedActorSystem

@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 { /* 구현 */ }
    // ... 더 많은 메서드들
}

결과:

  • DistributedActorSystem 프로토콜 요구사항이 너무 복잡
  • Swift 6 Concurrency 제약사항들
  • 프로덕션 수준 구현은 현실적으로 불가능

🔧 개발 과정에서 마주한 도전들

1. MultipeerConnectivity 네트워킹

발견한 제약사항들:

  • 시뮬레이터끼리는 통신 불가 (실제 기기 필요)
  • Mac Catalyst 모드에서 제한적 작동
  • 네트워크 권한 설정 필수
<!-- Info.plist -->
<key>NSLocalNetworkUsageDescription</key>
<string>다른 기기와 메시지를 주고받기 위해 로컬 네트워크 접근이 필요합니다.</string>

<key>NSBonjourServices</key>
<array>
    <string>_messenger._tcp</string>
</array>

2. UI 반응성 문제 해결

// ❌ 문제: @State 사용 시 업데이트 안됨
@State var viewModel: ConversationViewModel

// ✅ 해결: @ObservedObject 사용
@ObservedObject var viewModel: ConversationViewModel

📊 최종 성과: 실제 작동하는 P2P 메신저

실제 테스트 로그

📱 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: "안녕하세요! 잘 받았어요")

기능 완성도

✅ 완성된 기능들:

  • 실시간 P2P 메시징
  • 자동 기기 발견 및 연결
  • JSON 기반 메시지 직렬화
  • SwiftUI 실시간 UI 업데이트
  • Swift Concurrency 완전 활용
  • 네트워크 상태 디버깅

❌ 미완성 기능들:

  • 실제 분산 Actor 시스템
  • 멀티 피어 지원
  • 메시지 히스토리
  • 파일 전송

🎓 배운 점들

1. Distributed Actor의 현실

  • 이론적으로는 매력적이지만 실제 구현은 매우 복잡
  • 현재(2024-2025)로서는 학습용으로만 적합
  • 프로덕션에서는 기존 네트워킹 방식이 더 안정적

2. Swift Concurrency 마스터리

  • Actor를 통한 데이터 경합 방지
  • MainActor와 UI 스레드 관리
  • AsyncStream을 통한 이벤트 스트림 처리

3. MultipeerConnectivity 실전 활용

  • P2P 앱 개발의 핵심 기술
  • 네트워크 권한과 설정의 중요성
  • 실제 기기 테스트의 필요성

4. SwiftUI 상태 관리

  • @ObservedObject vs @State의 차이점
  • 비동기 작업과 UI 업데이트 연결
  • MVVM 패턴의 실제 적용

🚀 GitHub에서 코드 확인하기

전체 프로젝트는 GitHub에서 확인할 수 있습니다:
https://github.com/Peter1119/DistributedMessenger

핵심 파일들:

  • ConversationViewModel.swift: 메인 비즈니스 로직
  • MessengerActor.swift: Actor 기반 메시지 처리
  • NetworkManager.swift: MultipeerConnectivity 래퍼
  • DistributedMessengerActor.swift: Distributed Actor 실험

🎯 결론: 실패에서 얻은 성공

원래 목표였던 "Distributed Actor 활용"은 실패했지만, 그 과정에서 다음을 얻었습니다:

  1. 실제 작동하는 P2P 메신저 앱
  2. Swift Concurrency 깊은 이해
  3. MultipeerConnectivity 실전 경험
  4. SwiftUI 고급 패턴 학습

때로는 야심찬 목표를 추구하다가 예상치 못한 더 가치 있는 결과를 얻기도 합니다. Distributed Actor는 미래의 과제로 남겨두고, 완성된 P2P 메신저로 만족하기로 했습니다.

profile
iOS 개발자입니다.

0개의 댓글