오늘은 룬 시스템의 스탯 업데이트 성능을 개선하고 확장성을 높이는 작업을 했음
처음엔 단순하게 하드코딩으로 구현했다가, 나중에 새로운 스탯이 추가될 때마다 코드를 수정해야 하는 문제와 성능 이슈를 발견해서 전체적인 아키텍처를 개선했음
처음 구현한 스탯 업데이트 시스템을 보니 확장성에 문제가 있었음
// 기존 하드코딩 방식
void UGS_StatPanelWidget::InitStatList()
{
// 새로운 스탯 추가할 때마다 이 코드를 수정해야 함
TArray<TPair<FName, float>> StatInfos = {
{FName("HP"), FoundRow->HP},
{FName("ATK"), FoundRow->ATK},
{FName("DEF"), FoundRow->DEF},
{FName("AGL"), FoundRow->AGL},
{FName("ATS"), FoundRow->ATS}
};
}
void UGS_StatPanelWidget::UpdateStats(const FGS_StatRow& RuneStats)
{
// 각 스탯마다 개별적으로 업데이트 (비효율적)
UpdateStatWidget(FName("HP"), BaseStats->HP, RuneStats.HP);
UpdateStatWidget(FName("ATK"), BaseStats->ATK, RuneStats.ATK);
UpdateStatWidget(FName("DEF"), BaseStats->DEF, RuneStats.DEF);
UpdateStatWidget(FName("AGL"), BaseStats->AGL, RuneStats.AGL);
UpdateStatWidget(FName("ATS"), BaseStats->ATS, RuneStats.ATS);
}
문제점들
성능 문제 분석
// 룬 1개 배치할 때마다 발생하는 작업들
UpdateStats() {
for (5개 스탯) {
데이터테이블.FindRow(); // 5회 데이터베이스 조회
GetStatValueByName(); // 5회 리플렉션 호출
SetStatData(모든_값_재설정); // 5회 전체 UI 업데이트
}
}
확장성 문제
API 설계 고민
// 현재 방식: 전체 재설정
StatWidget->SetStatData(StatName, BaseValue, RuneValue, BonusValue);
// vs
// 부분 업데이트 방식
StatWidget->SetRuneStatData(RuneValue, BonusValue);
1단계: 리플렉션을 통한 동적 스탯 탐지
// 하드코딩 제거, 자동으로 모든 Float 프로퍼티 탐지
UStruct* StatStruct = FGS_StatRow::StaticStruct();
for (TFieldIterator<FProperty> PropIt(StatStruct); PropIt; ++PropIt)
{
FProperty* Property = *PropIt;
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Property))
{
FName StatName = Property->GetFName();
float StatValue = *FloatProp->ContainerPtrToValuePtr<float>(FoundRow);
// 자동으로 위젯 생성 및 등록
StatDataList.Add(StatName, StatWidget);
}
}
2단계: TMap을 활용한 효율적 업데이트
// 기존: 매번 데이터 테이블 조회 + 전체 재설정
void UpdateStats_Old(const FGS_StatRow& RuneStats)
{
UpdateStatWidget(FName("HP"), GetBaseValue("HP"), RuneStats.HP); // 비효율
}
// 개선: TMap 순회 + 부분 업데이트
void UpdateStats_New(const FGS_StatRow& RuneStats)
{
for (auto& StatPair : StatDataList) // O(1) 접근
{
FName StatName = StatPair.Key;
UGS_StatDataWidget* StatWidget = StatPair.Value;
float RuneValue = GetStatValueByName(RuneStats, StatName);
StatWidget->SetRuneStatData(RuneValue, 0.0f); // 변경된 부분만 업데이트
}
}
3단계: Widget 자체 상태 관리로 API 개선
// StatDataWidget이 자신의 상태를 관리하도록 개선
class UGS_StatDataWidget
{
private:
FName StatName;
float BaseValue; // 위젯이 자체 보관
float CurrRuneValue;
float CurrBonusValue;
public:
void InitStatWidget(FName InStatName, float InBaseValue); // 초기화
void SetRuneStatData(float InRuneValue, float InBonusValue = 0.0f); // 부분 업데이트
};
최종 아키텍처:
// 1. 초기화: 리플렉션으로 자동 스탯 탐지 (1회만)
InitStatList() {
리플렉션으로_스탯_목록_생성();
각_스탯별_위젯_생성_및_TMap_저장();
}
// 2. 업데이트: TMap 순회 + 부분 업데이트 (매번)
UpdateStats() {
TMap_순회() {
StatWidget->SetRuneStatData(변경된_값만); // 효율적!
}
}
성능 비교
룬 1개 배치 시
기존 방식:
- 데이터 테이블 조회: 5회
- 리플렉션 호출: 10회 (BaseValue 5회 + RuneValue 5회)
- UI 전체 재설정: 5회
개선 방식:
- 데이터 테이블 조회: 0회 (캐싱됨)
- 리플렉션 호출: 5회 (RuneValue만)
- UI 부분 업데이트: 5회
→ 약 2배 성능 향상
해결된 문제들
가장 큰 교훈
극한 최적화보다는 코드 가독성과 확장성이 더 중요함. 리플렉션은 약간의 성능 오버헤드가 있지만, 초기화 시에만 사용하고 나머지는 캐싱을 활용하면 충분히 실용적임. 그리고 API 설계할 때는 사용자(다른 개발자) 관점에서 직관적인지 고민해야 함
설계 철학 변화
// 구조체의 모든 프로퍼티 순회
UStruct* MyStruct = FMyStruct::StaticStruct();
for (TFieldIterator<FProperty> PropIt(MyStruct); PropIt; ++PropIt)
{
FProperty* Property = *PropIt;
// 타입별 처리
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Property))
{
FName PropName = Property->GetFName();
float Value = *FloatProp->ContainerPtrToValuePtr<float>(&StructInstance);
}
}
// 프로퍼티 이름으로 직접 접근
FProperty* Property = MyStruct->FindPropertyByName(PropertyName);
// TMap: O(1) 접근, 키로 직접 찾기 가능
StatDataList.Find(StatName); // 빠름
// TArray: O(n) 접근, 순차 탐색 필요
for (Widget in WidgetArray) {
if (Widget->GetStatName() == StatName) break; // 느림
}
단일 책임: 하나의 함수는 하나의 일만
직관성: 함수명으로 기능이 명확히 드러나야 함
확장성: 나중에 기능 추가가 쉬워야 함