TIL_048: 리팩터링

김펭귄·2025년 10월 20일

Today What I Learned (TIL)

목록 보기
48/104

오늘 학습 키워드

  • 리팩터링

1. 리팩터링

  • 동작은 그대로 유지한 채, 내부 구조를 바꾸어 코드 자체의 품질을 향상시키는 과정

코드 세분화 vs 통합

함수 추출하기

  • 하나의 이벤트로 묶을 수 있는 여러 과정은 하나의 함수로 추출하기
void Player::Update() {
    if (health <= 0) {
    	// 죽었을 때 다양한 처리들을 다 제거하고
        // isAlive = false;
        // animation.Play("Death");
        // sound.Play("DeathSFX");
        // gameManager.NotifyPlayerDied(this);
        
        // 하나의 함수로 묶기
        HandleDeath();
    }
}

변수 추출하기

  • 수식으로만 표현하지말고, 변수로 수식의 의도 알려주기

  • 나중에 본인도 다시봤을 때, 이해하기 쉬움

  • 디버깅할때도 중간값 보기 쉬움

// 수식을 보고 이해해야 함
float damage = baseDamage * (1 + criticalChance * 2.0f);

// 변수로 수식의 의도를 정확히 알림
float critMultiplier = 1 + criticalChance * 2.0f;
float damage = baseDamage * critMultiplier;

클래스 추출하기

  • 의존성을 낮추며, 클래스 각자는 자신의 역할만 하도록
class Player {
    FString Name;
    FString Address;
    FString Email; };
class ContactInfo {
    FString Address;
    FString Email; };

class Player {
    FString Name;
    ContactInfo Contact; };

함수, 변수, 클래스 인라인(Inline)하기

  • 추출을 하는 경우는 보통 복잡하거나 의도를 명확히 알리고 싶어서 함

  • 이미 의미를 충분히 알 수 있는 경우 추출하지 않는 것이 옳다

bool Player::IsDead() {
    return health <= 0;
}

void Player::Update() {
    if (IsDead())
        HandleDeath();
}
  • 하는 일도 없고, 의미도 이미 충분히 알 수 있으므로 Inline
void Player::Update() {
    if (health <= 0)
        HandleDeath();
}
  • 클래스의 경우, 멤버변수만 있고 기능은 따로 없거나, 사용도 빈번하게 일어나지 않는다면 Inline 하는것이 좋다
class Enemy
{
public:
    float X, Y;		// Position이라는 Class로 굳이 추출하지 않음
};

결정 기준

세분화가 적합한 상황통합이 적합한 상황
코드가 복잡하고 이해하기 어려울 때추상화가 오히려 복잡성을 증가시킬 때
재사용 가능성이 있을 때간단한 로직이 불필요하게 분리되어 있을 때
변경 가능성이 높은 부분사용되는 곳이 한 곳뿐인 단순한 코드
무엇과 어떻게를 분리할 필요가 있을 때함수/변수 이름이 실제 로직에 가치를 더하지 않을 때

객체 세분화 vs 통합

단계 쪼개기

  • 냉장고에서 음식 꺼내 먹는 것까지 한 번에 하더라도, 중간에 양념을 치고 싶을 때 수정하기 쉽지 않음

  • 따라서 한 번에 모든 단계를 하지말고, 단계별로 세분화해서 구현

void LoadAndDisplayItem() {
	// 한 번에 하는 경우
    Item item = LoadItemFromDisk("item.json");
    RenderItemOnScreen(item);
}
// 2단계로 나누어 처리
Item item = LoadItem();
DisplayItem(item)

반복문 쪼개기

  • 하나의 반복문에선 하나의 역할만 하도록

  • 반복문 2번 더 돌린다고 성능에 영향 별로 없음. 오히려, 의도를 파악하는 것이 더 중요한 문제

// 반복문 하나에 서로 관련 없는 작업 한 번에 하지 말기
// for (const Item& item : Inventory) {
//     item.CalculateWeight();
//     item.ApplyDurabilityDecay();
// }

// 차라리 반복문을 한 번 더 하는게 낫다
for (const Item& item : Inventory) {
    item.CalculateWeight();
}

for (const Item& item : Inventory) {
    item.ApplyDurabilityDecay();
}

함수를 클래스로 묶기

  • 관련 데이터와 동작이 함께 있는 것이 자연스러울 때 묶음
// 타이머 관련 함수이므로
// void StartTimer();
// void StopTimer();
// float GetElapsedTime();

// 하나의 클래스로 묶는다
class Timer {
public:
    void Start();
    void Stop();
    float GetElapsed() const;
private:
    float StartTime;
    float EndTime;
};

결정 기준

세분화가 적합한 상황통합이 적합한 상황
책임이 명확히 분리될 때강한 응집력이 필요할 때
다른 용도로 재사용 가능할 때관련 데이터와 동작이 함께 있는 것이 자연스러울 때
독립적으로 테스트하고 싶을 때공유 상태에 대한 관리가 필요할 때
복잡한 단계를 나누어 명확히 하고 싶을 때여러 작은 함수들이 항상 함께 사용될 때

간접 접근 vs 직접 접근

  • 객체의 data에 접근할 때, 어떻게 접근하는 것이 좋은지

변수 캡슐화

  • 중요한 data는 캡슐화하여 간접 접근하도록 해야함
player->SetHealth(100);	// 체력은 중요한 data
  • 인벤토리도 중요한 data이므로 객체로 만들어, 예외처리들도 해줘야함
class InventorySystem {
public:
    void AddItem(Item* NewItem) {
        if (!NewItem || IsFull()) return;
        Items.Add(NewItem);
    }

    const TArray<Item*>& GetItems() const { return Items; }
};

중재자/위임자 제거

  • Chain이 너무 길고, 내부 구조를 다 알아야하므로 직관적이도록
// 너무 긺
// FString name = gameManager->GetPlayerManager()->GetMainPlayer()->GetName();

FString name = gameManager->GetMainPlayerName();
FString GameManager::GetMainPlayerName() const
{
    return PlayerManager->GetMainPlayer()->GetName();
}

결정 기준

간접 접근이 적합한 상황직접 접근이 적합한 상황
데이터 검증이나 부가 처리가 필요할 때과도한 래퍼가 복잡성만 증가시킬 때
변경 추적이 필요할 때성능이 중요한 핫스팟일 때
향후 구현 변경 가능성이 있을 때단순한 데이터 구조에서
중복된 접근 로직이 여러 곳에 있을 때위임 체인이 너무 길어질 때

조건문 vs 다형성

조건문 합치기

  • 조건문에 여러 조건들이 있으면 의도를 이해하기 힘듦

  • 여러 조건문을 하나의 함수로 묶어 의도를 명확히 함

// 문 여는 조건인데 너무 많아 이해 힘듦
// if (player.HasKey() && player.IsAlive() && !player.IsStunned())
//     OpenDoor();

// 하나의 함수로 묶기
if (player.CanOpenDoor())
    OpenDoor();
  • 비슷한 동작하는 조건문도 합쳐주기
// 둘 다 비슷한 동작
// if (player.IsDead())
//     return false;

// if (player.IsDisconnected())
//     return false;

// 하나로 묶어주기
if (!player.CanParticipate())
	return false;

조건부를 다형성으로

  • 타입 추가가 많아지거나, 동작 확장 가능성이 높을 때
// 타입마다 조건문 넣는 것도 힘듦
float Enemy::GetDamage() {
    if (Type == "Orc") return Strength * 1.2f;
    if (Type == "Goblin") return Strength * 0.8f;
    return Strength;
}

// 다형성으로 쉽게 해결
class Enemy { virtual float GetDamage() const = 0; };

class Orc : public Enemy {
    float GetDamage() const override { return Strength * 1.2f; }
};

Null 객체 생성

  • 현재 캐릭터 있는지 없는지, 이름 있는지 없는지 등 항상 nullptr을 확인하는 것은 비효율적

  • 심지어 이름 없을 때 "None" 이름 반환하게 했는데 "Guest"로 바꾸라고 하면 일일이 다 수정해야함

if (player)
    ShowPlayerName(player->Name);
else
    ShowPlayerName("Guest");
  • 그래서 기존 player 객체와 동일한 interface를 제공하는 새로운 nullplayer 객체를 생성
class NullPlayer : public Player {
public:
    FString GetName() const override { return "Guest"; }
};

Player* GetPlayerOrNull(int id)
{
    Player* player = FindPlayer(id);
    if (!player)
    {
        static NullPlayer nullPlayer;
        return &nullPlayer;
    }
    return player;
}

// 이제 어디서든 null 걱정 없이 사용 가능
Player* player = GetPlayerOrNull(playerId);
ShowPlayerName(player->GetName());  // 항상 안전!

결정 기준

조건문이 적합한 상황다형성이 적합한 상황
단순한 분기 로직일 때타입별 동작 차이가 뚜렷할 때
일회성이거나 지역적인 결정일 때타입 추가가 자주 발생할 때
성능이 매우 중요한 곳일 때타입별 코드가 반복적으로 나타날 때
타입 배열이 고정적일 때동작이 확장될 가능성이 높을 때

상속 vs 위임

  • 상속 : 책임이 아주 강하게 연결된 관계. 부모가 바뀌면 자식도 바뀜. 런타임 도중 변경이 안 됨
  • 위임 : 책임이 약함. 훨씬 유연함. 기능 위주. 컴포넌트라 런타임 도중 변경 가능

필드, 메서드 부모로 올리기

  • 자식클래스들에서 공통으로 사용되는 필드와 메서드는 부모클래스로 올려 상속받기
class Enemy {
public:
    virtual void Die() { /* 공통 죽음 처리 */ }
    int Health;		// 공통 필드
};
// 이후 Orc, Goblin 자식들이 상속 받음

슈퍼클래스로 묶기

  • 다른 객체여도, 공통적인 부분이 있다면 슈퍼클래스로 묶기
// 다른 객체여도 공통 부분이 있음
class Player {
    FString Name;
    FVector Position;
};
class NPC {
    FString Name;
    FVector Position;
};
// 하나의 슈퍼클래스로 묶기
class ActorBase
{
    FString Name;
    FVector Position;
};

class Player : public ActorBase {};
class NPC : public ActorBase {};

서브클래스를 위임으로 바꾸기

class Enemy {
    virtual FString GetSound() const = 0;
};

class Orc : public Enemy {
    FString GetSound() const override { return "Roar"; }
};
  • 위와 같이 상속하면, 런타임 도중에 우는 소리를 바꾸는 것이 불가능

class SoundBehavior
{
public:
    virtual FString GetSound() const = 0;
};

class Enemy
{
    TUniquePtr<SoundBehavior> Sound;
    FString MakeSound() const { return Sound->GetSound(); }
};
  • 따라서, 컴포넌트로 소리내는 기능을 위임시키고 런타임 도중 변경 가능하도록 함

  • "has-a" 관계이면, 위임시키기

  • 상속은 가족관계, 위임은 계약관계

결정 기준

상속이 적합한 상황위임이 적합한 상황
명확한 "is-a" 관계가 있을 때"has-a" 관계나 행동 공유일 때
공통 기능이 많고 타입 계층이 필요할 때런타임에 동작을 변경해야 할 때
다형성을 활용한 확장이 자연스러울 때다중 상속과 같은 효과가 필요할 때
코드 재사용이 수직적일 때기존 클래스 변경 없이 기능 확장이 필요할 때
profile
반갑습니다

0개의 댓글