본문 바로가기
Unreal Engine/멀티플레이어 게임 개발

[언리얼 멀티플레이 기초 개념] 멀티플레이어 기본 개념 및 네트워크 아키텍처

by 4.41p._.m 2026. 7. 30.

1. 개요 및 네트워크 서버 모델 분류의 중요성

1-1. 왜 이것이 중요한가

멀티플레이어 게임 개발에서 네트워크 서버 모델 분류를 명확히 이해하는 것은 프로젝트의 네트워크 아키텍처와 비용, 그리고 보안 정책을 결정하는 첫 단추입니다. 특히 각 컴퓨터가 클라이언트와 서버 역할을 동시에 수행하는 P2P(Peer-to-Peer) 구조의 메커니즘을 오해하면, 팀 프로젝트 실습 시 동기화 타이밍을 놓치거나 보안에 취약한 구조를 설계하게 됩니다. 각 모델의 한계와 특성을 정확히 파악해야 프로젝트 규모에 맞는 올바른 서버 모델을 선택할 수 있습니다.

 


2. 핵심 서버 모델 3종 비교 및 P2P 모델의 이해

2-1. P2P(Peer-to-Peer) 모델의 정의와 구조적 특징

P2P 모델은 중앙에서 진실을 통제하는 절대적인 서버가 존재하지 않습니다. 네트워크에 참여하는 모든 컴퓨터(Peer)가 클라이언트인 동시에 서버의 역할을 나누어 가집니다.

 

  • 동등한 권한: 모든 참가자가 동일한 권한을 가지므로, 특정 플레이어의 입력이 다른 모든 참가자에게 직접 전달되어야 합니다.
  • 네트워크 연결의 기하급수적 증가: 참가자가 N명일 때, 각 참가자는 자신을 제외한 N-1명과 연결되어야 하므로 전체 연결 수는 N(N-1)/2로 증가합니다. 즉, 플레이어가 조금만 늘어나도 클라이언트의 네트워크 연산 과부하가 심해집니다.
  • 보안 취약성: 중앙에서 패킷의 유효성을 검증하는 권한(Authority) 주체가 없기 때문에, 클라이언트 메모리 변조나 핵(Hack) 행위를 차단하기가 매우 어렵습니다.

 

2-2. Worked Example: P2P 환경에서 데이터 전파 흐름

P2P 구조에서 데이터가 동기화되는 구체적인 제어 흐름과 한계를 시뮬레이션 예제를 통해 확인해 보겠습니다.

Example 1: P2P 환경에서 플레이어 A의 이동 데이터 전파 흐름

중앙 서버가 없는 P2P 환경에서 플레이어 A가 앞으로 이동했을 때, 이 데이터가 다른 플레이어들에게 도달하는 단계별 과정입니다.

 

  1. 로컬 입력 및 자체 연산: 플레이어 A의 컴퓨터에서 이동 키 입력을 받아 로컬 캐릭터의 위치를 업데이트합니다.
  2. 모든 피어(Peer)에게 패킷 직접 발송: 플레이어 A는 중앙 서버를 거치지 않고, 현재 세션에 접속 중인 플레이어 B, 플레이어 C의 IP로 각각 이동 결과 패킷을 직접 전송합니다.
  3. 수신 및 독단적 동기화: 패킷을 받은 플레이어 B와 C는 별도의 검증 단계 없이 플레이어 A가 보낸 위치 값대로 화면에 렌더링합니다. 이 과정에서 한 피어의 데이터가 변조되어도 검증할 주체가 없습니다.

 

2-3. 네트워크 서버 모델 간 핵심 비교

언리얼 엔진 멀티플레이어 개발에서 선택할 수 있는 서버 모델들과 P2P의 차이점을 요약하면 다음과 같습니다.

서버 모델 권한(Authority)의 주체 호스팅 비용 비주얼/렌더링 수행 주요 단점
P2P 모든 참가자가 분산 소유 없음 모든 참가자가 수행 참여자 증가 시 과부하, 핵에 매우 취약
리슨 서버 방장(Host) 1명 없음 방장만 수행, 게스트는 UI만 수행 방장의 환경에 종속, 호스트 변조 위험
데디케이티드 서버 독립된 독립 서버 프로세스 높음 아예 수행하지 않음 (Headless) 서버 유지 및 호스팅 비용 발생

 


3. 리슨 서버 (Listen Server) 깊이보기

3-1. 리슨 서버(Listen Server)란?

리슨 서버는 멀티플레이 세션을 플레이하는 참가자 중 한 명(Host/방장)이 서버 역할과 클라이언트 역할을 동시에 수행하는 서버 구조입니다.

  • NetMode: NM_ListenServer
  • 특징:
    • 별도의 독립 서버(Dedicated Server)를 배포 및 호스팅할 비용 부담이 없어 캐주얼 협동 게임이나 소규모 멀티플레이에 주로 사용됩니다.
    • 호스트(방장)의 PC에서는 게임 화면이 렌더링되고 UI가 출력되며, 동시에 다른 원격 클라이언트(NM_Client)의 접속을 수신 및 통제(Authority)합니다.
    • 호스트 PC의 성능이나 네트워크 환경에 세션 전체의 게임 품질이 종속되며, 방장이 게임을 종료하면 세션 전체가 닫히는 단점이 있습니다.

 

3-2. ?Listen 플래그의 역할

언리얼 엔진의 일반 게임 클라이언트 빌드(.exe)나 에디터 프로세스는 기본적으로 싱글 플레이어 모드(NM_Standalone)로 동작하도록 설계되어 있습니다.

?Listen은 "이 레벨을 전환할 때 단순 싱글 플레이로 열지 말고, 네트워크 포트를 개방하여 외부 클라이언트의 접속을 받는 리슨 서버 모드로 전환하라"고 엔진에 전달하는 URL 옵션 플래그입니다.

  • 명령어 예시:
    • 콘솔 명령어: open Map_Chatting?Listen
    • C++ / 블루프린트 레벨 이동: UGameplayStatics::OpenLevel(GetWorld(), "Map_Chatting", true, "Listen");
    • 서버 트래블: GetWorld()->ServerTravel("/Game/Maps/Map_Chatting?Listen");

 

3-3. 리슨 서버 구동 및 초기화 내부 흐름

?Listen 인자와 함께 레벨 로딩이 요청되면 언리얼 엔진 내부에서는 다음 단계로 진행됩니다.

  1. URL 파싱 및 옵션 검출: 엔진의 UEngine::Browse() 또는 UEngine::LoadMap() 과정에서 전환하려는 맵 URL 문자열 내에 Listen 파라미터가 포함되어 있는지 검사합니다.
  2. NetMode 설정 (NM_ListenServer): ?Listen 옵션이 확인되면 월드의 네트워크 모드가 NM_Standalone에서 NM_ListenServer로 변경됩니다.
  3. NetDriver 생성 및 포트 개방:엔진 내부에서 네트워크 통신을 전담하는 UNetDriver 객체를 생성합니다. 기본 네트워크 포트(예: 7777)를 바인딩하고 소켓 수신 대기(Listen Socket) 상태에 들어갑니다.
  4. 월드 및 프레임워크 에셋 생성: 해당 레벨의 GameModeBase와 GameState가 서버 권한으로 스폰됩니다. 호스트 플레이어 자신을 위한 PlayerController와 로컬 폰(Pawn)이 생성되며, 로컬 화면에 UI/UMG가 렌더링됩니다.
  5. 클라이언트 접속 수락: 원격 클라이언트가 호스트의 IP 및 포트로 접속을 시도하면, UNetDriver가 이를 수락하여 해당 접속을 나타내는 UNetConnection을 생성하고 레벨 복제 및 게임 입장을 진행합니다.

4. 데디케이티드 서버 (Dedicated Server) 및 구동 메커니즘

데디케이티드 서버 실행 및 접속 흐름: 왜 이것이 중요한가

언리얼 엔진 멀티플레이어 팀 프로젝트를 진행할 때, 많은 개발자가 데디케이티드 서버 빌드와 리슨 서버 구동 방식의 인자값 설정을 혼동하여 접속 오류를 겪습니다. 특히 서버 프로세스가 백그라운드로 실행되거나 특정 맵을 열 때 사용하는 명령줄 인자(?Listen)의 아키텍처적 유효성을 정확히 알아야만 타겟 빌드 환경에 맞는 멀티플레이어 인프라를 올바르게 구축할 수 있습니다.

 

4-1. 데디케이티드 서버 vs 리슨 서버 비교

구분 리슨 서버 (Listen Server) 데디케이티드 서버 (Dedicated Server)
NetMode NM_ListenServer NM_DedicatedServer
실행 주체 일반 게임 클라이언트 빌드 서버 전용 빌드 (Server.exe) 또는 -server
?Listen 인자 필요 여부 필수 (open LevelName?Listen) 불필요 (open LevelName 만으로 충분)
구동 메커니즘 싱글 바이너리로 시작하여 ?Listen을 확인한 뒤 포트를 개방 실행 시점부터 Headless(무렌더링) 전용 서버 프로세스로 인식되어 자동으로 포트 개방
UI / 렌더링 호스트 화면 렌더링 및 UI 수행 수행하지 않음 (Headless 상태로 연산만 수행)

 

4-2. 데디케이티드 서버 vs 리슨 서버의 엔진 초기화 메커니즘

언리얼 엔진은 실행 시점에 결정되는 NetMode에 따라 NetDriver를 초기화하고 네트워크 포트(기본 7777)를 개방하는 주체가 완전히 달라집니다.

 

  • 데디케이티드 서버 (NM_DedicatedServer): 그래픽 연산(UI, 렌더링)이 없는 Headless 상태로 구동되는 독립 프로세스입니다. 이 모드로 엔진이 켜지면, 명령줄이나 URL 변수에 ?Listen 플래그가 없더라도 엔진 소스 레벨에서 자동으로 NetDriver를 생성하고 수신 대기(Listen) 포트를 활성화합니다. 서버로서의 본업이 곧 접속 대기이기 때문입니다.
  • 리슨 서버 (NM_ListenServer): 일반 게임 클라이언트(NM_Standalone 혹은 NM_Client)로 실행된 프로세스가 특정 맵을 열 때 ?Listen 인자를 수신하여 구동됩니다. 즉, "클라이언트 화면을 그리면서, 동시에 포트를 열어 다른 유저의 접속을 받는 서버 역할도 겸임하겠다"는 수동 활성화 스위치 역할을 합니다.

 

4-3. ?Listen 플래그의 생략이 아키텍처에 미치는 영향

데디케이티드 서버 빌드(-server 명령줄 스위치 사용 또는 Server.exe 실행 파일) 환경에서는 레벨 전환(ServerTravel)이나 오픈 URL에 ?Listen을 붙이지 않는 것이 표준 아키텍처 설계입니다.

  • 데디케이티드 서버 실행 스크립트 예시:
  • ChatXServer.exe /Game/Maps/Chatting -log (자동으로 NetDriver 포트 개방)

만약 태생이 데디케이티드 서버인 프로세스에 불필요하게 ?Listen 플래그를 누적하여 전달하면, 내부적으로 NM_DedicatedServer인 모드가 오작동하거나 불필요한 URL 파싱 오버헤드가 발생할 수 있으므로 명확히 분리하여 생략해야 합니다.

 

4-4. Worked Example: 서버 모델별 레벨 오픈 처리 절차

이제 맵 오픈 및 서버 travel 프로시저에서 각 서버 모델별 명령줄 인자가 어떻게 적용되는지 구체적인 절차를 확인해 보겠습니다.

 

Example 1: C++ 환경에서 데디케이티드 서버와 리슨 서버의 레벨 오픈 처리 절차

  1. 데디케이티드 서버의 레벨 오픈 (플래그 생략): UWorld::ServerTravel 사용
    GetWorld()->ServerTravel(TEXT("/Game/Maps/Chatting"));
    
    (엔진이 NM_DedicatedServer임을 감지하여 자동으로 NetDriver 소켓 활성화)
  2. 서버 전용 빌드에서는 단순 맵 네임만으로 포트를 엽니다.
  3. 클라이언트에서 리슨 서버로의 전환 (플래그 명시): URL 매개변수 바인딩.
    GetWorld()->ServerTravel(TEXT("/Game/Maps/Chatting?Listen"));
    
    (엔진이 URL을 파싱하여 NetMode를 NM_ListenServer로 강제 전환 후 소켓 개방)
  4. 일반 클라이언트가 방장이 되어 다른 클라이언트를 받으려면 반드시 ?Listen을 명시합니다.

5. 데디케이티드 서버 내부 통신 및 접속 프로시저

언리얼 엔진의 데디케이티드 서버에서 NetDriver와 NetConnection이 생성되고, 클라이언트가 핸드셰이크(Handshake) 과정을 거쳐 게임 플레이 세션에 최종 진입하는 단계별 내부 흐름을 상세히 설명합니다.

5-1. 초기화 단계: NetDriver 생성 및 포트 바인딩

  • 서버 프로세스 구동: 데디케이티드 서버가 실행되면 엔진은 NM_DedicatedServer 모드로 시작합니다.
  • UNetDriver 생성: GEngine->Init() 과정에서 Engine.ini에 정의된 넷 드라이버 클래스(기본적으로 IpNetDriver)를 인스턴스화합니다.
  • Socket Listen: UNetDriver::InitListen()이 호출되어 지정된 포트(기본 UDP 7777)에 소켓을 바인딩하고, 클라이언트 패킷 수신 대기 상태로 진입합니다.

 

5-2. 클라이언트 접속 요청 및 초기 핸드셰이크

  • 클라이언트 접속 시도: 클라이언트가 Open <Server_IP>:7777 명령을 실행하면, 서버 IP로 소켓 패킷을 전송합니다.
  • 핸드셰이크 패킷 교환 (Stateless Handshake):
    • Challenge 요청: 클라이언트가 서버에 접속 요청 패킷을 보냅니다.
    • Challenge 응답: 서버는 DOS 공격 방지를 위해 쿠키(Cookie) 형태의 Challenge 값을 클라이언트에 되돌려줍니다.
    • Challenge 확인: 클라이언트가 수신한 Challenge 값을 포함하여 다시 응답을 보내면 서버가 이를 검증합니다.

 

5-3. NetConnection 및 PendingNetGame 생성

  • UNetConnection 생성: 핸드셰이크가 정상 검증되면, 서버의 UNetDriver는 접속한 클라이언트 전용 통신 통로인 UNetConnection(UIpConnection) 객체를 인스턴스화하고 관리 목록(ClientConnections)에 추가합니다.
  • 클라이언트 측 UPendingNetGame: 클라이언트 측에서는 레벨 로딩 및 서버 연결 상태를 관리하는 UPendingNetGame 객체가 생성되어 서버와의 네트워크 동기화를 준비합니다.

 

5-4. 로그인 및 권한 검증 (Login Protocol)

  • NMT_Hello & NMT_Challenge: 클라이언트와 서버가 서로의 버전을 확인합니다.
  • NMT_Login (로그인 요청): 클라이언트가 플레이어의 UniqueId, 닉네임, 옵션 문자열 등을 담아 서버에 로그인을 요청합니다.
  • AGameModeBase::PreLogin:
    • 서버의 게임모드에서 접속 허용 여부를 검사합니다 (최대 인원 초과, 밴 여부 등).
    • 승인 거부 시 접속을 종료(NMT_Failure)합니다.
  • AGameModeBase::PostLogin:
    • 접속이 최종 승인되면 NMT_Join 패킷을 전송하여 레벨 전환 및 로딩을 지시합니다.

 

5-5. 레벨 로딩 및 PlayerController / Pawn 생성

  • Map Loading: 클라이언트가 서버와 동일한 레벨을 로컬에 로딩합니다.
  • PlayerController 생성: 서버는 승인된 클라이언트를 위해 APlayerController를 생성하고, 해당 UNetConnection에 소유권(Ownership)을 바인딩합니다.
  • Pawn Spawning 및 Possession: 서버의 AGameModeBase::RestartPlayer()가 호출되어 캐릭터(Pawn)를 스폰하고 PlayerController가 이를 빙의(Possess)합니다.
  • Network Replication: 서버의 PlayerController 및 Pawn 정보가 클라이언트로 복제(Replication)되어 클라이언트 화면에 자신의 캐릭터와 게임 월드가 정상 구동됩니다.

 

5-6. 데디케이티드 서버 접속 흐름 요약표

단계 주체 (Actor/Object) 핵심 역할 / 함수
NetDriver 초기화 UIpNetDriver InitListen() 호출을 통한 UDP 7777 포트 바인딩
Stateless Handshake Stateless Handler DOS 방지용 Challenge/Cookie 검증 및 소켓 교환
Connection 생성 UIpConnection 서버-클라이언트 1:1 전용 네트워크 채널 수립
Game Session 승인 AGameModeBase PreLogin() (접속 제어) 및 PostLogin() (세션 승인)
Actor 바인딩 APlayerController NetConnection에 소유권을 부여하고 Pawn 빙의 및 액터 복제 시작