[Project Arc] 서버와 클라이언트가 각각 생성될 때 Authority 관련 디버깅
리슨 서버/클라이언트 환경에서 Replicate 해야하는 인스턴스가 클라이언트에서도 생성되는 문제가 발생했습니다.
[Project Arc] 서버와 클라이언트가 각각 생성될 때 Authority 관련 디버깅
리슨 서버/클라이언트 환경에서 Replicate 해야하는 인스턴스가 클라이언트에서도 생성되는 문제가 발생했습니다. 특히 bReplicates 를 켜지 않았는데도 클라이언트에서 액터가 보이고, HasAuthority() 로 분기를 나눴다고 생각했는데도 여전히 중복 생성이 일어나는 상황이라, 네트워크 권한(Authority)와 NetMode 개념을 다시 정리할 필요가 있었습니다.
이번 글에서는 ACMProcedualMapGenerator + UCMMapGenerateLogicBaseComponent + ACMRoom 조합으로 작성된 맵 생성 로직에서 어떤 문제가 있었고, 이를 어떻게 디버깅하고 정리했는지 트러블슈팅 과정을 정리합니다.
1. 상황 정리
1.1. 관련 클래스 구조
ACMProcedualMapGenerator- 맵 전체를 생성하는 Actor
GenerateMap(int32 InSeed, UCMRoomDataDefinition* InMapData)에서 맵 생성 컴포넌트들을 동적으로 생성/교체MapGenerateLogicComponent:UCMMapGenerateLogicBaseComponent기반SpawnPositionSelectorComponent: 스폰 위치 선택 담당- 결과를
TMap<FCMRoomPosition, TObjectPtr<ACMRoom>> RoomMap에 저장
UCMMapGenerateLogicBaseComponent- 실제 방 배치, 연결, 보스/보물 방 결정 등 맵 생성 알고리즘 구현
- 주요 함수
ExecuteMapGenerationLogic: 전체 생성 파이프라인GenerateRoom: 좌표 기반 그래프 생성 및ACMRoom스폰PlaceWallsAndEntrances: 각 방 주변에 Entrance/Wall 스폰
ACMRoom- 한 개의 방을 나타내는 Actor
RoomBoundsBox, 4방향 Entrance 포인트, Entrance/Wall 클래스를 보유ExcuteSpawnRoom(int32 DirectionIndex, bool bIsEntrance)에서 실제로 Entrance/Wall Actor 스폰
1.2. 문제가 된 코드 (요약)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void UCMMapGenerateLogicBaseComponent::PlaceWallsAndEntrances(TMap<FCMRoomPosition, TObjectPtr<ACMRoom>>& OutRoomMap)
{
ENetMode NetMode = GetWorld() ? GetWorld()->GetNetMode() : NM_Standalone;
for (const TPair<FCMRoomPosition, TObjectPtr<ACMRoom>>& Pair : OutRoomMap)
{
if (ACMRoom* Room = Pair.Value.Get())
{
for (int32 i = 0; i < 4; ++i)
{
FCMRoomPosition CurrentPos(Pair.Key.X + DirectionX[i], Pair.Key.Y + DirectionY[i]);
if (OutRoomMap.Contains(CurrentPos))
{
if (NetMode != NM_Client)
{
UE_LOG(LogTemp, Log, TEXT("HasAuthority: %d, NetMode: %d"),
GetOwner()->HasAuthority(),
static_cast<int32>(NetMode));
Room->ExcuteSpawnRoom(i, true); // Entrance
}
}
else
{
Room->ExcuteSpawnRoom(i, false); // Wall
}
}
}
}
}
1.3. 증상
bReplicates를 블루프린트에서 켜지 않았는데도, 클라이언트에서 방/Entrance/Wall 이 모두 보임GetOwner()->HasAuthority()로 조건을 걸어도, 여전히 호스트와 클라이언트 양쪽에서 Entrance/Wall 이 각각 생성됨- 로그 상으로는
HasAuthority: 1이 서버와 클라이언트 양쪽에서 모두 찍히는 듯한 결과
2. 개념 정리: HasAuthority, NetMode, 복제 유무
2.1. HasAuthority()
AActor::HasAuthority()는 내부적으로Role == ROLE_Authority를 의미- 복제된 Actor 의 경우
- 서버에 있는 인스턴스:
HasAuthority() == true - 클라에 있는 인스턴스:
HasAuthority() == false
- 서버에 있는 인스턴스:
- 하지만 비복제 Actor 의 경우
- 서버에서 스폰된 인스턴스:
HasAuthority() == true - 클라에서 별도로 스폰된 인스턴스: 여기도 그 월드에서 유일한 Actor 라서
Role = ROLE_Authority→HasAuthority() == true
- 서버에서 스폰된 인스턴스:
- 따라서, “HasAuthority 가 true면 서버”라고 단정하면 안 됨
2.2. NetMode
UWorld::GetNetMode()로 현재 월드의 네트워크 모드를 알 수 있음NM_Standalone(0) : 싱글 플레이NM_DedicatedServer(1) : 디디케이티드 서버NM_ListenServer(2) : 리슨 서버(호스트)NM_Client(3) : 클라이언트
- “이 코드가 서버에서 실행 중인가?” 를 알고 싶다면:
World->GetNetMode() == NM_Client이면 클라이언트 → 서버 전용 로직은 실행하지 않도록 Early Return- 또는
World->GetNetMode() != NM_Client을 서버/호스트 쪽으로 간주해도 됨
3. 실제 원인 분석
3.1. MapGenerator 가 서버/클라에 각각 존재
ACMProcedualMapGenerator는 단순AActor이고,bReplicates세팅이나 서버 전용 제약 없이 월드에 배치/생성되어 있었음- 리슨 서버 + 클라이언트 환경에서
- 서버 월드에
ACMProcedualMapGenerator한 개 - 클라이언트 월드에도
ACMProcedualMapGenerator한 개 - 둘 다 자기 월드에서 유일한 존재 → 둘 다
HasAuthority() == true
- 서버 월드에
GenerateMap()호출 또한 서버/클라 각각에서 트리거되는 구조였기 때문에,UCMMapGenerateLogicBaseComponent::ExecuteMapGenerationLogicUCMMapGenerateLogicBaseComponent::PlaceWallsAndEntrancesACMRoom::ExcuteSpawnRoom이 세트가 서버와 클라이언트에서 각각 한 번씩 실행됨
3.2. PlaceWallsAndEntrances 의 조건
- 초기 버전에서는
GetOwner()->HasAuthority()만 보고 Entrance 를 생성 - 이후
ENetMode NetMode = GetWorld()->GetNetMode()를 도입해NetMode != NM_Client인 경우에만 Entrance 를 생성하도록 변경 - 하지만 여전히 문제가 남았던 부분:
else분기 (OutRoomMap에 인접 방이 없을 때)에서 Wall 스폰은 권한/NetMode 체크 없이 항상 실행ExcuteSpawnRoom내부에서도 별도의HasAuthority()체크가 없으면, 양쪽에서 모두 스폰
- 최종적으로는:
- 서버 MapGenerator → Entrance + Wall 스폰
- 클라 MapGenerator → (NetMode 체크 추가 후) Entrance 는 안 만들더라도, Wall 은 만들 수 있음
- 그리고
ACMRoom이 비복제라면, 서버/클라 각각의ACMRoom인스턴스도 자기 기준에서는HasAuthority() == true
3.3. 로그가 둘 다 1로 찍히는 이유
UE_LOG(LogTemp, Log, TEXT("HasAuthority: %d, NetMode: %d"), GetOwner()->HasAuthority(), (int32)NetMode);- 위 로그에서:
- 서버 월드:
HasAuthority = 1,NetMode = 2 (ListenServer)또는 1 (DedicatedServer) - 클라 월드:
HasAuthority = 1,NetMode = 3 (Client)인 케이스가 가능
- 서버 월드:
- 즉, 단순히
HasAuthority칼럼만 보면 둘 다 1로 찍히기 때문에,- “Authority 를 둘 다 갖고 있는 것 같다” 는 인상을 받게 됨
- 실제로는 서로 다른 월드에서 각각 Authority 인 비복제 Actor 일 뿐, 네트워크 복제와는 무관
4. 해결 전략
4.1. 서버/클라이언트 분기 기준을 명확히
- “맵을 생성하는 로직은 서버/호스트에서만 실행한다” 를 명확한 정책으로 잡음
- 이를 위해 다음 기준을 사용
UWorld::GetNetMode()가NM_Client이면 → 맵 생성 로직/스폰 로직은 아예 실행하지 않음HasAuthority()는 보조적으로 사용하되, 비복제 Actor 도 Authority 일 수 있다는 점을 항상 염두에 둠
5. 최종 정리
아래는 Entrance 인스턴스가 정상적으로 Replication On/Off 되는 결과물입니다.
- 언리얼 네트워크에서
HasAuthority()와NetMode는 비슷해 보이지만, 서로 다른 레이어의 개념이라는 점을 다시 상기HasAuthority(): 이 Actor 인스턴스가 자기 월드에서 서버 역할인지NetMode: 이 월드가 DedicatedServer / ListenServer / Client / Standalone 인지
- 비복제 Actor 는 서버와 클라이언트에서 각각 스폰되면, 둘 다
HasAuthority() == true라는 점이 핵심 포인트 따라서, 서버/클라이언트를 분기하고 싶을 때는 다음 우선순위를 지키는 것이 좋다고 정리
- 우선순위
UWorld::GetNetMode()로 프로세스 레벨(서버/클라) 구분NM_Client인 경우 서버 전용 로직을 실행하지 않음
AActor::HasAuthority()는 복제된 Actor 의 서버/클라 인스턴스 구분에 사용- 스폰 로직이 있는 함수(예:
ExcuteSpawnRoom) 내부에도HasAuthority()체크를 두어 이중 방어
이번 트러블슈팅을 통해 얻은 교훈
- 단순히 “Authority 가 true 니까 서버겠지” 라고 생각하면 쉽게 헷갈린다.
- 맵 생성처럼 월드 전체에 영향을 주는 로직은 반드시 서버/호스트 한 곳에서만 실행되도록 구조를 잡는 것이 중요하다.
- 디버깅 시에는
HasAuthority뿐 아니라NetMode값도 함께 로그로 남겨야, “어느 월드에서 무슨 역할로” 실행 중인지 명확히 볼 수 있다.
This post is licensed under CC BY 4.0 by the author.



