Unreal Engine 5에서 Steam Online Subsystem이 Lobby에 Join하지 못하는 이유
핵심 요약
본 글은 Unreal Engine 5에서 Steam Online Subsystem을 사용할 때 발생하는 네트워크 드라이버 연결 실패 해결 방안을 다룹니다. NetDriverDefinitions 설정 오류로 인해 Steam Lobby Join이 실패하는 원인과 이를 해결하기 위한 현대적인 SteamSockets 설정법을 보여줍니다. 아울러 AppID 480 Sandbox 테스트 중 발생하는 Lobby 오염 문제에 대응할 C++ 쿼리 필터 구성 방법 및 개발 효율성을 높여주는 전용 게임 Backend 도입 방안을 제시합니다.
Unreal Engine 5 프로젝트를 패키징하고, 두 개의 서로 다른 Steam 계정을 실행한 뒤, 클라이언트를 구동하여 세션 검색 결과에 자신이 호스팅한 Lobby가 표시되는 것을 확인합니다. "Join"을 누르고 5초간 기다리지만... 아무런 반응이 없습니다. 화면이 멈추고, 콘솔 로그에는 일반적인 Socket 경고가 출력되며, 메인 메뉴로 다시 튕겨 나갑니다.
만약 Steam Online Subsystem이 Lobby 연결을 Join하지 못하고 있다면, 여러분은 Unreal Engine 5에서 가장 골치 아픈 네트워크 설정 문제 중 하나를 겪고 있는 것입니다. 이는 Steam Matchmaking API가 Lobby를 올바르게 등록했음에도 불구하고, 엔진의 Network Driver가 로우레벨 연결 Handshake를 완료하지 못해 발생하는 무응답 실패(silent failure) 현상입니다. 이 가이드에서는 근본적인 Network Driver 아키텍처를 분석하고, 흔히 발생하는 DefaultEngine.ini 설정 불일치를 식별하며, AppID 480 Sandbox의 제한 사항을 해결하고, Unreal Engine 5에서 견고한 Steam 연결 파이프라인을 설정하는 방법을 살펴보겠습니다.
Epic Games와 Valve의 Netcode 아키텍처 이해하기
Join이 실패하는 이유를 이해하려면, Unreal Engine의 Netcode 레이어가 어떻게 Steam Lobby 검색 결과를 네트워크 연결로 변환하는지 파악해야 합니다. 게임 코드에서 JoinSession을 호출하면, OnlineSubsystemSteam 세션 인터페이스가 Lobby를 연결 문자열(connection string)로 해석합니다. Steam의 경우, 이 연결 문자열은 호스트의 고유 Steam ID를 나타내는 steam.STEAM_ID(예: steam.76561198000000000) 형식으로 구성됩니다.
Unreal Engine의 Network Driver 팩토리(GameNetDriver)는 이 연결 문자열을 수신하고 스키마 접두사(steam.)를 파싱합니다. 그런 다음 DefaultEngine.ini 설정의 [/Script/Engine.GameEngine] 아래를 확인하여 Steam 연결을 처리하도록 구성된 클래스를 찾습니다. 만약 이 매핑이 누락되었거나 일치하지 않거나, 올바른 네트워크 연결 클래스가 로드되지 않았다면 엔진은 IpNetDriver로 폴백(fallback)합니다.
IpNetDriver는 Steam ID를 해석할 수 없습니다. 이 드라이버는 steam.76561198000000000을 일반적인 DNS 호스트 이름이나 IP 주소로 취급하려고 시도하다가, 해석에 실패하고 네트워크 타임아웃(timeout)을 발생시킵니다. 만약 Network Driver 설정이 잘못되었거나 연결 Handshake가 실패하면, 개발자들을 괴롭히는 Unreal Engine network driver timeouts과 동일한 Network Driver 타임아웃 오류를 마주하게 됩니다. NetDriver 정의가 연결 스키마와 어떻게 매칭되는지 이해하는 것이 이 설정을 바로잡는 첫 걸음입니다.
SteamSockets와 SteamNetDriver의 충돌
Unreal Engine 5에서 이러한 실패가 발생하는 가장 흔한 원인은 레거시 SteamNetDriver와 현대적인 SteamSockets 플러그인 간의 충돌입니다. 과거에 Unreal Engine은 Valve의 이전 P2P API를 기반으로 하는 레거시 SteamNetDriver를 사용했습니다. 최신 UE5 프로젝트는 Valve의 Steam Networking Sockets API(DDoS 방어 및 라우팅 최적화를 위한 Steam Datagram Relay(SDR) 지원)를 활용하는 SteamSockets 플러그인을 사용합니다.
많은 개발자가 C++에서 프로젝트의 종속성 모듈에 "SteamSockets"를 추가하지만, 설정 파일에서 Network Driver 클래스 정의를 업데이트하는 것을 잊어버립니다. 반대로, 엔진은 Steam Sockets를 초기화하려고 시도하는 중에 레거시 드라이버 클래스를 지정하기도 합니다. 각 접근 방식에 맞는 올바른 설정을 살펴보겠습니다.
레거시 SteamNetDriver 설정
레거시 OnlineSubsystemSteam Network Driver를 사용하는 경우, DefaultEngine.ini에서 GameNetDriver를 레거시 클래스에 매핑해야 합니다.
[/Script/Engine.GameEngine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"
또한, DefaultEngine.ini의 [OnlineSubsystemSteam] 카테고리 아래에 bUseSteamNetworking=true가 설정되어 있는지 확인하십시오.
현대적인 SteamSockets 설정
.uproject 파일에서 SteamSockets 플러그인을 활성화한 경우, 드라이버 및 연결 클래스 이름을 변경해야 합니다. 클래스 이름과 섹션 헤더의 변경 사항에 유의하십시오.
[/Script/Engine.GameEngine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="SteamSockets.SteamSocketsNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
[/Script/SteamSockets.SteamSocketsNetDriver]
NetConnectionClassName="SteamSockets.SteamSocketsNetConnection"
Build.cs에 SteamSockets를 포함하도록 구성했더라도 INI 파일에서 레거시 OnlineSubsystemSteam.SteamNetDriver 설정을 사용하면, 엔진이 잘못된 네트워크 연결 클래스를 초기화합니다. 이로 인해 클라이언트의 네트워크 Handshake가 호스트의 Steam ID를 확인하지 못하게 되어, Join 시도가 중단되고 타임아웃이 발생합니다.
빌드 종속성 확인하기
프로젝트의 .Build.cs가 선택한 설정과 일치하는지 확인하십시오. 예를 들어, 현대적인 Steam Sockets 구현을 목표로 하는 경우, 메인 게임 모듈의 Build.cs 파일에 OnlineSubsystemSteam과 SteamSockets를 모두 명시적으로 선언해야 합니다.
using UnrealBuildTool;
public class MyTPSGame : ModuleRules
{
public MyTPSGame(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
"InputCore",
"EnhancedInput",
"OnlineSubsystem",
"OnlineSubsystemUtils",
"OnlineSubsystemSteam",
"SteamSockets",
"UMG",
"Slate",
"SlateCore"
});
}
}
Steam AppID 480 Sandbox 함정
설정이 올바른데도 여전히 Join할 수 없다면, Steam Dev AppID 480 Sandbox 제한의 함정에 빠졌을 가능성이 큽니다. 기본적으로 개발자들은 Valve에 상용 애플리케이션을 등록하지 않고 Steam 연동을 테스트하기 위해 AppID 480(Spacewar)을 사용합니다. 하지만 AppID 480은 전 세계의 수많은 개발자가 공유하여 사용합니다.
이로 인해 두 가지 고유한 문제가 발생합니다.
- Lobby 오염: AppID 480에서 세션을 검색하면 다른 개발자의 게임에서 호스팅한 Lobby들까지 함께 반환됩니다. 클라이언트가 무작위 Lobby에 Join하려고 시도하면, 게임 버전이나 빌드 ID 불일치로 인해 실패하게 됩니다.
- 지역 고립: Steam Lobby는 핑(ping)을 낮게 유지하기 위해 기본적으로 지역별 가시성(regional visibility)을 가집니다. 다른 지역에 있는 팀원(예: 한 명은 뉴욕, 다른 한 명은 런던)과 함께 테스트하는 경우, 검색 거리 필터(distance filter)를 수정하지 않으면 표준 세션 쿼리로 서로를 찾거나 연결할 수 없습니다.
이러한 제한을 우회하려면 C++에서 세션 검색 설정을 명시적으로 구성하여 전 세계 거리 필터(worldwide distance filter)와 사용자 정의 쿼리 매개변수를 사용해야 합니다.
관련 없는 AppID 480 트래픽을 필터링하면서 글로벌 Steam Lobby를 대상으로 하는 사용자 정의 C++ 세션 검색 쿼리를 작성하는 방법은 다음과 같습니다.
#include "OnlineSubsystem.h"
#include "Interfaces/OnlineSessionInterface.h"
#include "OnlineSessionSettings.h"
void UMultiplayerSessionSubsystem::FindSteamLobbies(int32 MaxResults)
{
IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get();
if (!Subsystem)
{
UE_LOG(LogTemp, Warning, TEXT("Failed to get OnlineSubsystem."));
return;
}
IOnlineSessionPtr SessionInterface = Subsystem->GetSessionInterface();
if (!SessionInterface.IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("Session interface is invalid."));
return;
}
// Allocate a new session search configuration
SessionSearch = MakeShareable(new FOnlineSessionSearch());
SessionSearch->MaxSearchResults = MaxResults;
SessionSearch->bIsLanQuery = false;
// Use presence to ensure we look for Steam lobbies rather than dedicated servers
SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);
SessionSearch->QuerySettings.Set(SEARCH_LOBBIES, true, EOnlineComparisonOp::Equals);
// CRITICAL: Set Steam Lobby Search Distance Filter
// 0 = Close, 1 = Default, 2 = Far, 3 = Worldwide
// Worldwide is necessary if testing across different regions on AppID 480
SessionSearch->QuerySettings.Set(SEARCH_LOBBY_SEARCH_DISTANCE_FILTER, 3, EOnlineComparisonOp::Equals);
// Apply a unique game identifier key to filter out other developers' Spacewar lobbies
// Replace "MY_UNIQUE_GAME_ID_KEY" with a unique string specific to your prototype
SessionSearch->QuerySettings.Set(TEXT("GAME_VERSION_KEY"), FString("MyTPSGame_v1.0.4"), EOnlineComparisonOp::Equals);
// Bind callback to handle search completion
OnFindSessionsCompleteDelegateHandle = SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(
OnFindSessionsCompleteDelegate,
FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)
);
ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
if (LocalPlayer)
{
SessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), SessionSearch.ToSharedRef());
}
}
호스팅 클라이언트에서 세션을 생성할 때, 동일한 사용자 정의 키(값 MyTPSGame_v1.0.4을 가진 GAME_VERSION_KEY)를 FOnlineSessionSettings::Settings 맵에 추가해야 합니다. 이렇게 하면 검색 쿼리가 내 게임의 Lobby들만 반환하여 AppID 480 오염 문제를 완전히 해결할 수 있습니다. 초기 Lobby 연결 단계를 성공적으로 넘기더라도, Join 직후의 게임플레이 상태가 깨지지 않도록 RPC가 Unreal Engine RPC replication desyncs 오류 없이 안정적으로 Replication되는지도 확인해야 합니다.
Port Bindings 및 단일 머신 테스트 충돌
단일 PC에서 두 개의 Steam 계정(예: Sandboxie를 사용하거나 커맨드 라인 스크립트로 두 번째 빌드 인스턴스를 실행하는 방식)을 사용하여 로컬로 Multiplayer를 테스트하는 경우, Port Binding 충돌이 발생할 확률이 높습니다. 호스팅을 시작할 때 Unreal Engine의 Steam 서브시스템은 로컬 게임 서버를 등록하려고 시도합니다. DefaultEngine.ini의 bInitServerOnClient=true 설정은 클라이언트가 Steamworks의 게임 서버 API를 초기화하도록 지시합니다.
동일한 컴퓨터의 두 인스턴스가 모두 기본 Steam Query Port(27015) 및 Game Port(7777)에 바인딩하려고 시도하면, 두 번째 인스턴스는 Socket을 열지 못합니다. 결과적으로 두 번째 클라이언트는 Lobby를 검색해서 찾아낼 수는 있지만, 들어오는 연결 Socket을 초기화할 수 없게 됩니다.
이를 해결하려면 다음 단계를 따르십시오.
- 두 번째 인스턴스의 Port 변경: 터미널 또는 배치 스크립트에서 두 번째 게임 인스턴스를 실행할 때, 명령줄 매개변수에
-port=7778을 추가하여 다른 Port에 강제로 바인딩되도록 설정합니다. - 올바른 Query Port 오프셋 지정:
[OnlineSubsystemSteam]항목에서 쿼리 및 게임 Port 설정을 확인하십시오.
동일한 로컬 네트워크에서 테스트하는 경우, 라우터의 방화벽이 해당 Port들의 UDP 트래픽을 차단하지 않는지 확인하십시오.[OnlineSubsystemSteam] bEnabled=true SteamDevAppId=480 bInitServerOnClient=true bUseSteamNetworking=true GameServerQueryPort=27015
현대적인 Game Backend를 통한 Matchmaking 문제 해결
Steam의 ini 설정, 거리 필터, AppID 제한 사항들과 수동으로 씨름하는 것은 인디 개발 팀에게 엄청난 시간 낭비입니다. 지역별 폴백, Matchmaking 규칙 및 안정적인 Network Driver Handshake를 갖춘 실제 서비스용 Lobby 시스템을 구축하는 데는 백엔드 엔지니어링에만 쉽게 4~6주가 소요될 수 있습니다.
이것이 바로 전용 게임 Backend가 필요한 이유입니다. horizOn은 게임 개발자를 위해 특별히 설계된 Backend-as-a-Service (BaaS)입니다. horizOn은 Steam 전용 Socket 라이브러리를 디버깅하거나 복잡한 C++ 래퍼 모듈을 작성하도록 강요하는 대신, Lobby, Matchmaking, 플레이어 세션을 관리할 수 있는 통합 SDK를 제공합니다.
Matchmaking 로직을 horizOn으로 이전하면 엔진 설정 파일의 Network Driver 매핑에 대해 걱정할 필요가 없습니다. 플레이어 세션은 글로벌 분산 서버 인프라를 통해 관리되므로, 즉각적인 Matchmaking과 디도스(DDoS) 방어가 적용된 Relay 서버를 기본적으로 제공받을 수 있습니다.
Unreal Engine Steam Multiplayer 개발 모범 사례
프로토타입 테스트를 넘어 확장 가능한 안정적인 Multiplayer 파이프라인을 구축하려면 다음 모범 사례를 따르십시오.
- 원활한 Steam Sockets 전환: 레거시 P2P 네트워킹 드라이버 설정은 가급적 피하십시오. 현대적인
SteamSocketsNetwork Driver에 바인딩하여 Steam Datagram Relay (SDR) 라우팅을 활용하고 NAT punch-through 실패를 방지하십시오. - 사용자 정의 필터링 키 적용: AppID 480에서 테스트할 때 세션 설정에 프로젝트 고유 식별자를 추가하십시오. 이를 통해 클라이언트가 Sandbox를 공유하는 다른 게임에 연결을 시도하는 것을 방지할 수 있습니다.
- NULL Subsystem 폴백에 대한 유연한 처리: Steam이 실행 중이지 않을 때 Netcode가
NULL서브시스템으로 폴백될 수 있도록 하십시오. 이를 통해 연결 로직을 손상시키지 않고 오프라인 LAN 테스트가 가능해집니다. - 세션 Join 타임아웃 최적화:
[ActiveNetDriver]아래의ConnectionTimeout을 최소 15.0초로 설정하십시오. Steam의 P2P Handshake는 특히 글로벌 SDR Relay를 경유해 라우팅될 때 완료되는 데 몇 초 정도 걸릴 수 있습니다.
결론
Unreal Engine 5에서 발생하는 Steam Lobby 연결 실패 문제를 해결하려면 빌드에 사용된 네트워크 플러그인과 엔진 설정을 일치시켜야 합니다. NetDriverDefinitions를 올바르게 매핑하고 Spacewar 검색 필터를 재정의(override)하면 플레이테스트를 위한 안정적인 연결을 구축할 수 있습니다.
백엔드 인프라 구축의 번거로움을 피하고 게임플레이 개발에만 전념하고 싶다면 전용 Backend와의 통합을 고려해 보십시오. Multiplayer 백엔드를 확장할 준비가 되셨나요? horizOn을 무료로 시작해 보거나 API docs를 확인해 보십시오.