개인프로젝트/AFO_Refactor

[AFO_Refactor #1] Critical 버그 수정 - AnimNotify 권한 누락, Aurora 오타, 팀 비교 반전

Client Side 2026. 6. 10. 17:17

AFO_Refactor 개발일지 #1

Critical 버그 수정

AnimNotify 권한 누락 · Aurora 오타 · 팀 비교 반전 · BeginPlay 중복 호출 · 인코딩 변환

🎯 이번 작업 목표

#0에서 분석한 버그 목록 중 코드 수정으로 즉시 해결 가능한 것들을 먼저 정리했다. GAS 도입 전에 기존 코드의 명백한 오류들을 제거해야 리팩토링 기반이 안정적으로 잡힌다.

이번에 수정한 항목은 총 5개다.

# 버그 등급 파일
1 AnimNotify_AttackHit HasAuthority 누락 🔴 Critical AFAnimInstance.cpp
2 AFAurora::StopSprint Super 오타 🔴 Critical AFAurora.cpp
3 Multicast_NotifyDamage 팀 비교 반전 🟠 High AFAttributeComponent.cpp
4 BeginPlay InitializeCharacterData 중복 호출 🟠 High AFPlayerCharacter.cpp
5 소스 파일 인코딩 CP949 → UTF-8 with BOM 🟡 Medium 33개 파일

🔴 Fix 1 — AnimNotify_AttackHit HasAuthority 누락

문제

AnimNotify는 애니메이션이 재생되는 모든 클라이언트에서 발동된다. 멀티플레이에서 캐릭터 애니메이션은 Multicast로 모든 클라이언트에 복제되기 때문에, AnimNotify도 서버 + 모든 클라이언트에서 동시에 실행된다.

그런데 기존 코드는 권한 체크 없이 바로 DealDamage()를 호출하고 있었다.

BEFORE

void UAFAnimInstance::AnimNotify_AttackHit()
{
    if (auto Owner = TryGetPawnOwner())
    {
        if (auto Character = Cast<AAFPlayerCharacter>(Owner))
        {
            Character->DealDamage();  // HasAuthority() 체크 없음!
        }
    }
}

원인

DealDamage()는 내부에 HasAuthority() 가드가 있지만, 클라이언트에서 호출 자체가 되면 Dedicated Server 환경에서 불필요한 실행 경로가 생긴다. 특히 DealDamage() 내부 가드가 없는 버전이 혼재할 경우 크래시로 이어진다.

근본 원칙은 "서버 전용 함수는 진입부에서 차단"이다. 내부에서 막는 것보다 외부에서 먼저 막는 게 안전하다.

AFTER

void UAFAnimInstance::AnimNotify_AttackHit()
{
    APawn* Owner = TryGetPawnOwner();
    if (!Owner) return;
    if (!Owner->HasAuthority()) return;  // 서버에서만 실행

    AAFPlayerCharacter* Character = Cast<AAFPlayerCharacter>(Owner);
    if (!Character) return;

    Character->DealDamage();
}

🔴 Fix 2 — AFAurora::StopSprint Super 오타

문제

오로라 캐릭터의 StopSprint()에서 부모 함수를 잘못 호출하고 있었다. Super::StopSprint를 호출해야 하는데 Super::StartSprint를 호출하고 있었다.

BEFORE

void AAFAurora::StopSprint(const FInputActionValue& Value)
{
    Super::StartSprint(Value);  // ← StopSprint인데 StartSprint 호출
    bIsSprinting = false;
}

결과

스프린트 키를 떼면 부모의 StartSprint가 실행된다. StartSprintMaxWalkSpeed를 스프린트 속도로 올리는 함수이므로, 키를 떼도 빠른 속도가 유지된다. bIsSprinting = false로 플래그만 내려가고 실제 이동 속도는 복원되지 않는 버그.

AFTER

void AAFAurora::StopSprint(const FInputActionValue& Value)
{
    Super::StopSprint(Value);  // 수정
    bIsSprinting = false;
}

💡 교훈: 캐릭터 파생 클래스가 늘어날수록 이런 복사-붙여넣기 오타가 발생하기 쉽다. 이 문제는 이후 포스팅에서 다룰 Interface 도입으로 근본적으로 해결할 예정이다. bIsSprinting을 베이스 클래스로 올리면 파생 클래스에서 이 함수를 override할 필요 자체가 없어진다.

🟠 Fix 3 — Multicast_NotifyDamage 팀 비교 반전

문제

데미지 수치 UI의 색상이 반대로 표시되고 있었다. 적을 때리면 아군 색상, 아군을 때리면 적 색상으로 뜨는 버그.

BEFORE

// 같은 팀 공격 = 적 데미지로 표시 (반전)
if (MyPS->GetTeamID() == InstigatorPS->GetTeamID())
{
    bIsEnemyDamage = true;
}

AFTER

// 다른 팀(적) 공격 = 적 데미지로 표시 (정상)
if (MyPS->GetTeamID() != InstigatorPS->GetTeamID())
{
    bIsEnemyDamage = true;
}

🟠 Fix 4 — BeginPlay InitializeCharacterData 중복 호출

문제

팀 작업 중 병합 과정에서 동일한 블록이 두 번 들어간 것으로 추정된다. DataTable에서 캐릭터 데이터를 로드하는 함수가 BeginPlay()에서 두 번 실행됐다.

BEFORE

void AAFPlayerCharacter::BeginPlay()
{
    Super::BeginPlay();

    if (!CharacterKey.IsNone())
    {
        InitializeCharacterData(CharacterKey.ToString());  // 1번째
    }

    if (!CharacterKey.IsNone())
    {
        InitializeCharacterData(CharacterKey.ToString());  // 2번째 (중복)
    }
    ...
}

결과

DataTable 로드가 두 번 실행되면서 CharacterSkills 배열이 Empty() 후 다시 채워지는 과정이 반복된다. 부작용이 숨어있는 코드다. 두 번째 블록을 제거해서 정리했다.

🟡 Fix 5 — 소스 파일 인코딩 CP949 → UTF-8 with BOM

문제

팀원들이 Windows 한국어 환경에서 작성한 파일들이 CP949(EUC-KR 계열) 인코딩으로 저장되어 있었다. BOM이 없는 상태라 UTF-8 기반 도구에서 한글 주석이 모두 깨져 보였다.

// ❌ CP949 - UTF-8 도구에서 깨짐
// ¼­¹ö Àü¿ë: ½Ã°£ ¼³Á¤
void AAFGameState::SetRemainingTime(int32 NewTime)

해결

33개 파일(15 .cpp + 18 .h)을 UTF-8 with BOM으로 일괄 변환했다. UE5 MSVC 환경은 UTF-8 with BOM을 표준으로 처리하므로 컴파일에 영향 없다.

📌 UTF-8 with BOM vs UTF-8 without BOM
UE5 C++ 프로젝트에서는 BOM이 있는 UTF-8을 권장한다. BOM이 없으면 MSVC가 시스템 코드페이지(한국: CP949)로 파일을 해석해 한글이 깨질 수 있다.

📌 이번 작업에서 배운 것

AnimNotify의 실행 컨텍스트를 항상 의식해야 한다.

AnimNotify는 애니메이션이 재생되는 모든 머신에서 실행된다. Multicast로 복제된 몽타주라면 서버 + 전체 클라이언트에서 동시에 발동된다. 게임 로직(데미지, 스폰 등)은 반드시 HasAuthority()로 서버에서만 처리해야 한다.

팀 프로젝트 코드에는 병합 잔재가 남아있다.

중복 호출, 오타, 인코딩 문제는 모두 팀 작업 과정에서 발생한 전형적인 잔재다. 리팩토링 첫 단계에서 이런 것들을 먼저 정리해야 이후 구조 개선 작업이 안정적으로 진행된다.

📌 다음 포스팅

[AFO_Refactor #2] Cast 체인 → Interface 교체

매 프레임 DarkKnight/Mage/Aurora Cast 3번을 Interface로 교체. bIsSprinting을 베이스 클래스로 올려서 파생 클래스 override 자체를 제거하는 구조 개선.