오르막길

[Unity] 옵저버 패턴 (Observer Pattern) 알아보기 본문

학습 기록하기/Unity

[Unity] 옵저버 패턴 (Observer Pattern) 알아보기

nanalyee 2025. 5. 26. 14:50

✅ 기본 개념

  • 옵저버 패턴 (Observer Pattern)은 발행자구독자 간의 일대다 관계를 정의하여, 발행자의 상태가 변할 때 이를 구독자에게 자동으로 알리는 패턴
  • 이벤트 기반의 비동기 구조에서 자주 사용됨

 

🗂️ 주요 특징

  • 느슨한 결합: 발행자와 구독자가 서로 직접 참조하지 않음
  • 확장성: 구독자를 쉽게 추가하거나 제거 가능
  • 재사용성: 동일한 이벤트를 여러 구독자가 동시에 사용할 수 있음
  • RuleTrigger옵저버 패턴으로 설계하는 게 좋음
  • EventPublisher가 이미 간단한 옵저버 역할을 하고 있지만, 더 체계적으로 관리할 수 있음

 

📝 옵저버 패턴 적용 (RuleTrigger)

  • EventPublisher - 이벤트 발송자
  • RuleManager - 이벤트 수신자
  • RuleData - 트리거 상태 관리

 

📂 구조

1. 발행자 (Publisher) - EventPublisher

using System;
using UnityEngine;

public static class EventPublisher
{
    public static event Action<string> OnRuleTriggered;

    public static void TriggerRule(string ruleID)
    {
        // 모든 구독자에게 알림
        OnRuleTriggered?.Invoke(ruleID);
        Debug.Log($"[Publisher] Rule Triggered: {ruleID}");
    }
}
  • 역할: 이벤트를 정의하고 발송하는 전역 이벤트 관리자.
  • 핵심: OnRuleTriggered 이벤트를 구독한 모든 객체에게 알림을 전달.

2. 구독자 (Observer) - RuleManager

using UnityEngine;
using System.Collections.Generic;

public class RuleManager : MonoBehaviour
{
    [SerializeField] private RuleDatabase ruleDatabase;
    private Dictionary<string, bool> ruleStatus = new Dictionary<string, bool>();

    private void OnEnable()
    {
        // 이벤트 구독
        EventPublisher.OnRuleTriggered += HandleRuleTriggered;
        InitializeRules();
    }

    private void OnDisable()
    {
        // 구독 해제
        EventPublisher.OnRuleTriggered -= HandleRuleTriggered;
    }

    private void InitializeRules()
    {
        foreach (RuleData rule in ruleDatabase.rules)
        {
            if (!rule.isRepeatable && !ruleStatus.ContainsKey(rule.ruleID))
            {
                ruleStatus[rule.ruleID] = false;
            }
        }
    }

    // 규칙 처리
    private void HandleRuleTriggered(string ruleID)
    {
        RuleData rule = ruleDatabase.rules.Find(r => r.ruleID == ruleID);
        if (rule != null)
        {
            // 반복 불가 규칙인지 검사
            if (!rule.isRepeatable && ruleStatus.ContainsKey(rule.ruleID) && ruleStatus[rule.ruleID])
            {
                Debug.Log($"[Observer] Rule '{rule.ruleID}' has already been triggered and is not repeatable.");
                return;
            }

            Debug.Log($"[Observer] Rule Triggered: {rule.description}");

            if (!rule.isRepeatable)
            {
                ruleStatus[rule.ruleID] = true;
            }
        }
        else
        {
            Debug.LogWarning($"[Observer] No matching rule found for ID: {ruleID}");
        }
    }
}
  • 역할: 특정 이벤트가 발생했을 때 이를 처리하는 구독자.
  • 핵심: EventPublisher.OnRuleTriggered를 구독하여 HandleRuleTriggered() 메서드로 이벤트를 처리.

3. 트리거 (Trigger) - RuleTrigger

using UnityEngine;

public class RuleTrigger : MonoBehaviour
{
    [SerializeField] private string ruleID;

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            // 트리거 발동 (Publisher에게 알림 전송)
            EventPublisher.TriggerRule(ruleID);
        }
    }
}

  • 역할: 물리적인 트리거 이벤트를 감지하고 발행자에게 이벤트를 전달.
  • 핵심: OnTriggerEnter2D()에서 EventPublisher.TriggerRule() 호출로 발행.

 

📂 전체 데이터 흐름

  1. 플레이어가 트리거 영역에 진입 (RuleTrigger)
    • OnTriggerEnter2D()가 호출되어 EventPublisher.TriggerRule(ruleID) 실행.
  2. 이벤트 발송 (EventPublisher)
    • OnRuleTriggered 이벤트가 모든 구독자 (RuleManager)에게 발송됨.
  3. 이벤트 수신 (RuleManager)
    • HandleRuleTriggered(string ruleID)가 호출되어 규칙을 처리.

🛠️ 실제 흐름 예시 (플레이어가 트리거에 닿을 때)

  1. 플레이어가 RuleTrigger의 콜라이더에 닿음
  2. [RuleTrigger] Player entered trigger area
  3. EventPublisher가 이벤트 발송
  4. [Publisher] Rule Triggered: Rule001
  5. RuleManager가 이벤트 수신
  6. [Observer] Rule Triggered: (Rule001에 대한 설명)
  • RuleTrigger는 단순히 물리적 충돌을 감지하는 발생 조건일 뿐
  • EventPublisher는 이벤트를 발송하는 핵심 발행자
  • RuleManager는 이 발행자의 이벤트를 구독하여, 알림이 오면 적절히 반응하는 구독자

 

📂 그 외 추가 가능한 구독자 예시

1. 🗺️ 미니맵 시스템 (MiniMapManager)

  • 목적: 특정 룰이 발동되면 미니맵에 새로운 정보를 표시.
  • 예시: 어떤 지역의 문이 열리면 미니맵에 경로 표시.
using UnityEngine;

public class MiniMapManager : MonoBehaviour
{
    private void OnEnable()
    {
        EventPublisher.OnRuleTriggered += HandleRuleTriggered;
    }

    private void OnDisable()
    {
        EventPublisher.OnRuleTriggered -= HandleRuleTriggered;
    }

    private void HandleRuleTriggered(string ruleID)
    {
        Debug.Log($"[MiniMap] Updating map for rule: {ruleID}");
        // 예시: 문이 열리면 미니맵에 새로운 경로 표시
    }
}

2. 💡 환경 효과 시스템 (EnvironmentEffectManager)

  • 목적: 특정 규칙이 발동되면 게임 환경에 시각적 효과 추가.
  • 예시: 특정 트리거에서 불꽃이 일어나거나, 조명이 깜빡임.
using UnityEngine;

public class EnvironmentEffectManager : MonoBehaviour
{
    private void OnEnable()
    {
        EventPublisher.OnRuleTriggered += HandleRuleTriggered;
    }

    private void OnDisable()
    {
        EventPublisher.OnRuleTriggered -= HandleRuleTriggered;
    }

    private void HandleRuleTriggered(string ruleID)
    {
        Debug.Log($"[EnvironmentEffect] Triggering environmental effect for rule: {ruleID}");
        // 예시: 연기가 발생하거나 조명이 깜빡이는 효과 추가
    }
}

3. 🖥️ UI 시스템 (UIManager)

  • 목적: 규칙이 발동될 때 UI를 업데이트하거나 메시지 표시.
  • 예시: 특정 문이 열리면 "새로운 길이 열렸습니다!" 팝업 표시.
using UnityEngine;

public class UIManager : MonoBehaviour
{
    private void OnEnable()
    {
        EventPublisher.OnRuleTriggered += HandleRuleTriggered;
    }

    private void OnDisable()
    {
        EventPublisher.OnRuleTriggered -= HandleRuleTriggered;
    }

    private void HandleRuleTriggered(string ruleID)
    {
        Debug.Log($"[UI] Displaying message for rule: {ruleID}");
        // 예시: 화면에 팝업 메시지 표시
    }
}

4. 🎥 카메라 시스템 (CameraManager)

  • 목적: 특정 이벤트가 발생하면 카메라를 전환하거나 화면을 흔들리게 함.
  • 예시: 적이 등장하면 카메라가 그 방향으로 순간 이동.
using UnityEngine;

public class CameraManager : MonoBehaviour
{
    private void OnEnable()
    {
        EventPublisher.OnRuleTriggered += HandleRuleTriggered;
    }

    private void OnDisable()
    {
        EventPublisher.OnRuleTriggered -= HandleRuleTriggered;
    }

    private void HandleRuleTriggered(string ruleID)
    {
        Debug.Log($"[Camera] Changing camera perspective for rule: {ruleID}");
        // 예시: 특정 상황에서 카메라 이동 또는 줌 효과
    }
}

🔗 구독자들의 공통점

  • OnEnable: 이벤트 구독 시작
  • OnDisable: 이벤트 구독 해제
  • HandleRuleTriggered: 규칙 발동 시의 반응 로직 정의

 

🛠️ 장점

  • 유지보수 용이: 발행자와 구독자가 독립적이므로 코드 관리가 쉬움
  • 확장성: 새로운 구독자를 쉽게 추가 가능
  • 재사용성: 동일한 이벤트를 다양한 구독자가 동시에 처리 가능

 

⚠️ 주의점

  • 구독 해제를 잊으면 메모리 누수가 발생할 수 있음
  • 이벤트 호출이 많아지면 성능 문제가 발생할 수 있음

 

📝 옵저버 패턴 팁

  1. 메모리 관리
    • 구독 해제 (=)를 항상 잊지 말기
    • 특히 씬 전환 시 구독 해제를 안 하면 메모리 누수의 주범
  2. 멀티스레딩
    • 이벤트가 많을 때는 비동기 처리와의 호환성을 고려해야 함.
    • 특히, UnityMain Thread 문제는 항상 염두에 두기
  3. 구독자 관리
    • 너무 많은 구독자가 하나의 이벤트를 구독할 때는 성능 이슈가 생길 수 있음
    • 필요하다면 Event QueueEvent Buffer로 최적화
  4. 다양한 이벤트 유형
    • 단순 Action뿐만 아니라 EventArgs, UnityEvent, C# Delegate 같은 다양한 이벤트 방식을 혼용해보기
  5. 디버그 편의성
    • 이벤트 로그를 잘 관리하면 나중에 디버그가 훨씬 쉬워짐
    • 특히, 예기치 않은 이벤트 호출 문제를 쉽게 잡아낼 수 있음