본문 바로가기

NetMode, NetConnection, NetDriver, NetRole

@iamrain2025. 11. 14. 10:40

1. 언리얼 엔진 네트워크

 언리얼 엔진(UE)의 네트워킹 모델은 '리플리케이션(Replication, 복제)'이라는 강력한 추상화 모델 위에 구축되어 있다. 개발자는 어떤 액터(Actor)의 어떤 프로퍼티(Property)가 네트워크를 통해 동기화되어야 하는지만 지정하면, 엔진이 알아서 그 상태를 서버와 클라이언트 간에 일치시켜 준다. 내부에 NetMode, NetDriver, NetConnection, NetRole이라는 네 가지 핵심 컴포넌트가 유기적으로 얽혀 동작하는 정교한 시스템이 존재한다.

2. 핵심 컴포넌트

컴포넌트들은 하나의 시스템을 구성하는 각기 다른 계층의 요소들이다.

  • ENetMode: "나는 지금 어디에 있는가?" (네트워크 문맥)
  • UNetDriver: "누가 통신을 총괄하는가?" (네트워크 관리자)
  • UNetConnection: "누구와 대화하고 있는가?" (네트워크 연결 통로)
  • ENetRole: "내 역할은 무엇인가?" (액터의 역할과 책임)

2.1. ENetMode: 네트워크 문맥(Context) 정의

 ENetMode는 게임 인스턴스가 현재 어떤 네트워크 환경에서 실행되고 있는지를 나타내는 열거형(Enum)이다. UWorld::GetNetMode()를 통해 언제든지 현재 월드의 NetMode를 확인할 수 있으며, 이는 엔진 전반의 분기 처리에서 가장 기본적이고 중요한 스위치 역할을 한다.

  • NM_Standalone: 독립 실행 모드. 네트워크 기능이 전혀 사용되지 않는 싱글플레이어 게임 상태다. 서버도, 클라이언트도 아니다. 가장 단순한 모드.
  • NM_DedicatedServer: 데디케이티드 서버 모드. 렌더링이나 사용자 입력 없이, 오직 게임 월드의 시뮬레이션과 클라이언트들의 상태 동기화만을 목적으로 실행되는 순수한 서버다.
  • NM_ListenServer: 리슨 서버 모드. 한 명의 플레이어가 자신의 머신에서 서버를 호스팅하면서 동시에 게임을 플레이하는 상태다. 즉, 자기 자신에게는 서버이면서 동시에 클라이언트이고, 다른 원격 플레이어들에게는 서버 역할을 수행한다.
  • NM_Client: 클라이언트 모드. 데디케이티드 서버나 리슨 서버에 접속한 원격 플레이어의 상태다.

NetMode는 엔진 시작 시 -server, -listen과 같은 커맨드라인 인자나, UGameInstance::StartGame, APlayerController::ClientTravel 등의 함수 호출을 통해 결정된다. 게임플레이 코드에서 HasAuthority()와 같은 권한 검사는 결국 현재 NetMode가 서버(Listen or Dedicated)인지 클라이언트인지를 확인하는 것으로 귀결되는 경우가 많다.

2.2. UNetDriver: 네트워크의 총괄 관리자(Orchestrator)

UNetDriver는 하나의 UWorld 내에서 발생하는 모든 네트워크 트래픽을 관리하는 최상위 클래스다. 월드가 네트워크 기능을 시작할 때(예: Listen() 호출) 생성되며, 네트워크의 '운영체제'와 같은 역할을 수행한다.

내부 구조 및 핵심 책임

  • 소유 관계: UEngineUWorld를 소유하고, UWorldUNetDriver를 소유한다.
  • 연결 관리:
    • ClientConnections (TArray<UNetConnection*>) : 서버에서, 자신에게 접속한 모든 클라이언트와의 연결(UNetConnection)을 이 배열에 저장하고 관리한다.
    • ServerConnection (UNetConnection*) : 클라이언트에서, 자신이 접속한 서버와의 단일 연결을 이 포인터로 관리한다.
  • 리플리케이션 총괄: 어떤 액터를, 어떤 클라이언트에게, 어떻게 복제할 것인지를 결정한다.
    • ReplicationDriver: UNetDriver의 가장 중요한 기능 중 하나로, 리플리케이션을 처리하는 구체적인 로직을 담당한다. UE5에서는 FReplicationDriver가 이 역할을 하며, 액터의 우선순위, 컬링(culling), 관련성(relevancy) 등을 판단하여 효율적인 복제를 수행한다. 차세대 시스템인 아이리스(Iris)는 이 ReplicationDriver를 대체하여 더 높은 확장성과 성능을 제공하는 것을 목표로 한다.
  • 주요 실행 함수 (Tick):
    • TickDispatch(float DeltaTime): 매 프레임 호출되는 메인 루프. 소켓으로부터 들어온 패킷을 수신하고, 각 UNetConnection에 분배하여 처리하도록 지시한다.
    • TickFlush(float DeltaTime): TickDispatch 이후 호출. 각 UNetConnection이 보내기 위해 큐에 쌓아둔 데이터를 실제 패킷으로 만들어 소켓을 통해 전송한다.

구현 방식

UNetDriver는 추상 클래스이며, 실제 구현은 하위 클래스에서 이루어진다.

  • UIpNetDriver: 가장 일반적인 구현체로, UDP/IP 기반의 통신을 담당한다. 실제 소켓 생성 및 데이터 송수신은 이 클래스와 그 하위의 플랫폼별 소켓 서브시스템에서 이루어진다.
  • UNullNetDriver: 실제 네트워크 통신 없이 메모리 내에서 리플리케이션 로직을 테스트하기 위한 드라이버다. 싱글플레이 환경에서 서버와 클라이언트를 모두 시뮬레이션하여 멀티플레이어 버그를 디버깅하는 데 매우 유용하다 (-netmode=1 PIE 옵션).

2.3. UNetConnection: 1:1 연결 통로(Conduit)

UNetConnection은 서버와 단일 클라이언트 간의 논리적인 연결을 표현하는 클래스다. 데이터가 오가는 실제 통로이며, UNetDriver에 의해 소유 및 관리된다.

내부 구조 및 핵심 책임

  • 채널(Channel) 기반 다중화: 하나의 UNetConnection은 여러 개의 UChannel 을 통해 데이터를 다중화(multiplexing)한다. 각 채널은 특정 종류의 데이터를 안정적으로 주고받는 역할을 담당한다.
    • UControlChannel (채널 0): 연결의 생명주기를 관리하는 가장 기본적인 채널. 연결 수립, 로그인, 엔진 버전 및 애셋 호환성 검사 등 필수적인 제어 메시지를 담당한다. 연결이 살아있는 동안 항상 존재한다.
    • UVoiceChannel (채널 1): 음성 채팅 데이터를 처리한다.
    • UActorChannel (동적 할당): 리플리케이션의 핵심. 서버에서 클라이언트로 복제되는 각 액터마다 하나의 액터 채널이 생성된다. 이 채널의 역할은 다음과 같다.
      1. 액터 생명주기 관리: 클라이언트에 해당 액터를 생성(Spawn)하거나 파괴(Destroy)하라는 명령을 보낸다.
      2. 프로퍼티 리플리케이션: 담당 액터의 리플리케이트 프로퍼티 중 변경된 것들을 감지하고, 직렬화(serialization)하여 클라이언트에 전송한다.
      3. RPC 라우팅: 해당 액터를 대상으로 호출된 RPC(Remote Procedure Call)를 전송하거나, 수신된 RPC를 올바른 액터에 전달한다.
  • 상태 추적: 특정 액터의 어떤 프로퍼티가 마지막으로 언제 전송되었는지 등의 상태를 FReplicationChannelActorState와 같은 내부 구조체를 통해 추적하여, 불필요한 데이터 전송을 최소화한다.

2.4. ENetRole과 권한(Authority): 액터의 역할과 책임

ENetRole은 특정 액터가 현재 실행 중인 머신에서 어떤 역할을 수행하는지를 나타내는 열거형이다. 이는 액터의 상태를 누가 결정하고, 누가 시뮬레이션하는지를 명확히 구분하는 언리얼 네트워크의 핵심 철학이다.

  • ROLE_Authority: 권위. 해당 액터의 상태를 결정하는 '원본' 또는 '마스터' 복사본이다. 액터의 모든 프로퍼티를 자유롭게 변경할 수 있으며, 모든 중요한 로직(예: 체력 감소, 아이템 획득)은 반드시 권한을 가진 측에서 실행되어야 한다.
    • 서버 환경(NM_ListenServer, NM_DedicatedServer)에서는 월드에 스폰된 대부분의 액터가 이 역할을 갖는다.
  • ROLE_AutonomousProxy: 자율 프록시. 클라이언트에 존재하는 '내가 직접 조종하는' 액터의 복사본이다. (예: 내 플레이어 캐릭터). 서버로부터 상태 업데이트를 받지만, 동시에 사용자 입력을 받아 움직임을 '예측(predict)'하고 그 결과를 서버에 RPC로 전송할 수 있다. 이 예측 덕분에 네트워크 지연이 있어도 부드러운 조작감을 느낄 수 있다.
  • ROLE_SimulatedProxy: 시뮬레이션 프록시. 클라이언트에 존재하는 '남이 조종하는' 액터의 복사본이다. (예: 다른 플레이어 캐릭터, 몬스터, 움직이는 문). 이 액터는 서버가 보내주는 상태 업데이트(위치, 회전 등)를 수신하여 부드럽게 보간(interpolation)하여 보여주는 '관찰자' 역할만 수행한다. 스스로 상태를 변경할 수 없다.
  • ROLE_None: 네트워크와 무관한 액터.

AActor::HasAuthority() 함수는 GetLocalRole() == ROLE_Authority를 간단히 확인하는 헬퍼 함수로, 게임플레이 코드에서 가장 빈번하게 사용되는 권한 검사 구문이다.

3. 계층 구조 및 상호작용 분석

언리얼 네트워크 시스템은 다음과 같은 명확한 계층 구조를 가진다.

UEngineUWorldUNetDriverUNetConnectionUActorChannelAActor

이 구조 속에서 리플리케이션이 어떻게 동작하는지, 프로퍼티 변경을 예로 들어 단계별로 추적해보자.

  1. [서버: Authority] 상태 변경: 서버에서 권한을 가진(ROLE_Authority) AMyCharacter 액터의 Health 프로퍼티(Replicated로 지정됨)가 100에서 90으로 변경된다.
  2. [서버: UNetDriver] 변경 감지: 서버의 UNetDriver가 매 프레임 TickDispatch를 실행한다. 내부의 ReplicationDriverAMyCharacter가 'dirty' 상태가 되었음을 감지한다.
  3. [서버: UNetDriver] 관련성 검사: UNetDriver는 자신의 ClientConnections 배열을 순회하며 각 클라이언트를 검사한다. AMyCharacter가 클라이언트 A의 플레이어에게 '관련(relevant)'이 있는지(예: 시야에 보이는지, 충분히 가까운지) 판단한다.
  4. [서버: UNetConnection] 채널 선택: 클라이언트 A에게 관련이 있다면, UNetDriver는 클라이언트 A에 해당하는 UNetConnection에게 AMyCharacter의 복제를 지시한다. UNetConnectionAMyCharacter를 담당하는 UActorChannel을 찾거나, 없다면 새로 생성한다.
  5. [서버: UActorChannel] 직렬화: UActorChannelAMyCharacter의 프로퍼티 중 마지막으로 클라이언트 A에 보낸 상태와 현재 상태를 비교한다. Health가 변경되었음을 확인하고, 이 변경 사항(프로퍼티 인덱스, 새로운 값 90)을 비트 단위로 압축하여 전송할 데이터 덩어리(payload)에 기록한다.
  6. [서버: UNetDriver] 전송: UNetConnection은 준비된 데이터를 큐에 넣고, UNetDriverTickFlush 단계에서 이 데이터가 포함된 UDP 패킷을 클라이언트 A의 IP 주소로 전송한다.
  7. [클라이언트: UNetDriver] 수신 및 분배: 클라이언트 A의 UNetDriver가 UDP 패킷을 수신한다. 패킷 헤더를 분석하여 이 데이터가 ServerConnection의 특정 UActorChannel로 가야 함을 인지하고 전달한다.
  8. [클라이언트: UActorChannel] 역직렬화: 해당 UActorChannel은 수신된 데이터를 해석(역직렬화)하여, 자신이 담당하는 AMyCharacterHealth 프로퍼티를 90으로 업데이트해야 한다는 것을 알아낸다.
  9. [클라이언트: Proxy] 상태 적용: UActorChannel은 클라이언트 월드에 존재하는 AMyCharacter의 프록시(Autonomous 또는 Simulated) 인스턴스의 Health 값을 90으로 변경한다.
  10. [클라이언트: Proxy] RepNotify 호출: 만약 Health 프로퍼티가 RepNotify로 지정되었다면, 값이 변경된 직후 OnRep_Health() 함수가 클라이언트에서 호출된다. 이를 통해 체력 바 UI를 업데이트하는 등의 후처리를 수행할 수 있다.

4. 요약

 언리얼의 고수준 네트워크 시스템을 구성하는 계층적인 부품들입니다.

 먼저 NetMode 는 가장 간단한 개념으로, '지금 내 게임이 어떤 상태인가?'를 알려주는 이정표입니다. 혼자 하는 게임이면 Standalone, 전용 서버면 DedicatedServer, 방장이자 플레이어면 ListenServer, 접속한 클라이언트면 Client 중 하나의 상태를 갖습니다. 코드에서 HasAuthority() 같은 권한 검사를 할 때, 내부적으로는 이 NetMode를 보고 서버인지 클라이언트인지 판단하는 거죠.

 다음으로 NetDriver 는 '네트워크의 총괄 매니저'라고 생각하시면 됩니다. 게임 월드 하나당 NetDriver가 하나씩 존재하면서, 모든 네트워크 통신을 지휘합니다. 서버라면 모든 클라이언트와의 연결(NetConnection)들을 관리하고, 어떤 액터를 누구에게 복제해줄지 결정하고, 데이터를 패킷으로 만들어 보내는 모든 과정을 총괄합니다. 네트워크의 운영체제 같은 역할이죠.

 NetConnection 은 그 NetDriver 밑에서 일하는 '실무자'로, 서버와 특정 클라이언트 사이의 '1:1 연결 통로'를 의미합니다. 서버의 NetDriver는 여러 개의 NetConnection을 가지지만, 클라이언트는 서버로 향하는 단 하나의 NetConnection만 가집니다. 이 통로 안에는 또 여러 개의 '채널'이 있는데, 가장 중요한 것이 '액터 채널'입니다. 복제되는 모든 액터는 이 액터 채널을 통해 자신의 위치나 체력 같은 속성 변화나 원격 함수 호출(RPC)을 전달합니다.

 마지막으로 ENetRole 은 개별 액터의 입장에서 '내 역할이 무엇인가?'를 정의합니다.

  • Authority (권위): 이 액터의 상태를 결정하는 '진짜' 원본이라는 뜻입니다. 서버에 있는 액터들이 대부분 이 역할을 갖습니다.
  • AutonomousProxy (자율 프록시): 클라이언트에 있는 '내 캐릭터'처럼, 내가 직접 조종하는 복제본입니다. 서버의 허락을 받기 전에 스스로 움직임을 예측해서 부드럽게 보입니다.
  • SimulatedProxy (시뮬레이션 프록시): 클라이언트에 있는 '다른 플레이어 캐릭터'나 몬스터 같은 복제본입니다. 서버가 보내주는 정보대로 움직임을 따라가기만 하는 '관찰자' 역할이죠.

 종합하면 데이터가 이렇게 흐릅니다. 서버(Authority)에서 액터의 체력이 바뀌면, NetDriver가 이 변경을 감지합니다. 그리고 각 클라이언트와의 연결 통로인 NetConnection을 통해, 해당 액터를 담당하는 UActorChannel에게 "체력 바뀌었으니 보고해"라고 지시합니다. UActorChannel은 변경된 체력 값만 예쁘게 포장해서(직렬화) 클라이언트에 보냅니다. 클라이언트(Proxy)는 이 데이터를 받아서 자기 화면에 보이는 액터의 체력을 업데이트하는 겁니다.

 결론적으로, NetMode 는 현재의 네트워크 환경, NetDriver 는 그 환경의 총 책임자, NetConnection 은 개별 통신 라인, 그리고 ENetRole 은 그 라인을 통해 전달되는 데이터의 주체인 액터의 역할을 규정하는, 매우 체계적인 계층 구조라고 할 수 있습니다.

iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차