본문 바로가기

유니티/디자인 패턴

[Unity] 방문자 패턴 (Visitor Pattern)

반응형

방문자 패턴이란?

이름에서도 알 수 있듯이 주요 로직의 흐름이 방문을 통해 이루어진다.

어떠한 방문자가 데이터 각 요소에 방문을 함으로써 특정 요소의 작업을 할 수 있게 된다.

 

따라서 방문자에 해당하는 컴포넌트와 방문할 장소 그리고 이를 사용할 클라이언트로 크게 나뉜다.

 

다이어그램

 

IVisitor 인터페이스를 상속받은 Visitor가 IVisitable을 상속받은 ConcreteVisitable에 방문을 하는 구조라고 생각하면 된다. 그리고 Client는 이 구조를 사용하게 된다.

 

실제로 방문자 패턴을 구현해 보면 구조가 간단하지 않기 때문에 UML 다이어그램 구조를 기억한 채로 사용하는 편이 좋다.


왜 사용하는 것일까?

유니티 엔진에서 다양한 스킬 혹은 아이템으로 버프를 받는 상황을 생각해 보자.

 

아이템이나 스킬로 인한 버프를 리스트에 담아 관리를 해야 할까?

이러한 방식은 클래스 간의 강한 결합을 유도할 수 있고, 자칫하면 스파게티 코드가 될 가능성이 존재한다.

(아이템이나 스킬 등 버프를 주는 방식이 다양하다면 더욱 그럴 것이다.)

 

이 경우에 SOLID원칙을 지키며 스크립트를 작성해 나간다면 유지 보수를 단단하게 할 수 있다.

 

방문자 패턴은 단일 책임 원칙과 개방/폐쇄 원칙을 고수하면서 확장해 나갈 수 있는 디자인 패턴 중 하나이다. 클라이언트 자체는 ConcreteVisitor의 구체적인 내용을 알 필요 없이 사용할 수 있다.

 

다만 코드의 양이 방대해질 수 있기 때문에 방문자 패턴이 적절하게 쓰임이 있는 곳에만 사용해야 한다.

 

장점

단일 책임 원칙 : Visitable과 Visitor가 각각의 행동에 대한 책임만을 진다는 점에서 단일 책임 원칙 따름

개방/폐쇄 원칙 : Visitor 혹은 Visitable의 컴포넌트를 추가함으로써 확장에 열려있고 수정에는 닫힌 구조

 

단점

복잡성 : 일반적인 참조로 이루어진 구조에 비해 많은 스크립트와 상속과 참조를 가진 구조를 가지므로 상대적으로 복잡


어떻게 사용할 수 있을까?

위에서 예시로 든 버프를 관리하는 코드를 생각해 보자.

 

스크립트는 구체 클래스 Visitable을 위한 인터페이스 IVisitable로 시작을 한다.

 

public interface IVisitable
{
    void Accept(IVisitor visitor);
}

 

IVisitable 인터페이스는 방문자가 방문 가능한 클래스(Visitable)를 상속하기 위한 것이다.

아래는 IVisitable 인터페이스를 상속받은 구체 클래스의 예시이다.

 

첫 번째는 공격력 버프이다. 데미지와 관련된 버프를 받는다.

 

public class DamageBuff : MonoBehaviour, IVisitable
{
    public int damage = 0;

    public void Accept(IVisitor visitor)
    {
        Debug.Log("Damage increased!");
        visitor.Visit(this);
    }
}

 

두 번째는 방어력 버프이다.

 

public class DefenseBuff : MonoBehaviour, IVisitable
{
    public int defense = 0;

    public void Accept(IVisitor visitor)
    {
        Debug.Log("Defense Increased!");
        visitor.Visit(this);
    }
}

 

방문자(Visitor) 클래스는 이 두 개의 구체 클래스를 매개변수로 받는 함수를 가진다. 그러기 위해서 먼저 IVisitable 인터페이스를 작성한다.

 

아래는 IVisitor 인터페이스이다.

 

public interface IVisitor
{
    void Visit(DamageBuff visitable);
    void Visit(DefenseBuff visitable);
}

 

이를 상속받은 Visitor클래스는 Scriptable Object로 작성한다. 이는 여러 개의 프리팹으로 만들어 필요한 개수만큼 사용할 수 있음을 나타내기 위해서이다.

 

아래는 IVisitor 인터페이스를 상속받은 Enchant라는 클래스이며, 방문자 클래스로써 버프를 주는 역할을 한다.

 

[CreateAssetMenu(fileName = "Enchant", menuName = "DesignPattern/VisitorPattern/Enchant")]
public class Enchant : ScriptableObject, IVisitor
{
    public int damageIncrease;
    public int defenseIncrease;

    public void Visit(DamageBuff visitable)
    {
        visitable.damage += damageIncrease;
    }

    public void Visit(DefenseBuff visitable)
    {
        visitable.defense += defenseIncrease;
    }
}

 

Scriptable Object를 여러개 만들어서 예제에 활용하고자 하였다.

 

스크립터블 오브젝트로 제작하여 다양한 값 입력

 

추가로 유니티 월드 상에 배치되어 플레이어와 충돌할 경우 플레이어에게 이벤트를 전달할 PickUp 클래스도 작성한다.

이 클래스는 OnTriggerEnter을 통해 플레이어와 충돌 시 Enchant를 플레이어에게 전달하는 역할을 한다.

 

public class PickUp : MonoBehaviour
{
    public Enchant enchant;

    private void OnTriggerEnter(Collider other)
    {
        if (other.TryGetComponent(out IVisitable player))
        {
            player.Accept(enchant);
        }
        Destroy(gameObject);
    }
}

 

마지막으로 플레이어 클래스이다. 플레이어에겐 공격력과 방어력 버프가 있고 초기값은 0이다.

 

public class Player : MonoBehaviour, IVisitable
{
    List<IVisitable> buffList = new();

    public void Accept(IVisitor visitor)
    {
        foreach (var buff in buffList)
        {
            buff.Accept(visitor);
        }
    }

    private void Awake()
    {
        var damageBuff = gameObject.AddComponent<DamageBuff>();
        var defenseBuff = gameObject.AddComponent<DefenseBuff>();
        buffList.Add(damageBuff);
        buffList.Add(defenseBuff);
    }
}

 

이제 버프가 플레이어와 충돌되면 버프가 오르는 것을 확인할 때이다.

 

큐브가 플레이어이고 스피어는 모두 Enchant를 가진 PickUp들이다.

 

충돌 시 값 변경

 

방문자 패턴으로 구현된 아이템들이 모두 잘 적용되는 것을 확인할 수 있다.

 

UML 다이어그램

반응형