김하연 튜터님 강의 - ‘리팩터링의 두 얼굴 - 세분화와 통합 사이에서 살아남기’
리팩토링의 서로 상반되는 핵심 결정 원칙들을 알아보자
김하연 튜터님의 Notion 자료를 바탕으로 강의를 들으며
수정 및 재작성한 블로깅용 글
-
- 리팩토링
- 기능을 고친다 x , 버그 수정 x
코드 품질을 올린다!
(가독성, 유지보수성)
- 리팩토링
-
잠재적인 기술 부채를 미리미리 털어내는 것…!
(불편하다를 넘어서서 작업 속도에 영향을 줄 수 있으므로) -
리팩토링은 중간을 찾는 것에 가까움
ex)
함수로 많이 나누세요 vs 함수 호출은 잠재적으로 성능 영향 -
결국은 구조와의 싸움
= 끝없는 반복과 개선 - 코드를 보고 한 번에 알 수 있느냐~
아니라면 리팩토링의 대상이다!
1. 코드 세분화 vs. 코드 통합
- 나눌까 말까
- 안나누자니 함수가 너무 길어지는데?
- 나누니까 너무 함수를 많이 호출하는데?
- 안나누자니 함수가 너무 길어지는데?
코드 세분화
요점은 코드를 ‘나누는 것’
💡 함수 추출하기 (Extract Function)
🧨 Before
void Player::Update()
{
if (health < 0)
{
isAlive = false;
animation.Play("Death");
sound.Play("DeathSFX");
gameManager.NotifyPlayerDied(this);
}
}
- 업데이트 안에 Player 죽음 내용 까지 포함되는데?
✅ After
void Player::Update()
{
if (health < 0)
{
HandleDeath();
}
}
void Player::HandleDeath()
{
isAlive = false;
animation.Play("Death");
sound.Play("DeathSFX");
gameManager.NotifyPlayerDied(this);
}
- 함수 하나 파서 ‘의도’를 챙겨주자
- 별거 아니지만 ‘함수명’을 통해 가독성을 올릴 수 있음
💡 변수 추출하기 (Extract Variable)
🧨 Before
float damage = baseDamage * (1 + criticalChance * 2.0f);
- 저 계산 결과는 뭘 의미하는거지?
- 저 수치를 디버깅으로 보기 힘들다
✅ After
float critMultiplier = 1 + criticalChance * 2.0f;
float damage = baseDamage * critMultiplier;
- 아 크리티컬 배율이구나
- 디버깅에 저 변수명 넣고 조사식 돌려도 됨
💡 클래스 추출하기 (Extract Class)
🧨 Before
class Player
{
FString Name;
FString Address;
FString Email;
};
- 게임 캐릭터가 이메일과 주소를 왜 가져…?
✅ After
class ContactInfo
{
FString Address;
FString Email;
};
class Player
{
FString Name;
ContactInfo Contact;
};
접속 정보
로 나누어 별도의 개념으로 관리하자!- 다만 클래스 명을 너무 ‘세부적’으로 작성할 필요는 없음
코드 통합
쪼갤만큼 쪼갰으면 붙여!
💡 함수 인라인하기 (Inline Function)
🧨 Before
bool Player::IsDead()
{
return health <= 0;
}
void Player::Update()
{
if (IsDead())
{
HandleDeath();
}
}
- 한줄인데? 굳이?
- 이미 체력 < 0이면 죽는게 직관적이지 않나?
- 이거 굳이 함수 호출해야 하나?
✅ After
void Player::Update()
{
if (health <= 0)
{
HandleDeath();
}
}
- 여기서만 쓰면 그냥 이렇게 쓰는게 더 좋지 않아?
- 물론 가독성을 주거나, 여러 사용처가 있다면 함수화시키는 것이 더 좋을 수 있음
(너무 복잡한 수식은 그냥 함수화해서 가독성 챙겨주는것은 괜찮음)
- 물론 가독성을 주거나, 여러 사용처가 있다면 함수화시키는 것이 더 좋을 수 있음
💡 변수 인라인하기 (Inline Variable)
🧨 Before
int priceAfterDiscount = originalPrice - discountAmount;
int finalPrice = priceAfterDiscount + shippingFee;
- 한줄만 쓰이고 재사용 안하는 것 같은데?
- 각각의 변수명이 명확한 것 같은데
✅ After
int finalPrice = (originalPrice - discountAmount) + shippingFee;
- 어차피 의미가 명확한데 그냥 직접 쓰자…
- 다만 수식이 복잡하다면 의미를 부여하는 것은 옳다
- 다만 수식이 복잡하다면 의미를 부여하는 것은 옳다
💡 클래스 인라인하기 (Inline Class)
🧨 Before
class Position
{
public:
float X, Y;
};
class Enemy
{
Position Pos;
};
- Position이 x,y만 사용하지 않아?
그리도 Enemy만 사용하는데?
✅ After
class Enemy
{
public:
float X, Y;
};
- 그냥 사용하거나 ‘구조체’로 사용하는게 깔끔하지 않아?
결정 기준
세분화가 적합한 상황 | 통합이 적합한 상황 |
---|---|
코드가 복잡하고 이해하기 어려울 때 | 추상화가 오히려 복잡성을 증가시킬 때 |
재사용 가능성이 있을 때 | 간단한 로직이 불필요하게 분리되어 있을 때 |
변경 가능성이 높은 부분 | 사용되는 곳이 한 곳뿐인 단순한 코드 |
무엇과 어떻게를 분리할 필요가 있을 때 | 함수/변수 이름이 실제 로직에 가치를 더하지 않을 때 |
-
코드 중복은 죄악이기에 재사용성을 높이기에 세분화가 필요
-
다만 현재 하나만 사용하고 있다면 굳이 세분화할 필요는 없음
- 무엇 과 어떻게 를 구분하는 것도 방법
- 각 코드의 ‘목적’에 따른 분류를 잊지 말 것!
- 다만 목적성이 명확하더라도 ‘한 곳’만 쓰이고 있다면 통합을 고려하기
- 각 코드의 ‘목적’에 따른 분류를 잊지 말 것!
- 보통 대형 프로젝트에서 고려
- 소형프로젝트라면 ‘공부용’으로서의 가치 정도
- 소형프로젝트라면 ‘공부용’으로서의 가치 정도
2. 객체 세분화 vs. 객체 통합 🤔
- 객체 뭉칠까 말까
- 객체를 다루는 접근법에 대하여 생각해보자
- 객체를 다루는 접근법에 대하여 생각해보자
객체 세분화
💡 매개변수 객체 만들기 (Introduce Parameter Object)
🧨 Before
void CreateQuest(FString title, FString description, int rewardGold, int exp);
- 이전 강의에서 보았던 ‘많은 매개변수’
- 데이터 추가시, 함수 매개변수가 추가…?
✅ After
void CreateQuest(const FQuestData& Quest);
struct FQuestData
{
FString Title;
FString Description;
int RewardGold;
int Exp;
};
- 순서 기억을 굳이 할 필요 없음!
- 구조체 값만 추가/수정 하면 되기에 더 편리
- 구조체 이름도 어느정도 신경쓰고
구조체의 데이터 구조도 신경쓸것!
(구조체 안에 구조체 있는 건….)
- 구조체 이름도 어느정도 신경쓰고
💡 단계 쪼개기 (Split Phase)
🧨 Before
void LoadAndDisplayItem()
{
Item item = LoadItemFromDisk("item.json");
RenderItemOnScreen(item);
}
- 아이템 Load 후, Render
- 중간 단계 못나누나…?
✅ After
Item LoadItem();
void DisplayItem(const Item& item);
Item item = LoadItem();
DisplayItem(item)
-
함수로 쪼개자
(함수에 and 붙이지 말자..!) -
2개 이상의 역할을 같이 한다면 쪼개는 것 고려
💡 반복문 쪼개기 (Split Loop)
🧨 Before
for (const Item& item : Inventory)
{
item.CalculateWeight();
item.ApplyDurabilityDecay();
}
-
효율적으로 보이나
왜 같이 하지…? -
성능 걱정?
- 프로파일링을 보고 생각하기
- 이게 걱정될 수준이면 AI 나 파티클을 먼저 고려해야 하는 상황
- 프로파일링을 보고 생각하기
✅ After
for (const Item& item : Inventory)
{
item.CalculateWeight();
}
for (const Item& item : Inventory)
{
item.ApplyDurabilityDecay();
}
- 명확한 목적을 가지게 ‘나누는 것’을 고려하기
(분리하기 쉬우며, 멀티스레딩 환경에서 락 등을 걸기 더 쉬운 구조)
객체 통합
쪼개다 보면
합칠 것도 보인다!
(비슷한 개념끼리 뭉치는 것)
💡 여러 함수를 클래스로 묶기 (Combine Functions into Class)
🧨 Before
void StartTimer();
void StopTimer();
float GetElapsedTime();
- 전부 시간 관련이네?
- 각자 비슷한 변수를 사용할 것 같은데…
✅ After
class Timer
{
public:
void Start();
void Stop();
float GetElapsed() const;
private:
float StartTime;
float EndTime;
};
- 유틸리티 클래스로 만들어서 별도로 관리하도록 하자
💡 여러 함수를 변환 함수로 묶기 (Combine into Transform Function)
🧨 Before
float GetBaseDamage();
float GetCritBonus();
float GetTotalDamage()
{
return GetBaseDamage() + GetCritBonus();
}
- 함수가 함수를 부르는…?
- 쪼개놓고 어차피 다시 합치는 거 아닌가?
- 이거 거의 실시간으로 돌리는 함수잖아!
✅ After
float CalculateTotalDamage()
{
return baseDamage + (baseDamage * critChance * 2.0f);
}
- 중간 단계가 그렇게 중요한가?
- 괜한 함수 만들지 말고 하나로 합쳐서 관리하자
결정 기준
세분화가 적합한 상황 | 통합이 적합한 상황 |
---|---|
책임이 명확히 분리될 때 | 강한 응집력이 필요할 때 |
다른 용도로 재사용 가능할 때 | 관련 데이터와 동작이 함께 있는 것이 자연스러울 때 |
독립적으로 테스트하고 싶을 때 | 공유 상태에 대한 관리가 필요할 때 |
복잡한 단계를 나누어 명확히 하고 싶을 때 | 여러 작은 함수들이 항상 함께 사용될 때 |
세분화 고려
- 하나의 클래스가 너무 많은 일을 하지 않는가?
-
다른 클래스도 이 기능이 필요해 보이는데?
(ex : 전투, 대미지 계산, 시간 관리 등등)
(협업하는데 각각 따로 만들면…) -
독립적인 기능 테스트가 필요한 경우
(이건 게임에서 돌려봐야 알 것 같아!) - 복잡한 단계가 많음
(로딩, 파싱 등)
통합 고려
-
단계가 너무 명확해
(중간에 나눌 필요 x) -
관련 데이터들끼리 ‘따로’ 나뉘어져 있는 경우
-
데이터와 동작이 ‘같이’ 있는것이 당연할 때
(아이템 + 동작이 필요하다면 인벤토리 클래스 제작 고려) -
여러 작은 함수들이 사용된다면
그리고 각각 한군데만 사용된다면
일단 뭉치고 생각해보자
결국 중요한 건 나중에 알아볼 수 있는 가독성!
3. 간접 접근 vs. 직접 접근 🤤
캡슐화?
불필요 중간 다리 제거?
간접 접근
💡 변수 캡슐화하기 (Encapsulate Variable)
🧨 Before
player->health = 100;
- 이 중요한걸 외부 클래스가 왜 만져…?
✅ After
player->SetHealth(100);
void Player::SetHealth(int NewHealth)
{
health = FMath::Clamp(NewHealth, 0, maxHealth);
}
-
마음대로 바뀌면 매우 곤란한 데이터는
반드시 간접 접근! -
Setter 로 유용한 범위를 검사하고 보장해주자
(디버깅 가능성을 만들어두기)
(수정 부분만 디버깅 포인트 잡아주면 됨)
💡 레코드 캡슐화하기 (Encapsulate Record)
🧨 Before
struct PlayerData
{
FString Name;
int Level;
};
PlayerData player;
player.Level = 5; // 아무나 막 건드림
- 중요한 데이터를 구조체로 사용하면
좀…
✅ After
class Player
{
public:
int GetLevel() const { return Level; }
void SetLevel(int NewLevel) { Level = FMath::Max(1, NewLevel); }
private:
int Level;
};
- 중요한 데이터라면 그냥 클래스에 넣어두어
캡슐화의 대상으로 만들기
💡 컬렉션 캡슐화하기 (Encapsulate Collection)
🧨 Before
TArray<Item*> Inventory;
Inventory.Add(Sword);
Inventory.Add(Potion);
- 이거 null도 들어가지 않나?
✅ After
class InventorySystem
{
public:
void AddItem(Item* NewItem)
{
if (!NewItem || IsFull()) return;
Items.Add(NewItem);
}
const TArray<Item*>& GetItems() const { return Items; }
private:
TArray<Item*> Items;
bool IsFull() const { return Items.Num() >= MaxSlots; }
};
- 버그가 많이 나올수록 단단하고 까다로운 접근 방식을 세워야 안전함
(게임의 핵심 상태 관리이므로)
직접 접근
💡 중개자 제거하기 (Remove Middle Man)
🧨 Before
FString name = gameManager->GetPlayerManager()->GetMainPlayer()->GetName();
-
호출 체이닝…
-
nullcheck를 하더라도 길어지면 호출 체이닝은 맞음
✅ After
FString name = gameManager->GetMainPlayerName();
FString GameManager::GetMainPlayerName() const
{
return PlayerManager->GetMainPlayer()->GetName();
}
- 필요에 따라 Getter를 통해 바로 반환하는 내용이 필요
💡 위임 숨기기 (Hide Delegate)
🧨 Before
FString zip = player.ContactInfo.Address.ZipCode;
- 내부 구조가 너무 노출되는 상황
- 만약 내부 구조가 바뀌면 이 코드는 바로 수정되어야 함
✅ After
FString zip = player.GetZipCode();
FString Player::GetZipCode() const
{
return ContactInfo.Address.ZipCode;
}
- 그냥 Player 쪽에서 반환받는 값을 사용하는 것이
유지보수적으로 좋음
결정 기준
간접 접근이 적합한 상황 | 직접 접근이 적합한 상황 |
---|---|
데이터 검증이나 부가 처리가 필요할 때 | 과도한 래퍼가 복잡성만 증가시킬 때 |
변경 추적이 필요할 때 | 성능이 중요한 핫스팟일 때 |
향후 구현 변경 가능성이 있을 때 | 단순한 데이터 구조에서 |
중복된 접근 로직이 여러 곳에 있을 때 | 위임 체인이 너무 길어질 때 |
간접 접근
-
중요한 데이터는 기본적으로 간접 접근 추천
(Gold, HP 등) -
바뀔 가능성이 많은 것들
-
호출 체이닝이 많다면
함수를 만드는 것을 고려
직접 접근
-
과잉 설계
(래퍼가 지나치게 많은 경우) -
성능이 정말 중요한 경우
-
단순 데이터 구조, 위임 접근이 너무 길어진다는 등에 고려
4. 조건문 vs. 다형성
프로그램 흐름 분기의 다양한 방식
- 어느 쪽이 무조건 좋지는 않음
조건문
- 조금 더 예쁜 조건문을 써보자
(내부 조건이 복잡하면 대충 읽고 이해하지 않고 넘어가는 편…)
💡 조건문 분해하기 (Decompose Conditional)
🧨 Before
if (player.HasKey() && player.IsAlive() && !player.IsStunned())
{
OpenDoor();
}
- 나쁘지는 않지만 조금 더 좋은 가독성을 챙길순 없나?
✅ After
bool Player::CanOpenDoor() const
{
return HasKey() && IsAlive() && !IsStunned();
}
if (player.CanOpenDoor())
{
OpenDoor();
}
-
위의 조건들은 전부 ‘문’을 열 수 있는지를 확인하는 것이므로
명확한 함수명을 준다 -
요점은 ‘문’을 열 수 있는가에 가독성 확보
(의도 + 재사용성 가능성)
💡 조건식 통합하기 (Consolidate Conditional Expression)
🧨 Before
if (player.IsDead())
{
return false;
}
if (player.IsDisconnected())
{
return false;
}
return true;
-
저 위에 2개 어차피 return false인데
굳이 떨어트려야 하나? -
검사도 Player에서만 하네?
✅ After
bool Player::CanParticipate() const
{
return !(IsDead() || IsDisconnected());
}
- 그냥 플레이어에서 묶어서 처리하자
다형성
- 난 조건문 싫어…
- if문이 10줄 넘어가면 개편해야 해…!
💡 조건부 로직을 다형성으로 바꾸기 (Replace Conditional with Polymorphism)
🧨 Before
float Enemy::GetDamage()
{
if (Type == "Orc") return Strength * 1.2f;
if (Type == "Goblin") return Strength * 0.8f;
return Strength;
}
- 적 타입에 따라 대미지 적용이 다르게 됨
- 이건 잠재적으로 Switch 구문 처럼 된다!
✅ After
class Enemy { virtual float GetDamage() const = 0; };
class Orc : public Enemy
{
float GetDamage() const override { return Strength * 1.2f; }
};
class Goblin : public Enemy
{
float GetDamage() const override { return Strength * 0.8f; }
};
// 호출부
Enemy* enemy = GetRandomEnemy();
float damage = enemy->GetDamage();
-
각각의 적들이 별도의 타입을 가지게 하는것이 더 OOP적이야!
(타입을 굳이 몰라도 됨) -
다른 Enemy 클래스 추가되더라도
이 코드는 수정될 필요 없으므로 -
다만 적 타입이 300개가 넘어가게 되면
클래스의 남용…임
(전략 패턴 or 데이터 기반으로 문제를 해결해야 함)
💡 특이 케이스 추가하기 (Introduce Special Case)
🧨 Before
if (player)
{
ShowPlayerName(player->Name);
}
else
{
ShowPlayerName("Guest");
}
-
nullCheck에 따른 변화법
-
이게 코드 곳곳에 뿌려져 있다면
생각보다 머리 아픔
(기획자 : Guest 말고 다른 이름으로 바꾸죠)
✅ After
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()); // 항상 안전!
-
지역 static을 통해 nullPlayer를 그냥 가볍게 만들어서
계속 반환
(객체 하나 비용) -
GetName을 그냥 바로 반환하면 됨
-
지저분한 if문보단 null용 개체를 만드는 것이 좋을 수는 있음
(물론 케바케)
결정 기준
조건문이 적합한 상황 | 다형성이 적합한 상황 |
---|---|
단순한 분기 로직일 때 | 타입별 동작 차이가 뚜렷할 때 |
일회성이거나 지역적인 결정일 때 | 타입 추가가 자주 발생할 때 |
성능이 매우 중요한 곳일 때 | 타입별 코드가 반복적으로 나타날 때 |
타입 배열이 고정적일 때 | 동작이 확장될 가능성이 높을 때 |
조건문 고려
-
분기가 간단하다면 굳이 다형성을?
-> 오버 엔지니어링 -
성능이 중요할 때, 일회성일 때 등
-
난이도 쉬움,중간,어려움
(새로 추가 안될 것 같음)
다형성 고려
-
타입별 나눠야 하는 동작이 ‘뚜렷’한 경우
(오크는 물리 공격, 드래곤은 브레스~) -
신규 타입 추가 가능성
-
비슷한 타입에 대한 처리가 ‘반복적’으로 호출되는 경우
(유지보수가 걱정되면 다형성이 좋다) -
동작의 확장처리 가능성 존재
5. 상속 vs. 위임 😱
-
- 상속
- 쉬운 확장 + 책임을 같이짐
(부모 바뀌면 반드시 자식이 바뀜)
- 물려 받는것은 편함
- 책임을 모두 지기에 문제 터지면…
- 상속
-
- 위임
- 깔끔한 구조, 책임을 따로
(계약 관계)
- 유연하며 느슨한 관계
- 위임
함께한다면 상속!
기능만 빌려 쓴다면 위임!
상속
💡 메서드 올리기 (Pull Up Method)
🧨 Before
class Orc : public Enemy
{
void Die() { /* 공통 죽음 처리 */ }
};
class Goblin : public Enemy
{
void Die() { /* 공통 죽음 처리 */ }
};
- 코드 중복…!
✅ After
class Enemy
{
public:
virtual void Die() { /* 공통 죽음 처리 */ }
};
- 부모 클래스에서 처리하여 코드 중복 제거
다만, 이것도 예외가 존재함
- 유령 같은 불사 몬스터?
💡 필드 올리기 (Pull Up Field)
🧨 Before
class Orc : public Enemy
{
protected:
int Health;
};
class Goblin : public Enemy
{
protected:
int Health;
};
- 변수의 비슷한 상황
✅ After
class Enemy
{
protected:
int Health;
};
- 역시 올리기!
- 공통 인터페이스를 올려놓으면 ‘전투 로직’등의 통합에 편리
💡 슈퍼클래스 추출하기 (Extract Superclass)
🧨 Before
class Player
{
FString Name;
FVector Position;
};
class NPC
{
FString Name;
FVector Position;
};
- 다른 클래스이지만 둘다 공통된 특성은 존재함
- 그런데 둘다 진짜 다른 경우???
✅ After
class ActorBase
{
FString Name;
FVector Position;
};
class Player : public ActorBase {};
class NPC : public ActorBase {};
-
공통된 부분을 개념으로 빼내
하나의 클래스로 만들어주기 -
그렇다고 Name 같은 걸 죄다 뭉치는 불필요한 클래스를 만들필욘 없다…
위임
-
클래스가 다른 클래스 객체를 멤버로 갖고
특정 작업을 위임하는 것 -
책임을 전가
💡 서브클래스를 위임으로 바꾸기 (Replace Subclass with Delegate)
🧨 Before
class Enemy
{
virtual FString GetSound() const = 0;
};
class Orc : public Enemy
{
FString GetSound() const override { return "Roar"; }
};
-
런타임의 행동 변경이 까다로움
-
언리얼에서 상속 구조에서 컴포넌트 구조로 가려는 이유
(상속은 지나치게 굳어있는 구조)
✅ After
class SoundBehavior
{
public:
virtual FString GetSound() const = 0;
};
class Enemy
{
TUniquePtr<SoundBehavior> Sound;
FString MakeSound() const { return Sound->GetSound(); }
};
- Sound를 갈아끼는 방식으로 구현 가능
-
또한 ‘기능’이 별도의 클래스이기에
다른 Enemy 하위 클래스도 해당 Sound 클래스를 사용 가능 - 전략 패턴에 가까움
- 필요한 요소를 갈아끼워
행동이 다른 객체를 만들어냄 - 한번만 쓰는 고정 로직이라면 굳이 필요없긴 하지만…
(오버 엔지니어링)
- 필요한 요소를 갈아끼워
💡 슈퍼클래스를 위임으로 바꾸기 (Replace Superclass with Delegate)
🧨 Before
class UIWidget
{
public:
void Render();
};
class ScoreWidget : public UIWidget
{
void Render() override;
};
- 처음에는 죄다 Render하기에 만들었지만
점점 별도의 기능을 Render에 넣다가
‘다중 상속’의 길로 가게 됨
(UIWidget, UIAnimation, 등)
✅ After
class WidgetRenderer
{
public:
void Render();
};
class ScoreWidget
{
private:
WidgetRenderer Renderer;
public:
void Show() { Renderer.Render(); }
};
- 계보에서 독립
-
Render에게 자신의 일을 위임
- IsA -> HasA로 변경
결정 기준
상속이 적합한 상황 | 위임이 적합한 상황 |
---|---|
명확한 “is-a” 관계가 있을 때 | “has-a” 관계나 행동 공유일 때 |
공통 기능이 많고 타입 계층이 필요할 때 | 런타임에 동작을 변경해야 할 때 |
다형성을 활용한 확장이 자연스러울 때 | 다중 상속과 같은 효과가 필요할 때 |
코드 재사용이 수직적일 때 | 기존 클래스 변경 없이 기능 확장이 필요할 때 |
상속 고려
-
명확한 Is-A 관계
정말 ‘하위’ 관계가 확실하고 변하지 않음 -
공통 기능이 많고 ‘명확한 계층’이 필요
(진짜 공통 기능이 많음) -
다형성을 통해 해결하고 싶은 경우
(인터페이스 등도 포함) -
강화 등 코드 재사용이 고려 가능
위임 고려
-
Has-A 관계
‘무기’를 ‘가지는 것’은 Has-A임
(플레이어가 무기를 소유하는 것) -
런타임 중 행동 변경
(플레이어의 무기 변경)
(그에 따른 공격 방식도 변경해야 함) -
다중 상속과 비슷한 효과를 고려해야 함
-
기존 클래스의 변경 x
ex
ABaseCharacter :: Attack
|- AEnemy :: Attack
|- APlayer :: Attack
-> AWeapon :: Attack
플레이어와 적 모두 Attack을 가짐 - 상속
다만 플레이어는 무기에 따른 공격을 변경하고 싶다면
Weapon을 만든 후, Attack과 연결해줌 - 위임
댓글남기기