내일배움캠프/TIL

2차 디자인패턴 특강 정리 - 이벤트기반 프로그래밍, 명령패턴

서보훈 2024. 10. 30. 21:13

이벤트 기반 프로그래밍 - 옵저버 패턴, 이벤트 버스 패턴

  • 효율적으로 프로그램이 실행되도록 설계할 수 있는 패턴
  • Pub- Sub 패턴 이라고도함 (발행 - 구독 패턴)

발행자 1 - 구독자 1 : 함수 직접 호출 → 이 함수에 반응해야하는 함수가 늘어나면 일일이 호출해야함

 

발행자 1 - 구독자 다수 : 옵저버 패턴 사용 → 여러개의 함수에서 특정 함수를 호출하고싶으면 특정 함수를 모든 델리게이트에 등록해야함

 

발행자 다수 - 구독자 다수 : 이벤트 버스 패턴 사용 → 등록과 호출을 하나의 스크립트에서 전담


옵저버 패턴

  • 발행자 1 - 구독자 n 의 관계
  • 이벤트 발생시, 발행자가 이벤트 발생을 알리고 구독자들이 행동하는 패턴
  • 델리게이트를 통해 구현 가능
    • public event Action<> 의 형태로 사용하는것이 옵저버 패턴
public class CharacterBuffs : MonoBehaviour
{
    public Dictionary<int, Coroutine> nowBuffs = new Dictionary<int, Coroutine>();
    
    //이벤트 발행(발행자)
    public UnityAction<BuffData> onAddBuff;

    //버프를 받을때 호출
    public void AddBuff(BuffData addedBuff)
    {
        nowBuffs.Add(addedBuff.buffId, StartCoroutine(MaintainBuff(addedBuff)));
        //이벤트 호출
        onAddBuff?.Invoke(addedBuff);
    }
}

public class BuffListUI : MonoBehaviour
{
    public GameObject buffInfoUI;
    private Dictionary<int, GameObject> buffInfos = new Dictionary<int, GameObject>();

    private void Start()
    {
        //이벤트 등록(구독자)
        CharacterManager.Instance.Player.buffs.onAddBuff += AddBuffInfo;
    }
    
    //이벤트 발생시 작동할 함수
    public void AddBuffInfo(BuffData buff)
    {
        if (buffInfos.ContainsKey(buff.buffId))
        {
            Destroy(buffInfos[buff.buffId]);
            RemoveKey(buff.buffId);
        }

        buffInfos.Add(buff.buffId, Instantiate(buffInfoUI, transform));
        buffInfos[buff.buffId].GetComponent<BuffInfoUI>().SetUp(buff);
        buffInfos[buff.buffId].GetComponent<BuffInfoUI>().onDestroy += RemoveKey;
    }

    private void RemoveKey(int buffId)
    {
        buffInfos.Remove(buffId);
    }
}

 

이런식으로 event 를 등록, 호출하는것을 옵저버 패턴이라고 함

 


이벤트 버스 패턴

  • 발행자 n - 구독자 n 의 관계
  • 이벤트 구현시 발행자와 구독자 사이의 의존성을 만들지 않음
더보기

이벤트 버스 코드

public enum EventName
{
    Event1,
    Event2
}

public class EventManager : MonoBehaviour
{
    #region sington
    private static EventManager instance;
    public static EventManager Instance
    {
        get { return instance; }
    }

    public static bool InstanceExist {  get { return instance != null; } }

    private void Awake()
    {
        if(InstanceExist)
        {
            Destroy(gameObject);
            return;
        }

        instance = this;
    }

    private void OnDestroy()
    {
        instance = null;
    }
    #endregion

    private Dictionary<EventName, Action> events = new Dictionary<EventName, Action>();

    public void SubEvent(EventName name, Action action)
    {
        if(events.TryGetValue(name, out Action thisEvent))
        {
            thisEvent += action;
        }
        else
        {
            thisEvent = action;
            events.Add(name, thisEvent);
        }
    }

    public void DeSubEvent(EventName name, Action action)
    {
        if(events.TryGetValue(name, out Action thisEvent))
        {
            thisEvent -= action;
        }
    }

    public void Publish(EventName name)
    {
        if(events.TryGetValue(name, out Action thisEvent))
        {
            thisEvent.Invoke();
        }
    }
}

public class Listoner : MonoBehaviour
{
    private void OnEnable()
    {
        EventManager.Instance.SubEvent(EventName.Event1, DoSomething);
    }

    private void OnDisable()
    {
        EventManager.Instance.DeSubEvent(EventName.Event1, DoSomething);
    }

    public void DoSomething()
    {
        Debug.Log("이벤트 발생 확인");
    }
}

public class Publisher : MonoBehaviour
{
    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.A))
        {
            EventManager.Instance.Publish(EventName.Event1);
        }
    }
}

주의사항

  • 이벤트 버스에 대한 의존성이 높아질 수 있음
  • 복잡해질경우 관리가 어려울 수 있음

※ 옵저버 패턴과 이벤트 버스 패턴

  • 상술했듯, 이 둘을 묶어서 Pub - Sub 패턴 이라고도 함
  • 옵저버패턴은 발행자에게 이벤트를 직접 등록
  • 이벤트 버스 패턴은 버스에 이벤트를 구독하고, 버스를 통해 이벤트를 발행
    • 이 과정을 통해 발행자와 구독자간의 의존성이 사라짐

 


명령패턴

  • 행동을 객체로 만들어 재사용
  • 기록, 취소 병렬처리를 쉽게 하기 위해 사용됨
  • 객체화를 통해 복잡한 작업을 순차적으로 처리 or 되돌림

 

구현

  1. 로깅
    • 행동에 로그를 남길때 사용
    • ICommand 인터페이스를 사용, 로그를 남기는 메서드를 강제 구현함
    • 캐릭터 컨트롤러등에 커멘드 로거 연결, 리스트에 로그를 남김
  2. 되돌리기 시스템
    • ICommand 인터페이스에 실행, 역실행 메서드 강제 구현
    • ICommand인터페이스를 스택에 쌓은 뒤, 되돌리기 실행시 스택을 꺼내 역실행 메서드 실행

 


파사드 패턴 (Facade)

  • 작은 클래스들의 객체를 가지고 있는 큰 클래스 생성, 큰 클래스가 작은 클래스의 문지기 역할을 함
  • 작은 클래스에 명령이 들어올경우, 큰 클래스를 통해 전달함
public class Player : MonoBehaviour
{
    public PlayerController controller;
    public PlayerCondition condition;
    public Equipment equip;
    public CharacterBuffs buffs;

    public ItemData itemData;
    public UnityAction addItem;

    public Transform dropPosition;
    private void Awake()
    {
        CharacterManager.Instance.Player = this;
        controller = GetComponent<PlayerController>();
        condition = GetComponent<PlayerCondition>();
        equip = GetComponent<Equipment>();
        buffs = GetComponent<CharacterBuffs>();
    }
}

※ 숙련주차 강의의 Player 클래스에서 파사드 패턴이 활용됨, 해당 클래스는 싱글톤 클래스인 CharacterManager 에서 연결되어, Player를 통해 하위 클래스들에게 명령을 전달함