카테고리 없음

내일배움캠프 66일차 TIL 최종 프로젝트 - SOLID 원칙

joseph2518 2024. 12. 17. 00:08

20241216 / Unity_6차  15주차 월요일

 

 

SOLID 원칙은 객체지향 설계에서 코드의 유지보수성과 확장성을 높이기 위한 다섯 가지 설계 원칙이다.

  1. 단일 책임 원칙(SRP): 클래스는 하나의 책임만 가져야 한다.
  2. 개방-폐쇄 원칙(OCP): 클래스는 확장에 열려 있고, 수정에는 닫혀 있어야 한다.
  3. 리스코프 치환 원칙(LSP): 자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
  4. 인터페이스 분리 원칙(ISP): 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
  5. 의존성 역전 원칙(DIP): 고수준 모듈이 저수준 모듈에 의존해서는 안 된다.

 

이제 와서 이런 기초적인 이론에 대해 왜 상기하는가 하면

내가 설계한 코드가 위 원칙을 위배하여 실시간으로 문제를 겪었기 때문이다.

 

public abstract class AllyNPCAI : MonoBehaviour
{
    public AllyNPC NPC { get; protected set; }
    public AllyNPCAnimatorController AnimationController { get; protected set; }
    public bool StateLock;
    public bool IsInteractWithPlayer;

    /// <summary>
    /// 작업자 작업 위치 도착 후 작업 시작 시 발생
    /// <para>Use For : Worker</para> 
    /// </summary>
    public virtual event Action<AllyNPC> OnWorkStart;

    public abstract bool Initialize(AllyNPC allyNPC);
    /// <summary>
    /// 최적화를 위해 부모 오브젝트 Update에서 호출
    /// </summary>
    public abstract void UpdateAI();
    public abstract void Destroy();


    /// <summary>
    /// 기본 명령(배회) : 커맨드는 다른 작업이 없을 때 Idle State를 지정
    /// </summary>
    public abstract void SetCommand_Wander(float wanderBoundaryLeft, float wanderBoundaryRight);
    /// <summary>
    /// 지정된 위치 방어
    /// <para>Use For : Archer, Worker</para> 
    /// </summary>
    public virtual void SetCommand_DefensePosition(float defendBoundaryLeft, float defendBoundaryRight, bool standLeft)
    {
    }


    /// <summary>
    /// 코인 드랍
    /// <para>Use For : Vagrant 제외 전부</para> 
    /// </summary>
    public virtual void SetState_OfferCoin()
    {
    }
    /// <summary>
    /// 강등 시 초기 애니메이션
    /// <para>Use For : Vagrant, Villager</para> 
    /// </summary>
    public virtual void SetState_BlownAway(bool isBlownLeft)
    {
    }
    /// <summary>
    /// 거점 방어
    /// <para>Use For : Archer</para> 
    /// </summary>
    public virtual void SetState_DefensePosition(float defenseBoundaryLeft, float defenseBoundaryRight, bool standLeft)
    {
    }
    /// <summary>
    /// 작업 등록
    /// <para>Use For : Worker</para> 
    /// </summary>
    public virtual void SetState_Work(float workBoundaryLeft, float workBoundaryRight)
    {
    }
    /// <summary>
    /// 작업 완료
    /// <para>Use For : Worker</para> 
    /// </summary>
    public virtual void EndState_Work()
    {
    }
    /// <summary>
    /// 놀고 있는 일꾼에 한해서 방어 위치에서 대기
    /// <para>Use For : Worker</para> 
    /// </summary>
    public virtual void SetState_HoldPosition(float holdBoundaryLeft, float holdBoundaryRight)
    {
    }
}

 

아군 NPC는 위의 부모 AI 클래스를 상속받은 독자적인 AI 클래스 인스턴스를 가지고 있다.

 

NPC Controller는 모든 NPC에 대한 정보를 갖고 있으며,

상황에 따라 NPC AI의 함수를 호출하여 게임을 운영한다.

 

나는 NPC Controller에서 모든 아군 NPC를 리스트로 관리하기 편하도록

어떤 NPC AI에 대하여서도 부모 클래스에 정의된 멤버로 접근할 수 있길 바랐다.

 

그리고 이것이 다형성을 활용한 좋은 방법이라 생각했다.

 

그런데 저번의 리팩터링 때 처럼 공통부분 보다 개별 동작이 점점 많아졌다.

 

상속받은 AI 클래스들이 빈 virtual 함수를 override(구현)하지 않고 공백으로 두는 경우가 계속 발생했다.

 

이는 리스코프 치환 원칙(LSP)단일 책임 원칙(SRP)을 위반한 것이다.

 

부모 클래스에 정의된 메서드를 기대하고 호출했을 때 의도된 동작이 보장되지 않으며,

그렇게 부모 클래스에 불필요한 메서드가 많아지면 부모 클래스의 역할과 책임이 모호해지기 때문이다.
(부모 클래스가 아니라 인터페이스를 위와 같이 구성했다면

리스코프 치환 원칙 대신 인터페이스 분리 원칙을 위반한 셈이 된다)

 

 

사실 이것도 이쯤 되면 AI의 개별 동작 부분만 나누거나

비슷한 역할을 하는 AI들끼리 인터페이스를 공유하는 컴포지션 방식을 활용해야 한다.

 

그렇게 되면 NPC Controller에서도 필히 NPC를 종류별로 구분해야 하고

서로 다른 클래스에 접근하기 위한 비용이 늘어날 수밖에 없겠지만 말이다...

 

 

아니면 이대로 계속  부모 클래스를 키운 다음 공통부분에 대한 실마리를 찾는 방법도 있겠지