오늘은 객체지향 설계의 핵심 원칙인 SOLID에 대해 공부했고, 이를 게임 개발 관점에서 어떻게 적용할 수 있는지도 함께 정리해봤다.
이 원칙들은 Unity 같은 엔진에서 개발할 때도 유지보수성과 확장성을 높이기 위해 매우 중요하다.
| 원칙 | 이름 | 핵심 개념 |
|---|---|---|
| S | Single Responsibility Principle | 하나의 클래스는 하나의 책임만 가져야 한다 |
| O | Open/Closed Principle | 기존 코드를 수정하지 않고 기능을 확장할 수 있어야 한다 |
| L | Liskov Substitution Principle | 자식 클래스는 부모 클래스를 대체할 수 있어야 한다 |
| I | Interface Segregation Principle | 불필요한 인터페이스 구현을 강요하지 않아야 한다 |
| D | Dependency Inversion Principle | 구체 클래스가 아닌 추상화에 의존해야 한다 |
하나의 스크립트/컴포넌트는 하나의 역할만 담당해야 한다.
// ❌ 이동, 공격, 저장 로직이 한 클래스에 몰려 있음
public class Player : MonoBehaviour {
void Move() { }
void Attack() { }
void SaveData() { }
}
// ✅ 역할을 명확히 분리
public class PlayerMovement : MonoBehaviour { void Move() { } }
public class PlayerCombat : MonoBehaviour { void Attack() { } }
public class PlayerSave : MonoBehaviour { void SaveData() { } }
코드를 수정하지 않고, 기능을 확장할 수 있어야 한다
// 공격방식을 인터페이스로 추상화
public interface IAttackStrategy
{
void Attack();
}
public class MeleeAttack : IAttackStrategy { public void Attack() { } }
public class RangedAttack : IAttackStrategy { public void Attack() { } }
public class Enemy {
private IAttackStrategy attackStrategy;
public Enemy(IAttackStrategy strategy)
{
this.attackStrategy = strategy;
}
public void Attack() => attackStrategy.Attack();
}
부모 클래스를 자식 클래스로 치환해도 동작이 깨지면 안 된다.
// X 자식 클래스가 부모의 계약을 깨뜨림
public class Enemy { public virtual void Die() { } }
public class ImmortalEnemy : Enemy
{
public override void Die() => throw new Exception("죽지 않음");
}
// O 공통 기능만 인터페이스로 분리
public interface IDamageable
{
void TakeDamage(int amount);
}
public class RegularEnemy : MonoBehaviour, IDamageable { }
public class ImmortalEnemy : MonoBehaviour { }
필요한 기능만 명확하게 나눈 인터페이스를 사용하자.
// X 모든 엔티티에 다 필요한 메서드는 아님
public interface IGameEntity {
void Move();
void Attack();
void CastSpell();
}
// O 기능별로 인터페이스를 분리
public interface IMovable { void Move(); }
public interface IAttackable { void Attack(); }
public interface ISpellCaster { void CastSpell(); }
클래스는 구체적인 구현보다 인터페이스(추상화)에 의존해야 한다.
// X GameManager가 AudioManager에 직접 의존
public class GameManager
{
private AudioManager audioManager = new AudioManager();
}
// O 추상화에 의존하여 유연성 확보
public interface IAudioManager
{
void Play(string clip);
}
public class GameManager
{
private IAudioManager audio;
public GameManager(IAudioManager audio)
{
this.audio = audio;
}
}
게임도 결국 소프트웨어다. 설계가 깔끔해야 확장도 쉽다.
SRP → 기능별 컴포넌트 분리로 유지보수 편리OCP → 새로운 무기/스킬/몬스터도 쉽게 추가 가능LSP → 상속보다 인터페이스 조합이 유리한 경우가 많음ISP → 엔티티마다 필요한 기능만 제공DIP → 테스트 가능한 구조, 교체 가능한 모듈 구성 가능항상 지키며 작업할 수는 없겠지만 최대한 지켜려고 노력하고,
앞으로는interface를 더 활용하여 설계를 해봐야겠다.