콘텐츠로 이동

UE5 Iris 복제 시스템 완전 가이드

Iris는 UE 5.1에서 도입된 차세대 네트워크 복제 시스템으로, 기존 액터 기반 복제의 성능 한계를 극복하기 위해 설계되었습니다. 데이터 중심 아키텍처로 대규모 멀티플레이어(수백~수천 플레이어)에서 CPU 효율과 대역폭 효율을 크게 개선합니다.

기존 복제 시스템은 매 NetTick마다 모든 액터를 순회하며 더티 속성을 찾는 O(n) 구조였습니다. Iris는 변경 사항을 즉시 추적하는 ReplicationFragment 단위로 전환해 불필요한 순회를 제거하고, 패킷 직렬화를 워커 스레드로 분산합니다.


; DefaultEngine.ini
[/Script/Engine.NetworkSettings]
bIrisEnabled=True
; 또는 커맨드라인
-IrisEnabled=1
// 런타임에서 Iris 활성 여부 확인
#include "Iris/ReplicationSystem/ReplicationSystem.h"
bool IsIrisActive(UWorld* World)
{
UReplicationSubsystem* RepSub =
World->GetSubsystem<UReplicationSubsystem>();
return RepSub && RepSub->IsUsingIrisReplication();
}

빌드 모듈 의존성:

Build.cs
PublicDependencyModuleNames.AddRange(new[]
{
"IrisCore",
"NetCore",
"ReplicationSystemTestPlugin", // 테스트 전용 (선택)
});

기존 시스템과 Iris의 핵심 차이는 복제 데이터가 어디에 저장되는지입니다.

[기존 Legacy 복제]
UNetDriver
└─ UNetConnection (클라이언트 연결)
└─ UActorChannel (액터당 1채널)
└─ UPROPERTY dirty 비트 검사 (매 틱 O(n))
└─ FObjectReplicator → 직렬화 (게임 스레드만)
[Iris 복제]
UReplicationSystem
└─ FReplicationSystemInternal
├─ FNetRefHandleManager — 오브젝트 레지스트리
├─ FReplicationStateStorage — 복제 상태 버퍼 (변경 추적)
├─ FNetObjectFilteringInfoStorage — 관련성 필터
└─ FDataStreamManager — 멀티스레드 직렬화 파이프라인

주요 개선 포인트:

  • 복제 상태를 별도 스토리지에 저장 → 게임 오브젝트 수정 없이 복제 데이터 격리
  • 더티 추적이 쓰기 시점에 발생 → 읽기(매 틱 순회) 비용 제거
  • 직렬화를 워커 스레드로 분산 → 게임 스레드 부하 감소

ReplicationFragment는 Iris의 핵심 추상화로, 복제할 데이터 구조와 직렬화 방법을 묶은 단위입니다.

#include "Iris/ReplicationSystem/ReplicationFragment.h"
#include "Iris/ReplicationFragment/ReplicationFragmentUtil.h"
// 복제할 상태 구조체
USTRUCT()
struct FPlayerStateReplicationData
{
GENERATED_BODY()
UPROPERTY()
float Health = 100.f;
UPROPERTY()
float Shield = 0.f;
UPROPERTY()
int32 Score = 0;
UPROPERTY()
FVector_NetQuantize Position; // 정밀도 손실 없이 대역폭 절약
};
// PlayerState에서 Fragment 등록
void AMyPlayerState::RegisterReplicationFragments(
UE::Net::FFragmentRegistrationContext& Context,
UE::Net::EFragmentRegistrationFlags Flags)
{
Super::RegisterReplicationFragments(Context, Flags);
// 유틸리티 함수로 자동 등록 — UPROPERTY 기반 직렬화 사용
UE::Net::FReplicationFragmentUtil::CreateAndRegisterFragmentForObject(
this, Context, Flags);
}

커스텀 직렬화가 필요한 경우 FReplicationFragment를 직접 상속합니다:

// 커스텀 Fragment: 비트 패킹으로 대역폭 최적화
class FWeaponStateFragment : public UE::Net::FReplicationFragment
{
public:
FWeaponStateFragment(UE::Net::EReplicationFragmentTraits InTraits,
UObject* InOwner)
: FReplicationFragment(InTraits, InOwner)
{}
virtual void ReplicateChangedStateToConnections(
UE::Net::FReplicationStateTransmissionRecord& Record,
const UE::Net::FNetBitStreamWriter& Writer) override
{
// 직접 비트 스트림 쓰기
auto* WeaponActor = Cast<AWeapon>(GetOwner());
if (!WeaponActor) return;
Writer.WriteBits(WeaponActor->CurrentAmmo, 7); // 최대 127발 가정
Writer.WriteBits(WeaponActor->ReserveAmmo, 10);
Writer.WriteBool(WeaponActor->bIsReloading);
}
virtual void DeserializeAndApplyState(
UE::Net::FNetBitStreamReader& Reader,
const UE::Net::FReplicationStateHeader& Header) override
{
auto* WeaponActor = Cast<AWeapon>(GetOwner());
if (!WeaponActor) return;
WeaponActor->CurrentAmmo = Reader.ReadBits(7);
WeaponActor->ReserveAmmo = Reader.ReadBits(10);
WeaponActor->bIsReloading = Reader.ReadBool();
}
};

4. NetHandle — 복제 오브젝트 참조

섹션 제목: “4. NetHandle — 복제 오브젝트 참조”

FNetRefHandle(이전 명칭: FNetHandle)은 Iris에서 복제 가능한 오브젝트를 고유하게 식별하는 토큰입니다. 포인터 대신 핸들을 사용해 오브젝트 소멸 후에도 안전하게 참조할 수 있습니다.

#include "Iris/ReplicationSystem/NetRefHandle.h"
#include "Iris/ReplicationSystem/ReplicationSystem.h"
// NetRefHandle 획득 및 우선순위 제어
void AMyGameMode::BoostPlayerReplication(APlayerState* Player)
{
UReplicationSystem* RepSystem =
GetWorld()->GetSubsystem<UReplicationSubsystem>()
->GetReplicationSystem(0);
if (!RepSystem) return;
// 액터로부터 핸들 획득
UE::Net::FNetRefHandle Handle =
UE::Net::FNetRefHandleManager::GetNetRefHandleFromObject(Player);
if (Handle.IsValid())
{
// 복제 우선순위 상향 (기본 1.0, 높을수록 먼저 복제)
RepSystem->SetReplicationPriority(Handle, 5.f);
// 특정 커넥션에서 강제 즉시 복제
// (예: 플레이어 리스폰 시 중요 초기 상태 전송)
RepSystem->ForceNetUpdate(Handle);
}
}
// 핸들 유효성 검사 패턴
void AMySystem::SafeHandleOperation(UE::Net::FNetRefHandle Handle)
{
if (!Handle.IsValid())
{
UE_LOG(LogNet, Warning, TEXT("유효하지 않은 NetRefHandle"));
return;
}
// IsStatic() — 정적 오브젝트 (레벨 배치, 파괴 안 됨)
// IsDynamic() — 동적 오브젝트 (스폰/파괴 가능)
if (Handle.IsDynamic())
{
// 동적 오브젝트 처리
}
}

FilterProfile은 “어떤 오브젝트를 어떤 커넥션에 복제할지”를 결정합니다. 기존 IsNetRelevantFor의 역할을 데이터 중심으로 재구현한 것입니다.

#include "Iris/ReplicationSystem/NetObjectFilter.h"
#include "Iris/ReplicationSystem/NetObjectFilterDefinitions.h"
// 팀 기반 커스텀 필터 구현
UCLASS()
class UTeamRelevancyFilter : public UNetObjectFilter
{
GENERATED_BODY()
public:
virtual void PreFilter(FNetObjectFilteringParams& Params) override
{
// 필터링 전 캐시 데이터 준비 (성능 최적화)
const int32 ConnectionId = Params.ConnectionHandle.GetId();
CachedTeamId[ConnectionId] = GetPlayerTeamId(ConnectionId);
}
virtual void Filter(FNetObjectFilteringParams& Params) override
{
const int32 ConnectionId = Params.ConnectionHandle.GetId();
const int32 ObjectTeam = GetObjectTeam(Params.Handle);
const int32 ViewerTeam = CachedTeamId[ConnectionId];
if (ObjectTeam == viewerTeam)
{
// 같은 팀: 모든 속성 복제
Params.OutAllowedObjects.SetBit(Params.ObjectIndex);
}
else
{
// 적 팀: 위치와 팀 색상만 복제 (스텔스 방지)
// FilterProfile에 등록된 "EnemyLOD" 프로파일 사용
}
}
private:
TMap<int32, int32> CachedTeamId;
};
// 필터 등록 (GameMode BeginPlay)
void AMyGameMode::InitIrisFilters()
{
UReplicationSystem* RepSystem = GetReplicationSystem();
if (!RepSystem) return;
// 필터 등록
UNetObjectFilterDefinitions* FilterDefs =
RepSystem->GetNetObjectFilterDefinitions();
FilterDefs->RegisterFilter<UTeamRelevancyFilter>(
FName("TeamRelevancyFilter"));
// 모든 플레이어 커넥션에 필터 적용
for (const FNetConnectionInfo& Conn : ActiveConnections)
{
RepSystem->SetFilter(
Conn.Handle,
FName("TeamRelevancyFilter"));
}
}

거리 기반 내장 필터 활용:

[/Script/IrisCore.ReplicationSystemConfig]
// 내장 GridFilter: 공간 분할 기반 관련성 (가장 성능 효율적)
// DefaultEngine.ini에서 설정
// FilterDefinitions=(FilterName="GridFilter", FilterClass=/Script/IrisCore.UNetObjectGridFilter)
// GridFilterConfig=(CellSize=10000, GridCenterMode=ActiveConnectionsCenter)

6. 복제 조건과 우선순위 세부 제어

섹션 제목: “6. 복제 조건과 우선순위 세부 제어”
#include "Net/Core/NetBitArray.h"
#include "Iris/ReplicationSystem/ReplicationCondition.h"
UCLASS()
class AMyCharacter : public ACharacter
{
GENERATED_BODY()
// 기존 DOREPLIFETIME 조건 — Iris에서도 완전 호환
UPROPERTY(Replicated)
float Health;
UPROPERTY(ReplicatedUsing = OnRep_Shield)
float Shield;
UPROPERTY(Replicated)
FVector_NetQuantize ServerLocation;
public:
virtual void GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const override
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// Iris는 이 함수를 자동으로 읽어 Fragment 생성
DOREPLIFETIME(AMyCharacter, Health);
// 소유자에게만 (클라이언트 예측 데이터)
DOREPLIFETIME_CONDITION(AMyCharacter, Shield,
COND_OwnerOnly);
// 비소유자에게만 (다른 플레이어 위치)
DOREPLIFETIME_CONDITION(AMyCharacter, ServerLocation,
COND_SkipOwner);
}
UFUNCTION()
void OnRep_Shield()
{
// 실드 변경 UI 업데이트
UpdateShieldBar(Shield);
}
};

복제 우선순위 동적 조정:

// 플레이어 시야 내 오브젝트 우선순위 상향
void AMyGameState::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
UReplicationSystem* RepSystem = GetReplicationSystem();
if (!RepSystem) return;
for (APlayerController* PC : PlayerControllers)
{
FVector ViewLoc;
FRotator ViewRot;
PC->GetPlayerViewPoint(ViewLoc, ViewRot);
// 시야 방향의 오브젝트 우선순위 높이기
TArray<AActor*> VisibleActors;
GetActorsInCone(ViewLoc, ViewRot.Vector(), 45.f, 5000.f, VisibleActors);
for (AActor* Actor : VisibleActors)
{
UE::Net::FNetRefHandle Handle =
UE::Net::FNetRefHandleManager::GetNetRefHandleFromObject(Actor);
if (Handle.IsValid())
RepSystem->SetReplicationPriority(Handle, 3.f);
}
}
}

콘솔 명령:
stat IrisReplication — Iris 복제 시간 통계
stat NetReplication — 전체 복제 통계 비교
net.Iris.PrintStats — 연결별 복제 오브젝트 수/대역폭
net.Iris.LogFragments 1 — Fragment 변경 로그
iris.debug.PrintFilter — 필터 현황 출력
메모리 분석:
net.Iris.DumpReplicationState <ActorName>
→ 해당 액터의 복제 상태 버퍼 덤프
// C++에서 Iris 통계 수집
#if !UE_BUILD_SHIPPING
void AMyGameMode::LogIrisStats()
{
UReplicationSubsystem* Sub =
GetWorld()->GetSubsystem<UReplicationSubsystem>();
if (!Sub) return;
UReplicationSystem* RepSys = Sub->GetReplicationSystem(0);
if (!RepSys) return;
// 전체 복제 오브젝트 수
uint32 TotalObjects = RepSys->GetNetRefHandleManager()
.GetNumReplicatedObjects();
UE_LOG(LogNet, Log, TEXT("Iris 복제 오브젝트 수: %u"), TotalObjects);
// 커넥션별 전송 대역폭 (디버그 빌드에서만)
RepSys->ForEachConnection([](const FNetConnectionInfo& Conn)
{
UE_LOG(LogNet, Log, TEXT("Connection %u: %.1f KB/s"),
Conn.Handle.GetId(),
Conn.OutBandwidth / 1024.f);
});
}
#endif

기존 프로젝트에서 Iris로 전환할 때 권장 순서입니다.

1단계: 활성화 테스트 (코드 변경 없음)
bIrisEnabled=True 설정 후 기존 복제 코드 그대로 실행
→ Iris는 GetLifetimeReplicatedProps를 자동으로 읽어 호환
2단계: 성능 측정
stat IrisReplication vs stat NetReplication 비교
병목 액터 식별 (액터 수가 많거나 틱 복제 비중이 높은 것)
3단계: Fragment 커스터마이징 (선택적)
병목 액터에 RegisterReplicationFragments 오버라이드
→ 비트 패킹, 양자화 등 직렬화 최적화
4단계: FilterProfile 도입
IsNetRelevantFor 로직을 UNetObjectFilter로 재구현
GridFilter 내장 필터 활용 (공간 분할 관련성)
5단계: 복제 우선순위 튜닝
시야/거리 기반 SetReplicationPriority 동적 조정

주의사항:

// Iris 전환 시 알려진 비호환 사항 (UE 5.3 기준)
// 1. OnRep 콜백 타이밍이 미세하게 달라질 수 있음
// → 프레임 단위가 아니라 패킷 수신 즉시 호출
// 2. bAlwaysRelevant = true 액터는 Iris에서도 동작하나
// FilterProfile 설정이 무시될 수 있음 → 명시적 필터 등록 권장
// 3. NetDormancy는 Iris에서 다르게 처리됨
// FlushNetDormancy() → Iris에서는 ForceNetUpdate() 사용
// 4. 커스텀 UNetConnection 서브클래스는 별도 마이그레이션 필요

Iris는 UE 5.1+ 대규모 멀티플레이어 프로젝트에서 유의미한 성능 향상을 제공합니다. 기존 GetLifetimeReplicatedProps 기반 코드와 대부분 호환되므로 bIrisEnabled=True로 활성화 후 stat IrisReplication으로 성능을 측정하는 것부터 시작하세요. 복제 병목 액터는 RegisterReplicationFragments로 직렬화를 커스터마이징하고, UNetObjectFilter로 불필요한 복제를 차단해 대역폭을 절감하세요. 내장 GridFilter는 별도 코드 없이 공간 분할 기반 관련성을 처리해 주므로 대규모 오픈 월드에서 가장 먼저 적용할 만한 최적화입니다.