스탠다드 특강 6 - 디자인 패턴 2

김정환·2024년 10월 30일
0

2회차에서 다룰 패턴

  • 옵저버 패턴
  • 이벤트 버스 패턴
  • 명령 패턴
  • 서비스 로케이터 패턴

1. 이벤트 기반 프로그래밍

  • Pub-Sub 패턴 : 발행(Publish) - 구독(Subscribe) 패턴
  • 기존 업데이트 방식
    • A 클래스가 B 클래스의 변수의 변화가 일어났는지 궁금한 상황
    • A 클래스가 B에게 변화가 일어났는지 계속 검사해야함 -> 매우 비효율적
  • 이벤트 기반 프로그래밍
    • 이것을 어떤 상황이나 변화일어날 때 이벤트를 호출하는 것으로 보완함
    • A 클래스가 B 클래스의 상태 변화에 대한 이벤트를 구독해두고
      B 클래스에서 변화가 일어나면 이벤트를 호출해주는 것.
  • 코드 실행의 흐름이 효율화할 수 있음.

대응 방식 유형

발행자 수구독자 수대응 방식
11함수 호출
1can be N옵저버 패턴
can be Ncan be N이벤트 버스 패턴

2. 옵저버 패턴 (매우 중요)

이벤트 기반 설계 시 많이 활용하는 패턴

발행자가 이벤트가 발생했을 때 알려주는 패턴

  • 업데이트 방식에선 구독자들이 발행자에게 지속적으로 물어봤음.
  • 옵저버 패턴에선 발행자가 행위가 일어나면 구독자들에게 알려줌.

단, 구독을 하면 나와 상관없는 것도 받아야 할 수 있음.

예시

입문 강의 내용 중,
TopDownController의 OnMoveEvent를 두 구독자들이 각각의 Move 메서드로 구독하고 있었음.

// 발행자
public class TopDownController : MonoBehaviour
{
    public event Action<Vector2> OnMoveEvent;
    // 생략
}

// 구독자1
public class TopDownMovement : MonoBehaviour
{
	public TopDownController controller;
    
    void Start()
    {
    	controller += Move;
    }
    
    void Move(Vector2 direction)
    {
    	// ~
    }
}
// 구독자2
public class TopDownAnimationController : MonoBehaviour
{
	public TopDownController controller;
    
    void Start()
    {
    	controller += Move;
    }
    
    void Move(Vector2 direction)
    {
    	// ~
    }
}

중요한 점

  • 구독할 때 메서드만 등록되는 것이 아니라
    어떤 인스턴스(주체)메서드(행동)가 등록되었다로 등록됨.
    • 만약 구독은 하지 않은 상태의 동일한 클래스의 다른 인스턴스가 있을 때 이벤트가 발행된다면,
      구독한 인스턴스에게만 이벤트가 전달됨.
    • 다시말해 델리게이트에 등록할 때 주체와 행동이 같이 등록되는 것.
      그래서, 이벤트가 발생하면 등록한 주체행동을 호출하는 것
  • 델리게이트는 직렬화가 안됨.
    • 다시 등록해야함. (권장)
    • 오픈소스를 이용한 방법 등이 있다.

3. 이벤트 버스

1개 이상의 발행자와 1개 이상의 구독자의 처리가 가능하다.

왜 쓸까?

이벤트 구현 시 발행자와 구독자 사이에서 직접적인 의존성을 만들지 않고 이벤트 기능을 만들 수 있다.

적용 사례

퀘스트 시스템 등

  • 돈을 얻는 방법은 퀘스트, 행사, 사냥 등 다양함
    • 기존 이벤트 방식에서는 너무 많은 이벤트 구독이 필요하므로 관리가 어려워짐.
  • 이런 부분을 쉽게 관리하기 위해서 이벤트 버스가 사용됨

구현

  • EventBus에는 이벤트를 등록.

  • Publisher가 이벤트를 Invoke. 이때 Publisher는 하나일 필요는 없음.
    하나 이상의 Publisher

    • 옵저버 패턴은 일반적으로 한 개의 클래스에 관심이 있는 경우 사용.
    • 이벤트 버스는 보다 복잡한 상황을 상정 (많은 Publisher, Subscribe)
  • EventBus는 하나 이상의 액션을 관리하기 때문에 대체로 Dictionary를 이용해서 구현

  • Publisher에게 직접 구독하지 않고 구독을 받아주는 대리자를 만들어서 모든 구독을 받음

  • Publisher들은 이 대리자를 통해서 이벤트를 발행시킴.

using System;
using System.Collections.Generic;
using UnityEngine;

public enum QuestEvent
{
    Hunt,
    Explore,
    Build
}

public static class QuestEventBus
{
    static Dictionary<QuestEvent, Action> questEventMap = new Dictionary<QuestEvent, Action>();

    // 등록
    public static void Subscribe(QuestEvent questEvent, Action listener)
    {
        if (questEventMap.TryGetValue(questEvent, out Action questAction))
        {
            questAction += listener;
            questEventMap[questEvent] = listener;
        }
        else
        {
            questAction = listener;
            questEventMap.Add(questEvent, questAction);
        }
    }    

    // 해제
    public static void Unsubscribe(QuestEvent questEvent, Action listener)
    {
        if (questEventMap.TryGetValue(questEvent, out Action questAction))
        {
            questAction -= listener;
            if(questAction == null)
            {
                questEventMap.Remove(questEvent);
            }
            else
            {
                questEventMap[questEvent] = listener;
            }

        }
    }

    public static void Publish(QuestEvent questEvent)
    {
        if (questEventMap.TryGetValue(questEvent, out Action questAction))
        {
            questAction?.Invoke();
        }
    }
}

// 사용 예시

public class QuestListener : MonoBehaviour
{
    [SerializeField] QuestEvent questType = QuestEvent.Hunt;
    void OnEnable() 
    {
        QuestEventBus.Subscribe(questType, Test);    
    }

    void OnDisable() 
    {
        QuestEventBus.Unsubscribe(questType, Test);    
    }

    void Test()
    {
        Debug.Log("Listener Test called");
    }
}

public class QuestPublisher : MonoBehaviour
{
    public void OnDead()
    {
        QuestEventBus.Publish(QuestEvent.Hunt);
    }
}

번외

  • 이벤트에 매개변수를 넣어야하는 경우
    • 매개변수로 object (C# 모든 객체들은 object로 치환 가능하므로)를 사용하는 것. (최후의 방법으로 사용)
    • 단, 이럴 경우 Boxing으로 인한 GC 발생할 수 있으므로 사용에 유의해야함.

고려사항

  • 의존성이 너무 높아질 가능성이 있음
    • 단위 테스트 및 관리가 어려워질 수 있음.
    • 누가 보냈는지 알기 어려워질 수 있음.
      • 그래서 enum을 통해서 찾기 편하도록 할 수 있음.
      • 어떤 enum 값에서 발생한 건지 찾으면 되므로.
  • 복잡해질 경우 이벤트 관리 안 될 가능성이 있음
    • 이벤트에서 에러가 생기면 어디서 문제가 생겼는지 알기 어려울 수 있음.

헷갈림

옵저버, 이벤트 버스 패턴이 비슷함.
그래서 발행-구독 패턴 Pub-Sub 패턴이라고 함.

차이점

이벤트 버스

  • 중간에 버스(대리자)가 의존성을 받아준다.
  • 두 클래스 간 직접적인 의존성이 없어진다.

4. 명령 패턴

명령(행동) 하나하나를 객체로 만든다.

왜 쓸까

  • 행동을 객체로 만들어 재사용, 기록, 취소, 병렬처리를 쉽게 할 수 있다.
    • 행동을 객체로 만들어서 하나하나 실행하고, 이를 다시 하나씩 되돌리면
      본래의 값으로 복구할 수 있음.
  • 행동을 객체화해서 복잡한 작업을 순차적으로 처리하거나 되돌릴 수 있다.

명령을 객체로

  • 행위를 TODO 리스트로 묶는다는 개념
    • 자연스럽게 할 일들을 객체화하는 것.
  • 행동과 관련된 정보를 객체로 묶고 이를 체계적으로 관리하는 것.

구현

목적에 따라 구현이 달라질 수 있음.

  1. 로깅 시스템
    • 플레이어들이 어떤 행동을 했었는지 꾸준히 로깅.
    • 로그들을 기반으로 부정 플레이를 감지하는 것.
  2. 되돌리기
    • 행동들을 순차적으로 사용하고 이를 내역(Stack)에 저장.
      • 위로 이동 -> 점프 -> 오른쪽으로 이동 순서로 Stack에 저장되는 것.
      • 거꾸로 되돌리면 오른쪽으로 이동 -> 점프 -> 위로 이동 순으로 출력.
    • 메멘토 패턴 적용
      • 객체가 한 행동을 모두 저장하고 있어야 함 -> 명령 패턴
      • 스택 자료구조

파사드 패턴

  • 수많은 클래스들이 서로 상호작용 하고 있는 상황
  • 이때, 외부의 다른 클래스가 이 클래스군에 접근을 해야한다면 모든 클래스와 상호작용하기 어려움
    그러므로 내부 클래스를 외부 클래스와 소통할 수 있도록 창구 역할을 하는 것.

명령 패턴이 적용되어 있는 사례

  • 커맨드 큐

5. 서비스 로케이터

왜 쓸까

  • 구체적인 서비스에 대한 의존성을 낮추기
  • Locate : 찾다
    • 서비스를 찾는 매니저의 매니저

특징

  • 싱글톤을 많이 만들다보면 싱글톤(일반적으로 '서비스') 참조가 난잡해질 수 있음.
  • 이를 해결하기 위해 각 로직을 담당하는 코드들이 구체적인 서비스 클래스를 참조하지 않고,
    서비스 로케이터를 참조하도록 설계할 수 있음.
  • 서비스 로케이터는 이런 서비스들을 등록하고 관리하는 역할을 수행.
profile
사파 개발자

0개의 댓글