Deff_Dev

[디자인 패턴] 옵저버(관찰자) 패턴 (Observer Pattern) 본문

CS/디자인 패턴

[디자인 패턴] 옵저버(관찰자) 패턴 (Observer Pattern)

Deff_a 2024. 11. 7. 00:59

⚒️ 옵저버 패턴이 뭘까 

https://refactoring.guru/ko/design-patterns/observer

 

어떤 객체의 상태가 변할 때, 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지 받고 자동으로 업데이트될 수 있게 만드는 패턴

 

해당 객체 정보를 모르고 있더라도, 변화가 발생하면 등록되어 있는 이벤트들이 전부 다 실행되기 때문에 정말 편리하다.

 

 

사용 방법

C# 에서는 델리게이트(Action, Func 등)를 이용하여 여러 함수를 구독, 실행시킬 수 있다.

 

[체력 상호작용 예제 코드]

더보기
using UnityEngine;
using System;

public class PlayerHealth : MonoBehaviour
{
    [SerializeField] private int maxHealth = 100;
    private int currentHealth;

    // 체력 변경 이벤트
    public event Action<int> OnHealthChanged;
    // 사망 이벤트
    public event Action OnDeath;

    private void Start()
    {
        currentHealth = maxHealth;
    }

    public void TakeDamage(int damage)
    {
        currentHealth = Mathf.Max(currentHealth - damage, 0);
        OnHealthChanged?.Invoke(currentHealth);

        if (currentHealth == 0)
        {
            Die();
        }
    }

    public void Heal(int amount)
    {
        currentHealth = Mathf.Min(currentHealth + amount, maxHealth);
        OnHealthChanged?.Invoke(currentHealth); // 체력 이벤트 실행
    }

    private void Die()
    {
        OnDeath?.Invoke(); // 사망 이벤트 실행
    }

    public int GetCurrentHealth()
    {
        return currentHealth;
    }

    public int GetMaxHealth()
    {
        return maxHealth;
    }
}
using UnityEngine;
using UnityEngine.UI;

public class HealthUI : MonoBehaviour
{
    [SerializeField] private PlayerHealth playerHealth;
    [SerializeField] private Slider healthSlider;
    [SerializeField] private Text healthText;

    private void Start()
    {
    	// 이벤트 구독
        if (playerHealth != null)
        {
            playerHealth.OnHealthChanged += UpdateHealthUI;
            InitializeHealthUI();
        }
    }

    private void OnDestroy()
    {
    	// 이벤트 해제
        if (playerHealth != null)
        {
            playerHealth.OnHealthChanged -= UpdateHealthUI;
        }
    }

    private void InitializeHealthUI()
    {
        healthSlider.maxValue = playerHealth.GetMaxHealth();
        UpdateHealthUI(playerHealth.GetCurrentHealth());
    }

    private void UpdateHealthUI(int currentHealth)
    {
        healthSlider.value = currentHealth;
        healthText.text = $"HP: {currentHealth} / {playerHealth.GetMaxHealth()}";
    }
}

⚠️ 옵저버 패턴의 단점 

옵저버 패턴은 정말 편리한 디자인 패턴은 맞지만 옵저버 패턴이 가지고 있는 단점도 알고 있어야 한다.

 

※ 단점을 알고 있어야 문제가 생겼을 때, 문제 상황을 빠르게 해결할 수 있다.

 

내가 말할 부분은 단점보다는 주의할 점에 가깝긴 하다.

 

1. 이벤트 구독 해제를 꼭 해야한다.

이벤트 구독 해제를 하지 않는다면 파괴되거나 비활성화된 객체의 이벤트를 계속 호출하는 현상이 발생하고 이는 성능 저하 또는 크리티컬한 문제를 발생시킬 수 있다.

즉, 메모리 누수의 원인이 될 수 있다.

 

예를 들어, 캐릭터의 체력의 변화가 있을 때, 상태창 UI에 체력바 갱신 이벤트를 구독했다고 가정해보자.

상태창 UI가 활성화되어 있을 땐 원하는 대로 동작할 것 이다.

하지만 상태창 UI가 비활성화되어 있다면, 여전히 해당 이벤트를 참조하고 있기 때문에 GC가 수거해가지 않는다.

그렇다면 상태창 UI는 화면에 나오지 않지만 체력의 변화가 있을 때마다 체력바 갱신 이벤트가 실행될 것이다.

 

그러니 사용하지 않는 이벤트를 구독 해제하는 습관을 길러야한다.

 

2. 등록 이벤트들의 의존성을 줄여야 한다.

등록된 이벤트가 호출되는 순서가 있으므로, 각 이벤트들은 커플링이 있으면 안된다.

즉, 등록된 이벤트들의 동작 결과가 서로에게 영향을 주는 것을 최소화해야 한다.

 


📍이벤트 집계자 패턴 (Event Aggregator Pattern)

여러 객체 간의 이벤트 발행과 구독을 중앙에서 관리하여 객체간의 의존성을 줄인 패턴이다.

 

이벤트 발행자와 구독자 사이의 직접적인 의존성을 줄여 관리 및 사용의 효율성과 편리함을 향상시켰다.

 

모든 디자인 패턴이 그렇듯 과도한 사용은 시스템의 복잡성을 증가시킬 수 있고, 이벤트 흐름을 추적하기 어려울 수 있으므로 적절하게 사용해야한다.

 

[예시 코드]

더보기

실제로는 이벤트 이름을 상수나 열거형으로 관리하는 것이 좋다.

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

// 이벤트 집계자 클래스
public class EventAggregator : MonoBehaviour
{
    private static EventAggregator instance;
    private Dictionary<string, List<Delegate>> events = new Dictionary<string, List<Delegate>>();

    public static EventAggregator Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("EventAggregator");
                instance = go.AddComponent<EventAggregator>();
                DontDestroyOnLoad(go);
            }
            return instance;
        }
    }

    // 이벤트 구독
    public void Subscribe<T>(string eventName, Action<T> listener)
    {
        if (!events.ContainsKey(eventName))
        {
            events[eventName] = new List<Delegate>();
        }
        events[eventName].Add(listener);
    }

    // 이벤트 구독 해제
    public void Unsubscribe<T>(string eventName, Action<T> listener)
    {
        if (events.ContainsKey(eventName))
        {
            events[eventName].Remove(listener);
        }
    }

    // 이벤트 발행
    public void Publish<T>(string eventName, T eventArgs)
    {
        if (events.ContainsKey(eventName))
        {
            foreach (var listener in events[eventName])
            {
                (listener as Action<T>)?.Invoke(eventArgs);
            }
        }
    }
}

// 사용 예시: 플레이어 클래스
public class Player : MonoBehaviour
{
    private void OnEnable()
    {
        EventAggregator.Instance.Subscribe<int>("ScoreChanged", OnScoreChanged);
    }

    private void OnDisable()
    {
        EventAggregator.Instance.Unsubscribe<int>("ScoreChanged", OnScoreChanged);
    }

    private void OnScoreChanged(int newScore)
    {
        Debug.Log($"Player's score changed to: {newScore}");
    }

    public void IncreaseScore(int amount)
    {
        // 점수 증가 로직...
        EventAggregator.Instance.Publish("ScoreChanged", amount);
    }
}

// 사용 예시: UI 클래스
public class ScoreUI : MonoBehaviour
{
    private void OnEnable()
    {
        EventAggregator.Instance.Subscribe<int>("ScoreChanged", UpdateScoreDisplay);
    }

    private void OnDisable()
    {
        EventAggregator.Instance.Unsubscribe<int>("ScoreChanged", UpdateScoreDisplay);
    }

    private void UpdateScoreDisplay(int newScore)
    {
        Debug.Log($"Updating UI with new score: {newScore}");
        // UI 업데이트 로직...
    }
}