클린 코드

주상돈·2025년 3월 27일

TIL

목록 보기
44/53

1. 기이한 이름 (Mysterious Name)

나쁜 예시

// 이름만 보고는 이게 뭔지 알 수가 없다
void DoIt(int x);

// 의미가 전혀 안 드러나는 변수들
float AAA;
int WTF;

좋은 예시

// '무엇을 하기 위한 함수인지'가 분명하다
void AttackEnemy(int DamageAmount);

// 변수의 역할이 명확하다
float CurrentHealth;
int EnemyCount;

2. 중복 코드 (Duplicated Code)

나쁜 예시

// 플레이어 데미지 처리
void PlayerTakeDamage(float Amount)
{
    Health -= Amount;
    if (Health <= 0)
    {
        Die();
    }
}

// 보스 데미지 처리
void BossTakeDamage(float Amount)
{
    Health -= Amount;
    if (Health <= 0)
    {
        SummonMinions(); // 보스라서 특별히 미니언을 소환
        Die();
    }
}

좋은 예시

// 공통 부모 클래스에서 데미지 로직을 통일
class ACharacterBase : public AActor
{
protected:
    virtual void OnDeath() { /* 비워두거나, 기본 처리 */ }
    
public:
    void TakeDamage(float Amount)
    {
        Health -= Amount;
        if (Health <= 0) 
        {
            OnDeath();
        }
    }
};

// 플레이어
class APlayerCharacter : public ACharacterBase
{
protected:
    virtual void OnDeath() override 
    {
        // 플레이어 전용 사망 처리
    }
};

// 보스
class ABoss : public ACharacterBase
{
protected:
    virtual void OnDeath() override
    {
        SummonMinions();
        // 보스 전용 사망 처리
    }
};

3. 긴 함수 (Long Function)

나쁜 예시

void AMyCharacter::Tick(float DeltaTime)
{
    // 1. 이동 처리
    // 2. 점프 처리
    // 3. 공격 처리
    // 4. 버프/디버프 처리
    // 5. 체력 체크
    // 6. 애니메이션 업데이트
    // ...
    // ...
    // (200줄이 넘어가요!)
}

좋은 예시

void AMyCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    
    HandleMovement(DeltaTime);
    HandleJump();
    HandleAttack();
    UpdateAnimation();
}

void AMyCharacter::HandleMovement(float DeltaTime)
{
    // 이동 관련 로직만 심플하게!
}

void AMyCharacter::HandleJump()
{
    // 점프 관련 로직만 모아둠
}

void AMyCharacter::HandleAttack()
{
    // 공격 로직
}

4. 긴 매개변수 목록 (Long Parameter List)

나쁜 예시

void InitWeapon(FString Name, float Damage, float FireRate, int32 AmmoCount, float ReloadTime, USkeletalMesh* Mesh, USoundBase* Sound)
{
    // 와, 많다 ...
}

좋은 예시

struct FWeaponData
{
    FString Name;
    float Damage;
    float FireRate;
    int32 AmmoCount;
};

struct FWeaponAssets
{
    USkeletalMesh* Mesh;
    USoundBase* Sound;
};

void InitWeapon(const FWeaponData& InData, const FWeaponAssets& InAssets)
{
    // 훨씬 깔끔!
}

5. 전역 데이터 (Global Data)

나쁜 예시

// 글로벌 관리자
UGameManager* GGameManager; // 전역!

// 아무 함수에서나 직접 접근해 값 변경
void IncreaseScore()
{
    GGameManager->Score += 10;
}

좋은 예시

// 언리얼 Subsystem을 이용한 예
UCLASS()
class UScoreSystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

private:
    int32 Score;

public:
    void AddScore(int32 Amount)
    {
        Score += Amount;
        // 점수가 변경됐음을 알리는 로직
    }

    int32 GetScore() const { return Score; }
};

// 사용 예시
void AEnemy::OnDefeated()
{
    if (UGameInstance* GI = GetGameInstance())
    {
        if (UScoreSystem* ScoreSys = GI->GetSubsystem<UScoreSystem>())
        {
            ScoreSys->AddScore(50);
        }
    }
}

6. 가변 데이터 (Mutable Data)

나쁜 예시

class APlayerCharacter : public ACharacter
{
public:
    // 마음대로 바꿀 수 있는 공공재(!)
    float Health;
    int32 Level;
};

void SomeRandomFunc(APlayerCharacter* Player)
{
    Player->Health = 99999.f;
    Player->Level = 999;
    // 이걸 발견하면, 팀원들: "누가 또 장난쳤냐!"
}

좋은 예시

class APlayerCharacter : public ACharacter
{
private:
    float Health;
    int32 Level;

public:
    float GetHealth() const { return Health; }
    int32 GetLevel() const { return Level; }

    void TakeDamage(float Amount)
    {
        Health = FMath::Max(0.0f, Health - Amount);
        // 데미지 받은 로직은 여기에만!
    }

    void LevelUp()
    {
        Level++;
        Health = 100.f * Level;
    }
};

7. 뒤엉킨 변경 (Divergent Change)

나쁜 예시

class AGameManager : public AActor
{
public:
    // (1) 데이터 관련
    void LoadPlayerData();
    void SavePlayerData();

    // (2) 게임플레이 관련
    void StartNewGame();
    void SpawnEnemies();

private:
    // (1) 데이터 관련 필드
    FString SaveFilePath;

    // (2) 게임플레이 관련 필드
    TArray<AEnemy*> ActiveEnemies;
};

좋은 예시

// (1) 데이터 전용 클래스
class UPlayerDataManager : public UGameInstanceSubsystem
{
public:
    void LoadPlayerData();
    void SavePlayerData();
    // ...
};

// (2) 게임플레이 전용 클래스
class UGameplayManager : public UGameInstanceSubsystem
{
public:
    void StartNewGame();
    void SpawnEnemies();
    // ...
};

8. 샷건 수술 (Shotgun Surgery)

나쁜 예시

class APlayerCharacter : public ACharacter
{
public:
    void TakeDamage(float Amount)
    {
        // 데미지 로직 1
    }
};

class AWeapon : public AActor
{
public:
    float CalculateDamage()
    {
        // 데미지 로직 2
        return 0.0f;
    }
};

class AMyGameMode : public AGameModeBase
{
public:
    void UpdateDamageLeaderboard()
    {
        // 데미지 로직 3
    }
};

좋은 예시

class UDamageSystem : public UObject
{
public:
    // 데미지 계산 로직을 한 군데 모음!
    float CalculateDamage(AWeapon* Weapon, ACharacter* Target);
    void ApplyDamage(AWeapon* Weapon, ACharacter* Target);
    void UpdateDamageLeaderboard(ACharacter* Damager, ACharacter* Target, float Amount);
};

9. 기능 편애 (Feature Envy)

나쁜 예시

class UDamageCalculator : public UObject
{
public:
    float CalculateDamageReduction(AMyCharacter* Character, float Damage)
    {
        // Character의 정보를 훨씬 더 많이 사용!
        float HealthPercent = Character->GetHealth() / Character->GetMaxHealth();
        float ArmorFactor   = Character->GetArmor() * 0.1f;
        // ...
        return Damage * (1.0f - ArmorFactor * HealthPercent);
    }
};

좋은 예시

class AMyCharacter : public ACharacter
{
public:
    float CalculateDamageReduction(float Damage) const
    {
        float HealthPercent = Health / MaxHealth;
        float ArmorFactor   = Armor * 0.1f;
        // ...
        return Damage * (1.0f - ArmorFactor * HealthPercent);
    }
};

class UDamageCalculator : public UObject
{
public:
    float CalculateDamageReduction(AMyCharacter* Character, float Damage)
    {
        // 캐릭터가 스스로 계산하게끔 위임!
        return Character->CalculateDamageReduction(Damage);
    }
};

10. 데이터 뭉치 (Data Clumps)

나쁜 예시

void FireWeapon(float Damage, float Range, float Accuracy);
void ShowWeaponStats(float Damage, float Range, float Accuracy);
void UpgradeWeapon(float& Damage, float& Range, float& Accuracy);

좋은 예시

// 무기 스탯 구조체
USTRUCT(BlueprintType)
struct FWeaponStats
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Damage;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Range;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Accuracy;
};

void FireWeapon(const FWeaponStats& Stats);
void ShowWeaponStats(const FWeaponStats& Stats);
void UpgradeWeapon(FWeaponStats& Stats);

11. 기본형 집착 (Primitive Obsession)

나쁜 예시

float Health;
float MaxHealth;

FString PhoneNumber; // 형식 검증이 전혀 없음

좋은 예시

// 체력을 표현하는 클래스
class FHealth
{
public:
    FHealth(float InCurrent, float InMax)
        : Current(InCurrent), Max(InMax) {}

    void ApplyDamage(float Amount)
    {
        Current = FMath::Max(0.f, Current - Amount);
    }
    // ...
private:
    float Current;
    float Max;
};

// 전화번호 전용 타입
class FPhoneNumber
{
public:
    FPhoneNumber(const FString& InNumber)
    {
        // 유효성 검사 + 포맷팅
    }
    // ...
private:
    FString Number;
};

12. 반복되는 스위치문 (Repeated Switches)

나쁜 예시

switch (WeaponType)
{
    case EWeaponType::Sword:
        return DoSwordAttack();
    case EWeaponType::Bow:
        return DoBowAttack();
    // ...
}

좋은 예시

// 다형성 활용...!
// 무기 베이스
class AWeapon : public AActor
{
public:
    virtual void Attack();
};

// 무기별 클래스
class ASword : public AWeapon
{
public:
    virtual void Attack() override { /* 칼 공격 로직 */ }
};

class ABow : public AWeapon
{
public:
    virtual void Attack() override { /* 활 공격 로직 */ }
};

13. 반복문 (Loops)

나쁜 예시

// 인벤토리에서 무거운 아이템을 찾아서 무게를 계산하는 과정
void ProcessHeavyItems()
{
    TArray<UItem*> Items = GetAllItems();
    TArray<UItem*> HeavyItems;

    // (1) 무거운 아이템 골라내기
    for (int32 i = 0; i < Items.Num(); i++)
    {
        if (Items[i]->Weight > 10.f)
        {
            HeavyItems.Add(Items[i]);
        }
    }

    // (2) 무게 총합 계산
    float TotalWeight = 0.f;
    for (int32 j = 0; j < HeavyItems.Num(); j++)
    {
        TotalWeight += HeavyItems[j]->Weight;
    }

    // (3) 너무 무거우면 효과 적용
    if (TotalWeight > 50.f)
    {
        ApplySlowEffect();
    }
}

좋은 예시

void ProcessHeavyItems()
{
    TArray<UItem*> Items = GetAllItems();

    // 필터링 함수(FilterByPredicate) 사용
    auto HeavyItems = Items.FilterByPredicate([](UItem* Item)
    {
        return Item->Weight > 10.f;
    });

    // 무게 계산 + 효과 적용
    float TotalWeight = 0.f;
    for (UItem* Item : HeavyItems)
    {
        TotalWeight += Item->Weight;
    }

    if (TotalWeight > 50.f)
    {
        ApplySlowEffect();
    }
}

14. 게으른 요소 (Lazy Element)

나쁜 예시

// 과도하게 중간함수만 존재
class AProjectile : public AActor
{
public:
    void Launch(const FVector& Dir, float Speed)
    {
        // 여기서 다시 다른 함수를 호출만 함
        LaunchProjectile(Dir, Speed);
    }

private:
    void LaunchProjectile(const FVector& Dir, float Speed)
    {
        // 실제 로직
        ProjectileMovement->Velocity = Dir * Speed;
    }
};

좋은 예시

class AProjectile : public AActor
{
public:
    void Launch(const FVector& Dir, float Speed)
    {
        ProjectileMovement->Velocity = Dir * Speed;
    }

private:
    UProjectileMovementComponent* ProjectileMovement;
};

15. 추측성 일반화 (Speculative Generality)

나쁜 예시

// 엄청나게 확장 가능한 무기 클래스... 그런데 전혀 안 씀
class AWeapon : public AActor
{
public:
    virtual void APlayer::PlayWeaponSound()
{
    USoundBase* AttackSound = GetEquippedWeaponSound();
    if (AttackSound)
    {
        UGameplayStatics::PlaySound2D(this, AttackSound);
    }
}

USoundBase* APlayer::GetEquippedWeaponSound()
{
    // 아래 호출부에서 직접 소리를 반환
    return Inventory ? Inventory->GetAttackSound() : nullptr;
}

USoundBase* UInventoryComponent::GetAttackSound()
{
    if (!EquippedWeapon) return nullptr;
    return EquippedWeapon->GetAttackSound();
}

USoundBase* AWeapon::GetAttackSound()
{
    return SoundData ? SoundData->AttackSound : nullptr;
}ttack();
    virtual void SpecialAttack();   // 안 씀
    virtual void UltimateAttack();  // 안 씀
    virtual void ElementalAttack(); // 안 씀
    // ...

    void SetDamage(float BaseDamage, float Crit, float Splash, float Chain, float Summon);
    // TODO: 추후에 쓸 수도?
};

좋은 예시

class AWeapon : public AActor
{
public:
    // 필요한 기능만
    void Attack();
    void SetDamage(float InDamage);

private:
    float Damage;
};

// 필요할 때 다른 무기 타입을 '상속'해서 만듦
class AMagicWeapon : public AWeapon
{
    void ElementalAttack();
};

16. 임시 필드 (Temporary Field)

나쁜 예시

class AEnemy : public ACharacter
{
public:
    // 일반 공격
    float Health;

    // 원거리 공격 전용 (근접 적은 안 씀)
    float ProjectileSpeed;
    UParticleSystem* ProjectileEffect;

    // 텔레포트 전용 (다른 적은 안 씀)
    float TeleportCooldown;
    float LastTeleportTime;
};

좋은 예시

// "컴포넌트"로 분리
class URangedAttackComponent : public UActorComponent
{
    float ProjectileSpeed;
    void ExecuteAttack();
};

class UTeleportComponent : public UActorComponent
{
    float TeleportCooldown;
    void ExecuteTeleport();
};

// 적 캐릭터
class AEnemy : public ACharacter
{
    float Health;
    URangedAttackComponent* RangedComp;   // 원거리 적만 붙임
    UTeleportComponent* TeleportComp;     // 텔레포트 적만 붙임
};

17. 메시지 체인 (Message Chains)

나쁜 예시

// 길~~게 이어진 참조
void APlayer::PlayWeaponSound()
{
    if (Inventory
        && Inventory->EquippedWeapon
        && Inventory->EquippedWeapon->SoundData
        && Inventory->EquippedWeapon->SoundData->AttackSound)
    {
        UGameplayStatics::PlaySound2D(this, Inventory->EquippedWeapon->SoundData->AttackSound);
    }
}

좋은 예시

void APlayer::PlayWeaponSound()
{
    USoundBase* AttackSound = GetEquippedWeaponSound();
    if (AttackSound)
    {
        UGameplayStatics::PlaySound2D(this, AttackSound);
    }
}

USoundBase* APlayer::GetEquippedWeaponSound()
{
    // 아래 호출부에서 직접 소리를 반환
    return Inventory ? Inventory->GetAttackSound() : nullptr;
}

USoundBase* UInventoryComponent::GetAttackSound()
{
    if (!EquippedWeapon) return nullptr;
    return EquippedWeapon->GetAttackSound();
}

USoundBase* AWeapon::GetAttackSound()
{
    return SoundData ? SoundData->AttackSound : nullptr;
}

18. 중재자 (Middle Man)

나쁜 예시

class AMyPlayerController : public APlayerController
{
public:
    void MoveForward(float Value)  { Character->MoveForward(Value); }
    void MoveRight(float Value)    { Character->MoveRight(Value); }
    void Jump()                    { Character->Jump(); }
    void StartFire()               { Character->StartFire(); }
    void StopFire()                { Character->StopFire(); }
    // ...
private:
    AMyCharacter* Character;
};

좋은 예시

// 직접 캐릭터에 입력 바인딩
void AMyPlayerController::SetupInputComponent()
{
    Super::SetupInputComponent();

    // 현재 캐릭터 가져오기
    AMyCharacter* MyChar = Cast<AMyCharacter>(GetCharacter());
    if (MyChar && InputComponent)
    {
        // 캐릭터가 필요한 입력을 직접 바인딩
        MyChar->SetupPlayerInput(InputComponent);
    }
}

void AMyCharacter::SetupPlayerInput(UInputComponent* PlayerInputComponent)
{
    PlayerInputComponent->BindAxis("MoveForward", this, &AMyCharacter::MoveForward);
    PlayerInputComponent->BindAxis("MoveRight", this, &AMyCharacter::MoveRight);
    // ...
}

19. 내부자 거래 (Insider Trading)

나쁜 예시

// AEnemy가 APlayerCharacter의 내부 변수까지 막 참조
void AEnemy::Attack(APlayerCharacter* Player)
{
    if (!Player->bIsInvulnerable)
    {
        float Damage = AttackDamage - Player->EquippedArmor->DamageReduction;
        Player->CurrentHealth -= Damage;

        // UI도 직접 갱신?!
        Player->PlayerHUD->UpdateHealthBar(Player->CurrentHealth, Player->MaxHealth);
    }
}

좋은 예시

// AEnemy는 '공개된 함수'만 호출
void AEnemy::Attack(APlayerCharacter* Player)
{
    if (Player && Player->CanBeAttacked())
    {
        Player->ReceiveDamage(AttackDamage);
    }
}

// Player 내부
bool APlayerCharacter::CanBeAttacked() const
{
    return !bIsInvulnerable;
}

void APlayerCharacter::ReceiveDamage(float Damage)
{
    // 갑옷 계산, HUD 업데이트 등 내부적으로 처리
    float ActualDamage = EquippedArmor ?
        EquippedArmor->ApplyReduction(Damage) : Damage;

    CurrentHealth = FMath::Max(0.f, CurrentHealth - ActualDamage);
    PlayerHUD->UpdateHealth(CurrentHealth, MaxHealth);
}

20. 거대한 클래스 (Large Class)

나쁜 예시

class AGameCharacter : public ACharacter
{
public:
    // 이동 처리
    void MoveForward(float Value);
    void MoveRight(float Value);
    // 전투 처리
    void Attack();
    void Reload();
    // 인벤토리 처리
    void AddItem(UItem* Item);
    void RemoveItem(UItem* Item);
    // 퀘스트 처리
    void AcceptQuest(UQuest* Quest);
    void CompleteQuest(UQuest* Quest);
    // 대화 처리
    void StartDialogue();
    void EndDialogue();
    // ... 500줄 넘게 계속 ...
};

좋은 예시

class AGameCharacter : public ACharacter
{
public:
    AGameCharacter();
    // 핵심 동작만 유지, 나머지는 컴포넌트에 맡김
private:
    UPROPERTY()
    UMovementComponent* MovementComp;

    UPROPERTY()
    UCombatComponent* CombatComp;

    UPROPERTY()
    UInventoryComponent* InventoryComp;

    UPROPERTY()
    UQuestComponent* QuestComp;
    // ...
};

21. 서로 다른 인터페이스의 대안 클래스들 (Alternative Classes with Different Interfaces)

나쁜 예시

class ARangedWeapon
{
public:
    void FireProjectile();
    void Reload();
};

class AMeleeWeapon
{
public:
    void PerformAttack();
    void SharpenBlade();
};

// 플레이어 캐릭터
void APlayerCharacter::Attack()
{
    if (CurrentRangedWeapon)
        CurrentRangedWeapon->FireProjectile();
    else if (CurrentMeleeWeapon)
        CurrentMeleeWeapon->PerformAttack();
}

좋은 예시

class AWeapon : public AActor
{
public:
    virtual void Attack() = 0;  // 추상 메서드
    virtual void Reload() {}    // 기본 구현(근접 무기는 비워둘 수도)
};

class ARangedWeapon : public AWeapon
{
public:
    virtual void Attack() override { /* 원거리 공격 */ }
    virtual void Reload() override { /* 탄약 보충 */ }
};

class AMeleeWeapon : public AWeapon
{
public:
    virtual void Attack() override { /* 근접 공격 */ }
    // Reload()는 오버라이드 안 해도 됨(불필요)
};

// 플레이어 캐릭터
void APlayerCharacter::Attack()
{
    if (CurrentWeapon)
    {
        CurrentWeapon->Attack(); // 무기 종류 관계없이 한 번에 호출
    }
}

22. 데이터 클래스 (Data Class)

나쁜 예시

class FPlayerStats
{
public:
    float GetHealth() const { return Health; }
    void SetHealth(float H) { Health = H; }
    // ...
private:
    float Health;
    float MaxHealth;
    // ...
};

// 플레이어가 이 Stats를 막 조작
void APlayerCharacter::TakeDamage(float Damage)
{
    float NewHealth = PlayerStats.GetHealth() - Damage;
    PlayerStats.SetHealth(FMath::Max(0.f, NewHealth));
    // 레벨업 체크도 여기서 하고...
}

좋은 예시

class FPlayerStats
{
public:
    // 함수 안에서 로직 처리
    void ApplyDamage(float Damage)
    {
        float ActualDamage = Damage * (1.0f - Defense / 100.f);
        Health = FMath::Max(0.f, Health - ActualDamage);
    }

    bool IsDead() const { return Health <= 0.f; }

    // ...
private:
    float Health;
    float Defense;
    // ...
};

// 플레이어
void APlayerCharacter::TakeDamage(float Damage)
{
    PlayerStats.ApplyDamage(Damage);
    if (PlayerStats.IsDead())
    {
        Die();
    }
}

23. 상속 포기 (Refused Bequest)

나쁜 예시

class AWeapon
{
public:
    virtual void Attack();
    virtual void Reload(); // 근접 무기는 재장전 필요 X
};

class AMeleeWeapon : public AWeapon
{
public:
    virtual void Reload() override
    {
        // 근접 무기에선 의미가 없으니 비워둠
    }
};

좋은 예시

class ABaseWeapon : public AActor
{
public:
    virtual void Attack() = 0; // 모든 무기는 공격 기능
};

class ARangedWeapon : public ABaseWeapon
{
public:
    virtual void Attack() override { /* 발사 로직 */ }
    void Reload() { /* 탄약 보충 */ }
};

class AMeleeWeapon : public ABaseWeapon
{
public:
    virtual void Attack() override { /* 근접 공격 로직 */ }
    // Reload() 자체가 없음!
};

24. 주석 (Comments)

나쁜 예시

void AEnemy::UpdateBehavior()
{
    // 1. 플레이어 위치 가져오기
    // 2. 시야 범위 확인
    // 3. 시야 각도 계산
    // 4. 라인 트레이스 해서 장애물 있는지
    // 5. 없으면 공격, 있으면 패트롤
    // 50줄짜리 함수에 각 단계별 설명이 잔뜩 → 너무 복잡.
    ...
}

좋은 예시

void AEnemy::UpdateBehavior()
{
    if (CanSeePlayer())
    {
        EngagePlayer();
    }
    else
    {
        PatrolArea();
    }
}

bool AEnemy::CanSeePlayer()
{
    return IsWithinSightRange() && IsInFieldOfView() && HasLineOfSight();
}

0개의 댓글