🎮 구현 목표
경기 종료 후 Player Table → Leaderboard Table 순서로 DB 업데이트가 진행되어, 랭킹이 정확하게 반영되도록 합니다.
Player Table에 최신 승수가 저장된 후에야 Leaderboard가 올바르게 갱신될 수 있습니다.
🚨 문제 상황
1등이 2등으로 표시되는 버그
경기 종료 후 순위 계산 흐름:
1. Player Table에 승수 기록 (matchWins += 1)
2. Leaderboard Table에서 상위 10명 갱신
그런데 실제로는:
플레이어 A: 5승 → 6승 (우승!)
↓
[동시에 발생]
Lambda 1: Player Table에 6승 기록 중... (느림)
Lambda 2: Leaderboard 갱신 (Player Table 조회) → 아직 5승! ❌
↓
결과: Leaderboard에 5승으로 기록됨
문제의 핵심: 비동기 작업의 순서 미보장
AWS Lambda는 비동기로 실행되므로, 두 Lambda를 동시에 호출하면:
// 게임 종료 시 (잘못된 구현)
void ADS_MatchGameMode::OnMatchEnded()
{
// 두 요청이 거의 동시에 발생
GameStatsManager->RecordMatchStats(...); // Lambda 1
GameStatsManager->UpdateLeaderboard(...); // Lambda 2
// Lambda 2가 Lambda 1보다 먼저 끝날 수 있음!
}
실제 테스트 결과:
경기 1: 우승 → 랭킹 1위 ✅
경기 2: 우승 → 랭킹 2위 ❌ (Player Table 갱신 전 Leaderboard 조회)
경기 3: 우승 → 랭킹 1위 ✅
간헐적으로 발생하는 이유는 네트워크 지연에 따라 Lambda 실행 순서가 바뀌기 때문입니다.
💭 해결 방법
"Player Table 저장이 끝났다는 걸 어떻게 알지?"
비동기 작업의 순서를 보장하려면 "첫 번째 작업 완료 → 두 번째 작업 시작" 구조가 필요했습니다.
초기 고민: Timer로 지연?
GameStatsManager->RecordMatchStats(...);
GetWorldTimerManager().SetTimer(TimerHandle, [this]() {
GameStatsManager->UpdateLeaderboard(...);
}, 2.0f, false); // 2초 후 실행
문제:
- 2초면 충분한가? 3초는? → 정답 없음
- Lambda가 느려지면 여전히 실패
- 불필요한 대기 시간
해결: Delegate 체인
"Player Table 저장 성공 시 Delegate 발행 → Leaderboard 갱신" 구조로 변경
// GameStatsManager.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAPIRequestSucceeded);
class UGameStatsManager : public UHTTPRequestManager
{
public:
void RecordMatchStats(const FDSRecordMatchStatsInput& Input);
void UpdateLeaderboard(const TArray<FString>& PlayerNames);
UPROPERTY(BlueprintAssignable)
FOnAPIRequestSucceeded OnUpdatedGameStatsSucceeded; // 핵심!
};
Lambda 응답 처리:
// GameStatsManager.cpp
void UGameStatsManager::RecordMatchStats_Response(
FHttpRequestPtr Request,
FHttpResponsePtr Response,
bool bWasSuccessful)
{
if (!bWasSuccessful) return;
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> JsonReader =
TJsonReaderFactory<>::Create(Response->GetContentAsString());
if (FJsonSerializer::Deserialize(JsonReader, JsonObject))
{
if (ContainsErrors(JsonObject, true)) return;
// Player Table 저장 성공!
UE_LOG(LogDedicatedServers, Warning,
TEXT("RecordMatchStats_Response Succeeded!"));
OnUpdatedGameStatsSucceeded.Broadcast(); // Delegate 전파
}
}
GameMode에서 Delegate 구독:
// DS_MatchGameMode.cpp
void ADS_MatchGameMode::CreateGameStatsManager()
{
GameStatsManager = NewObject<UGameStatsManager>(this, GameStatsManagerClass);
// Delegate 구독 (핵심!)
GameStatsManager->OnUpdatedGameStatsSucceeded.AddDynamic(
this,
&ADS_MatchGameMode::OnGameStatsUpdated
);
}
void ADS_MatchGameMode::OnMatchEnded()
{
// 1. Player Table에 승수 기록만 요청
EndMatchForPlayerStats(); // RecordMatchStats 호출
// 2. Leaderboard는 여기서 호출 안 함!
// (Delegate 콜백에서 호출됨)
}
// Delegate 콜백
void ADS_MatchGameMode::OnGameStatsUpdated()
{
// Player Table 저장 완료 후 자동 호출됨!
UE_LOG(LogTemp, Warning, TEXT("OnGameStatsUpdated"));
// 2. 이제 Leaderboard 갱신
TArray<FString> LeaderIds;
if (AMatchGameState* GS = GetGameState<AMatchGameState>())
{
TArray<AMatchPlayerState*> Leaders = GS->GetLeaders();
for (AMatchPlayerState* Leader : Leaders)
{
LeaderIds.Add(Leader->GetUsername());
}
}
UpdateLeaderboard(LeaderIds); // 최신 데이터로 갱신 ✅
}
개선된 흐름:
1. 경기 종료
↓
2. RecordMatchStats() 호출 (Player Table 갱신)
↓
3. Lambda 실행... (비동기)
↓
4. Lambda 성공 응답
↓
5. OnUpdatedGameStatsSucceeded.Broadcast()
↓
6. OnGameStatsUpdated() 자동 호출
↓
7. UpdateLeaderboard() 호출 (최신 데이터 조회) ✅
🔧 고려사항
왜 서버에서 순위를 계산하지 않았나?
고민: "서버가 직접 순위 계산하면 Lambda 2개 필요 없지 않나?"
// 고려했던 방법
void ADS_MatchGameMode::OnMatchEnded()
{
// 서버가 직접 PlayerState 순회하며 순위 계산
TArray<AMatchPlayerState*> AllPlayers;
// ... 정렬 로직
// DB에 한 번에 저장
GameStatsManager->RecordMatchStatsWithRanking(AllPlayers);
}
그러나 DB는 Single Source of Truth여야 한다고 판단:
- 서버 크래시 시 데이터 소실 위험
- 여러 게임 세션 간 랭킹 통합 불가
- Lambda 분리 = 관심사 분리 (Player 기록 vs Leaderboard 갱신)
✅ 결과
✅ 순위 정확도 100%: Player Table 저장 후 Leaderboard 갱신 보장
✅ 간헐적 버그 해결: 네트워크 지연과 무관하게 순서 보장
✅ 확장성: 새로운 통계 추가 시 Delegate 체인 확장 가능
핵심 교훈:
"비동기 작업의 순서는 코드 순서가 아니라 Delegate로 보장한다"
HTTP 요청, Lambda 호출 같은 비동기 작업은 응답 시점을 예측할 수 없습니다.
Delegate 패턴:
작업 1 호출 → 응답 대기 → Delegate 전파 → 작업 2 자동 시작
이 방식은:
- 네트워크 지연과 무관하게 순서 보장
- Lambda 실행 시간이 늘어나도 안전
- 여러 단계의 비동기 작업 체인 가능
'프로젝트 회고' 카테고리의 다른 글
| [DirectX 11] Unity 리소스 메타데이터 파싱을 통한 스프라이트 시트 관리 (0) | 2025.12.02 |
|---|---|
| [UE5 팀 프로젝트] Pull-Request 시행착오와 교훈 (0) | 2025.12.02 |
| [Dedicated Server] SeamlessTravel: 레벨 전환 시 데이터 유실 방지 (0) | 2025.12.02 |
| [Dedicated Server] 비동기 폴링 기반 서버 접속 시도 (0) | 2025.12.02 |
| [UE5 팀 프로젝트] 지연 초기화: 런타임 액터 스폰, BeginPlay 호출 전 DataTable 데이터 무결성 확보 (0) | 2025.12.02 |
