[25.05.26] :: 가디언 앤 시커 프로젝트 07

chooha·2025년 5월 27일

가디언앤시커

목록 보기
7/25

📝 개발일지 - 룬 시스템 스탯 아키텍처 최적화 트러블슈팅

👨‍💻 오늘의 개발 작업

오늘은 룬 시스템의 스탯 업데이트 성능을 개선하고 확장성을 높이는 작업을 했음
처음엔 단순하게 하드코딩으로 구현했다가, 나중에 새로운 스탯이 추가될 때마다 코드를 수정해야 하는 문제와 성능 이슈를 발견해서 전체적인 아키텍처를 개선했음


💡 오늘의 5분 기록

1. 문제 확인 - 하드코딩된 스탯 관리의 한계

처음 구현한 스탯 업데이트 시스템을 보니 확장성에 문제가 있었음

// 기존 하드코딩 방식
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);
}

문제점들

  • 새 스탯 추가 시 3곳을 수정해야 함 (초기화, 업데이트, 위젯 생성)
  • 매번 데이터 테이블 조회로 성능 저하
  • SetStatData()로 전체 값을 매번 재설정하는 비효율성

2. 원인 파악 - 설계 방향성 고민

성능 문제 분석

// 룬 1개 배치할 때마다 발생하는 작업들
UpdateStats() {
    for (5개 스탯) {
        데이터테이블.FindRow();      // 5회 데이터베이스 조회
        GetStatValueByName();        // 5회 리플렉션 호출  
        SetStatData(모든_값_재설정);  // 5회 전체 UI 업데이트
    }
}

확장성 문제

  • 하드코딩된 스탯 목록으로 인한 유지보수 어려움
  • 새로운 스탯 추가 시 여러 파일 수정 필요
  • 실수로 빠뜨리는 스탯 발생 가능성

API 설계 고민

// 현재 방식: 전체 재설정
StatWidget->SetStatData(StatName, BaseValue, RuneValue, BonusValue);

// vs 

// 부분 업데이트 방식
StatWidget->SetRuneStatData(RuneValue, BonusValue);

3. 해결 과정 - 리플렉션 + TMap + 부분 업데이트

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);  // 부분 업데이트
};

4. 문제 해결 - 성능과 확장성 모두 확보

최종 아키텍처:

// 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배 성능 향상

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);

성능 최적화 전략

  1. 캐싱: 자주 사용하는 데이터는 미리 저장
  2. 부분 업데이트: 전체가 아닌 변경된 부분만 갱신
  3. 리플렉션 최소화: 초기화 시에만 사용, 런타임에는 캐시 활용

TMap vs TArray 성능 비교

// TMap: O(1) 접근, 키로 직접 찾기 가능
StatDataList.Find(StatName);  // 빠름

// TArray: O(n) 접근, 순차 탐색 필요
for (Widget in WidgetArray) {
    if (Widget->GetStatName() == StatName) break;  // 느림
}

API 설계 원칙

0개의 댓글