Post

2026-07-20 TIL (100일차)

2026-07-20 TIL (100일차)

언리얼 EOS 세션 갱신 버그 디버깅: 조용한 실패(Silent Failure)와 타입 불일치의 함정

한 줄 요약

서버가 세션의 현재 인원수를 정상적으로 갱신했음에도 클라이언트가 계속 0명으로 읽어오는 버그를 디버깅했다. 원인은 EOS 백엔드의 int64 타입 강제 변환과 언리얼 엔진 래퍼(Wrapper) API의 조용한 실패(Silent Failure)였으며, FVariantData에 직접 접근하여 문자열로 우회 파싱하는 방법으로 최종 해결했다.


문제 상황

클라이언트가 방 목록을 갱신할 때, 실제 방에 사람이 입장하여 서버가 인원수(CURPLAYERS)를 1로 갱신했음에도 불구하고 클라이언트 UI에는 계속 0/4로 표시되는 현상이 발생했다.

  • 초기 분석: 클라이언트가 아예 값을 못 읽었다면 폴백(Fallback) 로직에 의해 NumOpenPublicConnections를 역산하여 4/4로 떴어야 했다. 하지만 “방 생성(1빠로 입장)”이 정상 동작한다는 것은 값을 0으로 읽었다는 뜻이다.
  • 가설 3가지:
    1. 서버의 갱신 코드가 에디터(PIE) 환경 등의 이유로 건너뛰어짐.
    2. 갱신 요청은 보냈으나 EOS 백엔드에서 UpdateSession이 실패.
    3. 백엔드 반영 및 검색 노출까지의 전파 지연(Propagation Delay).

디버깅 과정

1단계: 서버 갱신 및 백엔드 반영 여부 확인

서버 쪽에 UpdateSession (또는 파괴 후 재생성) 결과를 확인하는 디버그 로그를 심었다.

1
2
3
EOS_SessionModification_AddAttribute() named (CURPLAYERS) with value (1)
...
[Session] CreateSession 완료: GameSession (성공)

1단계: 서버 갱신 및 백엔드 반영 여부 확인

  • 결과: 서버는 아무 문제가 없었다. SDK 로그를 통해 EOS 백엔드에 새 세션이 CURPLAYERS = 1 값을 가지고 완벽하게 생성 및 반영되었음을 확인했다.

2단계: 클라이언트 읽기 상태 확인

클라이언트가 검색한 결과를 어떻게 파싱하고 있는지 MakeRoomInfo에 로그를 심어 30초 후, 60초 후 계속 검색을 돌려보았다.

  • 결과: 클라이언트는 “읽기에는 성공했는데, 그 값은 0이다”라고 말하고 있었다. 전파 지연의 문제도 아니었다.
1
[Session][CLIENT] 방0: CURPLAYERS읽기=성공, 값=0, OpenConn=0 → 표시인원=0

원인 분석: 완벽한 착각을 만든 기만 전술

서버는 분명히 1을 보냈는데 클라이언트는 왜 ‘성공적으로 0을 읽었다’고 판단했을까? 범인은 자료형 불일치(Type Mismatch)였다.

  • EOS 백엔드의 업캐스팅: 서버 로직에서 int32로 인원수를 세팅해서 보냈지만, EOS 시스템은 자체 규격에 맞춰 모든 정수형 데이터를 더 큰 자료형인 int64로 변환하여 보관한다.
  • 언리얼 Get() 함수의 반쪽짜리 성공 판정: 클라이언트가 SessionSettings.Get(Key, Value)를 호출할 때, 언리얼 엔진은 해당 키(Key)가 존재하는지만 확인하고 true(성공)를 반환한다.
  • 타입 불일치로 인한 조용한 실패 (Silent Failure): 클라이언트는 int32 크기의 그릇을 내밀었는데 들어있는 데이터는 int64였다. 언리얼 내부의 FVariantData 처리 로직은 요청한 타입과 실제 타입이 100% 일치하지 않으면 메모리 오염을 막기 위해 값을 넣지 않고 기본값인 0으로 덮어씌워 버린다.

결론: 클라이언트는 “방에 0명이 있다”고 읽어온 것이 아니라, “키는 있어서 읽기엔 성공했는데 타입이 달라서 값을 못 꺼냈으니 에러 처리용으로 0을 반환했다”가 진실이었다. 초기 인원수인 ‘0’과 에러 반환값인 ‘0’이 우연히 일치하면서 디버깅 방향을 완전히 꼬아버린 것이다.


🛠️ 해결 시도 및 최종 해결책

1차 시도: int64로 읽어오기 (LNK2019 에러)

데이터 타입이 문제라는 것을 알고 클라이언트의 Get() 함수에서 매개변수를 int64로 수정했다.

  • 결과: 빌드 실패. LNK2019 (Unresolved External Symbol) 링크 에러 발생.
  • 이유: 언리얼 엔진의 FOnlineSessionSettings::Get 함수는 템플릿으로 구현되어 있는데, 엔진 내부에서 외부 모듈로 꺼내 쓸 수 있도록 명시적 인스턴스화(Export)를 해둔 타입에 int32, FString 등은 있지만 int64 타입은 Export 해두지 않아서 링커가 구현부를 찾지 못한 것이다.

2차 시도: FVariantData 직접 접근 및 우회 파싱 (최종 해결)

템플릿 Get() 함수를 통하지 않고 내부 데이터(FVariantData)에 직접 접근하여 문자열로 변환한 뒤 정수로 파싱하는 가장 안전한 방법을 적용했다. 내부가 int32int64든 완벽하게 숫자를 가져올 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1순위 — 서버가 직접 광고한 CURPLAYERS 키 (EOS에서 신뢰 가능한 유일한 값)
// 2순위 — 열린 슬롯 역산 (Null/LAN 폴백)
int32 AdvertisedCount = -1;

// SessionSettings 내부에 실제 세팅값들이 저장된 Settings TMap에서 키를 직접 찾습니다.
if (const FOnlineSessionSetting* Setting = R.Session.SessionSettings.Settings.Find(PlayerCountKey))
{
    // FVariantData가 들고 있는 값을 문자열로 변환한 뒤 int32로 안전하게 파싱합니다.
    // int64 링크 에러(LNK2019)를 피하고 타입 캐스팅 문제도 동시에 해결합니다.
    FString ValueStr = Setting->Data.ToString();
    AdvertisedCount = FCString::Atoi(*ValueStr);
}

if (AdvertisedCount >= 0)
{
    Info.CurrentPlayers = AdvertisedCount;
}
else
{
    Info.CurrentPlayers = Info.MaxPlayers - R.Session.NumOpenPublicConnections;
}

오늘 배운 점

  • 조용한 실패(Silent Failure)의 무서움: API가 true를 반환한다고 해서 내가 원하는 동작이 온전히 수행되었다고 맹신해서는 안 된다. 내부 데이터의 타입 일치 여부를 항상 의심해야 한다.
  • 플랫폼 백엔드의 데이터 강제 변환: 로컬(엔진)에서 보낸 데이터 타입이 백엔드(EOS 등)를 거치며 다른 규격(int32 -> int64)으로 강제 변환될 수 있음을 염두에 두어야 한다.
  • 링크 에러(LNK2019)의 실체: 템플릿 함수 사용 시 흔히 겪는 링커 에러는 엔진 코어가 해당 타입의 템플릿 인스턴스화를 외부로 노출(Export)하지 않았기 때문일 수 있으며, 이럴 때는 원시 데이터 구조체(FVariantData 등)에 직접 접근하여 우회하는 것이 좋은 해결책이 된다.
This post is licensed under CC BY 4.0 by the author.