네트워크 단절 트러블슈팅
VPS 멀티플레이 · NGO 2.12 · Distributed Authority

네트워크 단절: 원인과 해결 정리

친구찾기(DA·무호스트) 모드에서 겪은 네트워크 단절 문제들의 근본 원인·해결책·검증 근거를 한 곳에 모았다. 다음에 멀티플레이를 손댈 때 같은 길을 다시 헤매지 않기 위한 참고 문서.

대상 친구찾기 DA 모드 (WithDistributedAuthorityNetwork) 핵심 파일 FindFriendBootstrap · RaceReadyGate · NetworkDiagnostics 상태 오너 두절·층 이동 해결 검증 완료

한눈에 — 이것만 기억하면 절반은 끝난다

  1. "재연결이 안 된다"의 정체는 대개 재연결이 아니라 위치 공유다. 세션 재입장·NGO 전송은 성공하는데, RPC를 나르는 RaceReadyGate가 죽어 데이터만 안 흐르는 경우가 많다.
  2. DA에서 오너가 나가도 살아남아야 하는 씬 NetworkObject는 DontDestroyWithOwner = 1. 0이면 오너 이탈 시 세션 전체에서 파괴되고 다시 스폰되지 않는다. — 이번 오너 두절 문제의 단 하나의 근본 원인.
  3. 재연결 성공 판정은 IsConnectedClient가 아니라 "실제 데이터 재수신"으로 한다. 전송이 붙어도 앱 RPC는 안 올 수 있다(가짜 복구).
  4. 파괴적 재연결(Shutdown·ReconnectAsync·Leave+Join)은 자동 경로에서 금지. flapping 망에서 no-op·파괴적·'already a member'. 평상시엔 NGO 내장 Relay 재연결에 맡기고, 최후 수단만 재입장.
  5. 이동/두절 위험이 큰 쪽은 비오너(클라)로 고정. 이동하는 기기의 업링크가 먼저 죽고, 오너가 죽으면 세션 전체가 흔들린다.
01

아키텍처 배경 — 왜 이런 문제가 생기나

문제의 절반은 Distributed Authority(무호스트) 구조를 Relay(호스트 기반)처럼 다뤄서 생긴다. 세 가지만 이해하면 나머지가 풀린다.

DA vs Relay

친구찾기는 WithDistributedAuthorityNetwork()전용 호스트가 없다. 세션 오너(DAHost)가 라우팅 주체이고, 오너가 나가면 오너십이 다른 클라로 이관된다. 레이스는 WithRelayNetwork()(호스트 기반, host migration 없음 → 호스트 나가면 세션 소멸).

gate = RPC 통로

RaceReadyGate는 씬에 배치된 NetworkObject로, 두 기기 사이의 위치·층이동·도착 RPC를 전부 이걸로 나른다. gate가 죽으면 두 기기는 서로의 상태를 한 글자도 못 주고받는다.

오너 이관 ≠ gate 생존

DA 오너십 이관 자체는 잘 된다(CurrentSessionOwner 1→2). 하지만 gate NetworkObject가 이관되도록 설정돼 있지 않으면(DontDestroyWithOwner=0) 파괴되고, 이관될 대상이 사라진다. "세션은 살아있는데 통신은 죽는" 상태의 정체.

02

문제별 원인 → 해결

증상 / 근본 원인 / 해결 / 근거(로그) 순. 상태 칩으로 검증 여부를 표시했다.

① 오너(호스트) 두절 시 재연결이 안 되는 것처럼 보임

검증 완료 · 커밋 1ce759e
증상

세션 오너 기기의 네트워크를 끊으면, 재입장은 되는데 위치 공유가 다시 살아나지 않고 "연결 끊김" 팝업이 반복된다.

근본 원인

재연결(세션 재입장·NGO 전송 복구)은 실제로 성공한다. 실패하는 건 위치 공유다. RaceReadyGateDontDestroyWithOwner=0 때문에, DA에서 오너가 이탈하면 gate가 세션 전체에서 파괴되고 → 이관 대상이 사라져 → RPC 채널이 영구 사망한다. 오너가 재입장해도 gate가 다시 스폰되지 않는다.

해결

Assets/Scenes/7. FindFriend.unity의 RaceReadyGate NetworkObject DontDestroyWithOwner: 0 → 1 (1줄). 오너 이탈 시 gate가 파괴되지 않고 신 오너로 이관·생존 → RPC 채널 유지. 재입장한 옛 오너는 옵저버(SpawnWithObservers=1)로 gate를 받아 다시 붙는다.

BEFORE — DontDestroyWithOwner=0

오너 이탈 → 오너십 이관 1→2 성공
정지 클라 Gate OnNetworkDespawn (파괴)
오너 재입장(clientId 3,4) 후 Gate OnNetworkSpawn 없음
재연결 실패: 위치 RPC 재개 실패 반복 → 팝업

AFTER — DontDestroyWithOwner=1

정지 클라 Gate 소유권 획득 (파괴 없이 이관)
오너 재입장마다 Gate OnNetworkSpawn 재발생
진짜 복구 — 위치 RPC 재개 확인 수신경과 0.0s 4회 성공
양쪽 위치 공유 즉시 재개
근거 로그 (BuildLog 153906/153934 → 160219/160633)
# BEFORE (미수정) — 정지 클라 관점
[15:40:48] Gate OnNetworkDespawn — LocalClientId=2 NObjId=10001
[15:40:48] [NetDbg][L2] 세션오너 변경 1 → 2 (내가오너=True)   # 이관은 성공
   ...재입장(clientId=3,4) 이후에도 Gate OnNetworkSpawn 없음
[15:41:37] 재연결 실패 1/3: 위치 RPC 재개 실패(6s 내 상대 위치 무수신)

# AFTER (DontDestroyWithOwner=1)
[16:08:00] Gate 소유권 상실 → Gate 소유권 획득 — 내가 새 오너 (gate 생존·RPC 라우팅 유지)
[16:08:39] Gate OnNetworkSpawn — LocalClientId=3   # 재입장 시 재스폰
[16:08:40] 진짜 복구(전체 재입장) — 위치 RPC 재개 확인 hasRemote=True 수신경과=0.0s

② 재입장은 성공하는데 위치 RPC가 재개되지 않음 (가짜 복구)

검증 완료
증상

IsConnectedClient=true로 "연결됨"인데 상대 위치가 계속 안 온다.

근본 원인

NGO 전송이 붙었다는 것이 애플리케이션 RPC 재개를 뜻하지 않는다. gate 미재스폰(문제 ①), Reliable 큐 적체 등으로 데이터만 안 흐를 수 있다. 실측: '복구' 후 177s 무수신.

해결

재연결 성공 판정을 전송 플래그가 아니라 실제 상대 위치 재수신으로 한다 — WaitForRpcResumeAsync: 재연결 시각 이후 _lastRemoteRecvTime > since가 될 때까지 확인, timeout이면 '가짜 복구'로 재시도. 이게 있어야 팝업/토스트가 진짜 복구만 알린다.

③ 완전 두절(none) 복구 시간 단축 — "옵션1 튜닝"

검증 완료
증상

두절 후 팝업(→재입장)까지 최대 60s 하드캡을 기다려 복구가 느리다.

원인

초기엔 "총 무연결 NGO_GIVEUP_SEC(60s)" 하드캡에서만 팝업했다.

해결

망은 살아있는데(HasActiveNetwork) NGO만 끊긴 상태RECONNECT_NETUP_GIVEUP(15s) 누적되면 팝업→재입장. 망 none 동안은 카운트 정지(붙을 망이 없으면 재입장해도 실패하므로). 총 무연결 60s 하드캡은 안전망으로 유지.

근거

망사용가능 15s(임계 15s) → 끊김 팝업(최후 수단 전체 재입장) 발동 확인.

④ 이동 중 업링크(업로드) 두절 — 비대칭 단절

진단 확정
증상

한쪽이 이동(걷기·층 이동) 중이면 상대의 위치 수신이 끊긴다. 죽는 방향이 이동하는 기기를 따라 뒤집힌다.

원인

이동하는 기기의 업로드(업링크)가 먼저 끊긴다(셀룰러/계단/빠른 이동). 상대는 hasRemote=False로 수신 0건이 되어 화면 변화가 없다. 코드 문제가 아니라 무선 환경 문제.

완화

수신경과 3s↑ "연결이 약합니다" 토스트 + 15s↑ 능동 재연결. 근본 회피는 이동하는 쪽을 비오너로 고정(오너 두절 비대칭 위험까지 함께 해결).

⑤ 파괴적 재연결은 자동 경로에서 금지

교훈
시행착오

수동 재바인딩을 두 갈래로 시도했다 — 전략 A(Leave+Join), 전략 B(Shutdown+ReconnectAsync). flapping 망에서 각각 no-op·파괴적·'player is already a member'로 확인되어 자동 경로에서 폐기.

정본

평상시엔 NGO/UnityTransport 내장 Relay 재연결에 맡긴다(UpdateConnectionHealthIsConnectedClient로 감시). 자체 복구가 끝내 실패했을 때만 최후 수단으로 세션 정리 후 코드 재입장(JoinByCode). 팝업~재시도 사이 자체 복구되면(IsConnectedClient=true) 파괴적 재입장을 취소.

⑥ 크로스층 이동수단 노드 로드 실패 (부수)

수정 완료
증상

크로스층 유도용 이동수단(엘베·계단) 노드가 0개로 로드된다.

원인

LoadTransitNodesAsync가 Android persistentDataPath(로컬, file:// 스킴 없음)를 UnityWebRequest로 읽으려다 실패.

해결

경로에 "://"가 없으면(로컬) File.ReadAllText로 직접 읽도록 분기.

04

재연결 타이밍 상수

FindFriendBootstrap의 조정 포인트. 자체 복구를 자주 뺏으면 값을 올리고, 복구가 느리면 내린다.

상수의미
RECONNECT_NETUP_GIVEUP15s망 살아있음 + NGO만 끊김이 이만큼 누적되면 팝업→재입장 (옵션1 튜닝의 핵심)
NGO_GIVEUP_SEC60s총 무연결 하드캡. 망이 끝내 안 돌아와도 반드시 팝업
RPC_RESUME_TIMEOUT6s재입장 후 실제 상대 위치 재수신 대기(가짜 복구 판정)
MAX_RECONNECT3최후 수단 재입장 최대 시도(망 복귀 후 실제 시도만 카운트)
NGO_CONNECT_TIMEOUT8s재입장 후 IsConnectedClient 대기
NGO_SHUTDOWN_TIMEOUT3s재조인 전 NGO 완전 종료 대기('Failed to start' 레이스 방지)
RECONNECT_OP_TIMEOUT12sJoinByCode 무한 hang 방지 컷오프
CONN_WEAK_SEC3s수신 두절 이만큼이면 "연결 약함" 토스트
DisconnectTimeoutMS30sUnityTransport 런타임 값(씬 직렬화). 두절→끊김 판정 지연
none 복구 제약 — 우리 코드엔 none 자체의 시간제한이 없다(HasActiveNetwork 가드로 none 동안은 재시도를 보류해 시도가 소진되지 않음). 진짜 상한은 MPS 세션 / Relay 수명(SessionOptions에 TTL 미설정 = SDK 기본). 비오너가 none이어도 상대 오너가 세션을 지탱하면 버틴다.
05

재사용 체크리스트 — DA 멀티플레이를 다시 손댈 때

host migration으로 살아남아야 하는 씬 NetworkObject는 DontDestroyWithOwner=1
DA에서 오너가 나가면 0인 오브젝트는 세션 전체에서 파괴되고 재스폰되지 않는다. RPC를 나르는 오브젝트라면 통신이 영구 사망한다.
재연결 성공 판정은 실제 데이터 재수신으로
IsConnectedClient=true는 전송만 뜻한다. 앱 RPC가 실제로 다시 오는지(마지막 수신 시각 갱신)까지 확인해 '가짜 복구'를 걸러라.
파괴적 재연결은 자동 경로에서 쓰지 않는다
Shutdown·ReconnectAsync·Leave+Join은 flapping 망에서 no-op·파괴적·'already a member'. 내장 Relay 재연결을 우선하고, 재입장은 자체 복구 실패 시 최후 수단으로만.
이동/두절 위험이 큰 역할을 비오너로 고정
이동 기기는 업링크가 먼저 죽고, 오너가 죽으면 세션 전체가 흔들린다. 엇갈림 방지에도 유리.
망 없음(none) 동안은 재입장을 보류
HasActiveNetwork 게이트로 붙을 망이 없을 때의 무의미한 재시도 폭주('Cannot resolve host')를 막는다. 망 복귀가 재트리거.
단절 순간을 4계층(L1~L4)으로 동시에 찍어 상관한다
"어디서 끊기는지"를 모르면 엉뚱한 곳을 고친다. OS·오너·NGO·RPC를 공통 시계로 상관하면 원인 계층이 바로 드러난다.
06

알려진 한계 · 남은 것

복구 불가

두 기기 동시 두절(C) — 세션을 지탱할 오너가 없어 MPS 세션/Relay가 소멸한다. 팝업으로 수동 복귀만 가능. (테스트 대상은 "오너 두절 O / 둘 다 두절은 안 되는 것"이 맞다.)

엣지

친구 이동수단 방송 유실 — 이동 기기가 두절 상태에서 이동수단을 고르면 선택 RPC가 유실되고 재연결 후 재방송하지 않아, 정지 기기가 유도 노드를 못 찾는다(안전 보류로 저하, 오안내는 아님). 재연결 후 재방송이 후속 과제.

미측정

MPS 세션/Relay의 실제 TTL(무주인 세션 소멸 시각) — 아직 실측 안 함. 크로스층 DA·오너십 재이관의 장기 안정성도 해피패스만 검증됨.