1. SRP - 단일 책임 원칙(Single Responsibility Principle)
정의: 클래스는 단 하나의 변경 이유만 가져야 한다.
나쁜 예
class Player
{
public:
void Move();
void SaveToFile();
void RenderUI();
};
문제:
- 이동 로직 변경 -> 수정
- 저장 방식 변경 -> 수정
- UI 변경 -> 수정
좋은 예
class PlayerMovement {};
class PlayerSaveSystem {};
class PlayerUI {};
각각 책임 분리
언리얼에서는 액터 컴포넌트 구조 자체가 SRP 철학을 가지고 있으며, 저 또한 그 철학 유지하며
class FF_API UEquipmentSystem : public UActorComponent : 장착 역할을 가진 컴포넌트
class FF_API UInventorySystem : public UActorComponent : 인벤토리 역할을 가진 컴포넌트
class FF_API ULootingSystem : public UActorComponent : 루팅 역할을 가진 컴포넌트
이 같은 구성을 유지하고 있습니다.
2. OCP - 개방 폐쇄 원칙(Open-Closed Principle)
정의: 확장에는 열려 있고, 수정에는 닫혀 있어야 한다.
나쁜 예
int CalcDamage(int type)
{
if(type == 0) ...
else if(type == 1) ...
}
몹 추가할 때 마다 수정
좋은 예
class DamagePolicy
{
public:
virtual int Calc() = 0;
};
class FireDamage : public DamagePolicy {};
class IceDamage : public DamagePolicy {};
새 타입 추가 = 클래스 추가만
언리얼에서는 DataAsset, DataTable 클래스를 활용하여 Data-Driven 기반으로
새로운 무기/스킬/AI(Behavior Tree) 등을 관리할 수 있습니다.
여기서 switch / if 문이 계속 늘어나면 OCP 원칙에 위반할 가능성이 높아진다고 하네요.
3. LSP - 리스코프 치환 원칙(Liskov Substitutaion Principle)
자식 클래스는 부모를 완전히 대체할 수 있어야 한다.
나쁜 예
class Bird {
virtual void Fly();
};
class Penguin : public Bird {
void Fly() override { throw; }
};
좋은 예
class Bird {};
class FlyingBird : public Bird {};
주로 Weapon 계열을 구현할 때, 총기에만 사용하는 Fire를
냉병기나 방패 같은 클래스도 상속을 받으면 구조가 잘못됐다고 합니다.
최근에 저도 TPS 기준으로 개발하면서 Fire로 지은 함수를 일반적인 Attack으로 리팩토링 한 기억이 남습니다.
중요한 건 이 객체를 부모 타입으로 써도 문제 없는지 살펴보는 감각이라고 하네요.
4. ISP - 인터페이스 분리 원칙(Interface Segregation Principle)
정의: 클라이언트는 사용하지 않는 인터페이스에 의존하면 안 된다.
나쁜 예
class ICharacter
{
virtual void Fly();
virtual void Swim();
virtual void Shoot();
};
이렇게 되면 가만히 서있는 NPC가 있을 경우 빈 함수로 내버려둬야 할 것입니다.
좋은 예
class IFlyable {};
class ISwimmable {};
class IShootable {};
필요한 것만 구현하기 위해서 인터페이스는 작고 구체적일수록 좋으며,
저도 인터페이스가 편하긴 한데 추가할 때 신중한 편인 것 같습니다.
5. DIP - 의존성 역전 원칙(Dependency Inversion Principle)
정의: 상위 모듈은 하위 모듈에 의존하면 안 된다.
둘 다 추상화에 의존해야 한다.
나쁜 예
class Game
{
SteamNetwork net;
};
여기서 Steam이 아닌 PSN으로 바꾸면 게임 전체를 수정하게 됩니다.
좋은 예
class INetwork {};
class Game
{
INetwork* net;
};
class SteamNetwork : public INetwork {};
class PSNNetwork : public INetwork {};
플랫폼/서버/입력 시스템을 변경할 때 이 구조를 가지는게 중요합니다
요약
*SRP: 한 클래스 = 한 역할
*OCP: 수정하지 말고 확장하라
*LSP: 자식은 부모처럼 동작해야
*ISP: 인터페이스는 작게 쪼개라
*DIP: 구현이 아니라 추상에 의존
+ 유니티에서 좋은 영상이 올라와 추가
https://youtu.be/e9JfXuRcwkM?si=YBjQ3BOHk25RNVQ4