09
22
타입을 상속으로 표현하는 대신 참조 관계로 만들어서 유연하게 타입을 표현한다. 

 

다양한 몬스터가 등장하는 RPG를 만든다고 가정해보자.

첫 번째 마을 근처에는 다양한 슬라임들이 등장하고, 두 번째 마을에서는 고블린 등이 등장하고, 세 번째 마을에서는 오크들이 등장하고..

 

장소마다 특정 테마가 있고, 같은 종족의 다양한 몬스터가 등장하는게 일반적일 것이다.

레드 슬라임, 블루 슬라임, 그린 슬라임 등이 존재하고 고블린 아처, 고블린 워리어, 고블린 매지션 등이 존재할 때,

 

각 종족은 공통되는 특징들이 존재할 것이다. 고블린은 속도가 빠르다거나 오크는 체력이 많다거나 말이다.

 

 

상속으로 종족만들기

일단 출발은 MonsterBase클래스를 만들 것이다.

public class MonsterBase
{
    protected int _hp;
    
    public MonsterBase(int maxHP)
    {
        _hp = maxHP;
    }
    
    public virtual PrintMonsterName(){ }
}

MonsterBase클래스를 상속받아 슬라임 종족과 오크 종족을 만들었다.

각각 100, 300의 체력을 기준으로 시작한다.

public class Slime : MonsterBase
{
    public Slime() : base(100){ }
    
    public override PrintMonsterName()
    {
        Debug.Log("Slime");
    }
}

public class Ork : MonsterBase
{
    public Ork() : base(300){ }
    
    public override PrintMonsterName() 
        => Debug.Log("Ork");
}

 

직접 상속의 문제점

1. 종족마다 클래스를 하나씩 만들다 보면 수십-수백 가지 종류의 베이스 클래스들이 만들어질 것이다.

2. 기획자가 종족을 추가하고 싶을 때마다 프로그래머에게 요청해야 한다.

 

 

통합하기

각 종족마다 클래스를 만들지 말고

MonsterBase클래스에서 Breed(종족) 클래스를 참조해서 포함관계로 만들어보자.

public class MonsterBase
{
    private Breed _breed;
    private int _hp;
    
    public MonsterBase(Breed _breed)
    {
        _breed = _breed;
        _hp = _breed.baseHP;
    }
    
    public void PrintMonsterName()
    { 
        _breed.PrintMonsterName();
    }
}

public class Breed
{
    public int baseHP;
    private string _name;
    
    public void PrintMonsterName() 
        => Debug.Log(_name);
    
    public MonsterBase GenerateMonster()
    {
        return new MonsterBase(this);
    }
}

주목할 부분은 Breed클래스의 GenerateMonster() 메서드. 팩토리 패턴을 사용하였다.

// 일반 생성
MonsterBase monster = new Monster(breed);
// 팩토리 생성
MonsterBase monster = someBreed.newMonster();

 

 

타입 객체 패턴의 특징

1. 새로운 타입을 만들 때 코드를 수정하지 않아도 되는 장점이 있다.

2. 상속이 아니기 때문에 타입 종속적인 동작을 표현할 때 까다롭다.  

  - 예를 들어 종족마다 AI를 다르게 하고 싶다면 구현 방법이 까다롭다. 

  - 미리 AI를 정의해놓고, 타입 객체가 그중 하나를 선택하게 하는 등의 방법으로 우회해야 한다.

 

 

상속으로 데이터 공유하기

- 타입 객체로 만들면 생성이 자유롭다는 장점은 있으나 각 타입이 공유되는 데이터가 필요한 경우 중복 데이터가 생긴다는 문제가 발생한다. 

- 이 경우 타입 객체의 부모를 만들어서 해결할 수 있는데, 실제로 C#의 상속을 이용하는 것이 아니라 상위 객체를 참조로 들고 있게 된다.

public class Breed
{
    private Breed _parent;
    private int _baseHp;
    private string _name;

    // 상위객체가없으면 null
    public Breed(Breed parent, int baseHp, string name)
    {
        _parent = parent;
        _baseHp = baseHp;
        _name = name;
    }
    
    public int GetBaseHp()
    {
        // 자신의 값이 0이라면 상위 객체의 값을 사용한다. (재귀적)
    }
    
    public string GetName()
    {
        // 자신의 값이 Null 이라면 상위 객체의 값을 사용한다. (재귀적)   
    }
}

 

동적 위임

 - 재귀적으로 호출하고, null검사를 해야 하기 때문에 동작이 느리다.

 - 동적이라서 중간에 종족 값을 바꾸는 것도 유연하다.

public int GetBaseHp()
{
     if(_baseHp != 0 || _parent == null)
         return _attack;
     
     return _parent.GetBaseHp();
}
    
public string GetName()
{
    if(_name != null || _parent == null)
        return _name;
    
    return _parent.GetName();
}

 

카피다운(copy-down) 위임

 - 직접 값을 넣어서 상위 레퍼런스를 가지고 있지 않아도 된다. 

public Breed(Breed parent, int baseHp, string name)
{
    _parent = parent;
    _baseHp = baseHp;
    _name = name;
    
    if(_parent != null)
    {
        if(baseHp == 0)
            _baseHp = _parent.GetBaseHp();
        if(_name == null)
            _name = _parent.GetName();
    }
}

 

 

생각해볼 논제 : 타입 객체를 공개? 비공개?

1. 공개할 경우

 - 인스턴스를 거치지 않고 접근 가능.

 - 공개된 API가 늘어나기 때문에 복잡성이 커져 유지보수가 어렵다.

2. 비공개할 경우

 - 모든 메서드를 포워딩해야 한다. 

 - 선택적으로 오버라이드 가능.

 

 

마치며

지금은 종족에 대해서 예시를 들었지만, 공격 방식으로 근거리 공격 몬스터 / 원거리 공격 나눠서 구현할 때도 사용할 수 있어 보인다. 특히나 각 타입은 원거리 공격이면서 원형 범위,  원거리 공격이면서 직선 범위 등으로 상속이 더해지는 경우가 많은데, 이를 타입 객체로 활용해보면 어떨까.

 

위임한다는 측면에서 컴포넌트 패턴이나 전략 패턴과 비슷한 면도 있는 듯.

 

 

COMMENT