김현우
  • Unity(C#)와 Unreal(C++) 중심의 옵저버 패턴 (Observer Pattern)
    2025년 06월 01일 17시 16분 08초에 업로드 된 글입니다.
    작성자: kugorang
    728x90
    728x90

    들어가며

    감히 싱글톤과 양대 산맥이라 불리울 수 있지 않을까 싶다.

     

    이번 학기 오픈소스SW개발방법및도구 강의에서 마지막으로 디자인 패턴을 조사하는 과제가 주어졌다. 싱글톤 패턴을 정리할까 하다, 싱글톤은 눈 감고도 코드를 작성하는 수준이라 아직 눈 뜨고 코드를 작성해야 하는 옵저버 패턴과 조금 더 친숙해보고자 이를 조사했다. 목적, 동작 방식, UML, 그리고 코드 순서로 문서를 기술한다.

     

    Goal (목적)

    옵저버 패턴은 한 객체의 상태 변화를 자동으로 여러 다른 객체들에게 알리는 디자인 패턴이다. 이를 통해 한 객체(주체, Subject)의 변화가 다수의 객체(옵저버, Observer)로 일대다로 전파되어 각 객체가 알아서 대응할 수 있다. 이 패턴의 주요 목적은 객체 사이의 결합도를 낮추고 이벤트 기반의 효율적인 상호 작용을 구현하는 데 있다. 옵저버 패턴이 없다면 여러 객체들이 변화 여부를 계속 폴링(polling)해야 하거나 직접 서로를 호출해야 해서 비효율과 복잡성이 증가한다. 옵저버 패턴은 이를 해결하기 위해 주체가 변화 통보를 중앙 관리하여, 옵저버들은 필요한 이벤트만 받아 처리함으로써 불필요한 반복 요청이나 스파게티식 의존 관계를 줄여준다. 결과적으로 게임 엔진을 비롯한 소프트웨어 전반에서 이벤트 기반 시스템의 토대가 되는 패턴이며, MVC(Model-View-Controller) 아키텍처의 핵심 원리 중 하나로도 활용된다.

     

    Detail (동작 방식 및 활용)

    옵저버 패턴은 크게 Subject(발행자)Observer(구독자) 두 요소로 구성된다. Subject는 자신의 상태를 관리하면서 Observer 목록을 유지한다. Subject의 상태가 변경되면 미리 등록된 Observer들에게 이벤트알림(notify)을 보내 각 Observer가 자신의 update 메서드 등을 자동 호출하도록 한다. Observer 측은 Subject에 등록(subscribe)하거나 해지(unsubscribe)하는 방법을 제공받아, 관심 대상의 변화가 있을 때 통지를 받고 필요한 동작을 수행한다. 이때 Subject는 Observer들의 구체적인 내용이나 개수를 알 필요 없이, 인터페이스를 통해 느슨하게 통신하므로 코드의 결합도가 낮아진다. 또한 알림 전달 방식에는 두 가지가 있는데, 변화된 데이터를 함께 보내는 푸시(push) 방식과 변경 사실만 알리고 Observer가 변경된 내용을 직접 가져가는 풀(pull) 방식이 있다. 예를 들어 푸시 방식에서는 Subject가 "변경된 값 = 50"과 함께 통지하고, 풀 방식에서는 Subject가 "변경됨"만 알리면 Observer가 필요한 정보를 추가 요청하는 식이다 (각 방식은 상황에 따라 장단점이 있다).

     

    게임 개발을 비롯한 다양한 소프트웨어 분야에서 옵저버 패턴은 이벤트 처리와 화면 갱신 등에 폭넓게 응용된다. GUI 프레임워크에서는 한 모델의 데이터가 바뀌면 여러 View(UI 요소)에 즉시 반영되는 구조에 옵저버 패턴이 쓰이며, 실시간 게임 개발에서는 캐릭터 상태 변화나 게임 이벤트 발생 시 이를 여러 시스템이 실시간으로 감지하도록 구현하는 데 활용된다. 예를 들어 플레이어 캐릭터의 체력 변화 이벤트를 정의해 두면, UI 체력바, 사운드 효과 시스템, 업적(Achievement) 시스템 등이 각각 해당 이벤트를 구독하여 체력 변화 시 자동으로 UI 갱신, 경고음 재생, 업적 달성 검사 등을 수행할 수 있다. Unity 엔진에서는 C# 언어 차원에서 제공되는 이벤트(event)델리게이트(delegate)를 사용하여 옵저버 패턴을 구현하는 경우가 많다. 예컨대 event 키워드로 이벤트를 정의하고 여러 리스너가 += 연산으로 구독하여, 주체에서 EventName.Invoke()만 호출하면 다수의 구독자가 콜백을 받는 구조를 쉽게 만들 수 있다. Unreal 엔진의 경우 C++ 기반 구현이지만, 델리게이트(Delegate)이벤트 디스패처(Event Dispatcher) 개념을 통해 동일한 패턴을 구현한다. Unreal의 델리게이트는 언리얼식 Observer 패턴의 구현체로서, 객체들이 특정 이벤트에 구독하도록 하고, 디스패처(주체)는 자신의 이벤트 발생을 알지만 누가 듣는지는 모르는 채 브로드캐스트하는 구조를 취한다. 이런 방식으로 Unreal에서는 DECLARE_DELEGATE, DECLARE_EVENT 등의 매크로로 커스텀 이벤트를 정의하고, AddDynamic/AddUObject 등의 함수로 Observer 측의 함수를 바인딩하여, 주체에서 Broadcast()를 호출하면 다수의 구독자들이 한꺼번에 알림을 받을 수 있다. 정리하면, 옵저버 패턴은 이벤트 기반으로 여러 객체의 동작을 느슨하게 연결해 주며, Unity와 Unreal 엔진 모두 이러한 개념을 활용해 게임 오브젝트 상태 변화에 따른 UI 업데이트, 게임 진행 로직 처리 등을 효율적으로 관리한다.

     

    UML (구조 다이어그램 및 구성요소 역할)

    그림 1: 옵저버 패턴의 UML 클래스 다이어그램.  한 개의 Subject가 다수의 Observer에게 통지하는  일대다 관계 를 보여준다.

     

    Subject는 상태 변경 시 자신의 Observer 리스트에 등록된 대상들에게 알림을 보내며, Observer들은 해당 알림을 받으면 자신의 상태를 갱신한다. 아래는 각 구성 요소의 역할이다.

    • Subject (주제, 발행자): 관찰 대상 객체로서 상태를 가지고 있다. Observer 등록(register)해지(unregister)를 위한 메서드를 제공하며, 내부 상태 변화가 발생하면 notify() 메서드를 통해 연결된 Observer들에게 변화를 통지한다. Subject는 구독자들의 구체적 클래스나 동작을 몰라도 되며, 오직 인터페이스(API)로만 Observer들과 소통한다.
    • Observer (관찰자, 구독자): Subject로부터 알림을 받는 객체를 가리킨다. Observer 인터페이스(또는 추상 클래스)는 update()와 같은 통지 처리 메서드를 정의하며, 구체 Observer들은 이를 구현한다. Observer는 필요한 경우 Subject의 참조를 통해 상태를 조회할 수도 있지만(풀 방식), 대부분 알림과 함께 전달된 변경 정보를 활용한다(푸시 방식).
    • ConcreteSubject (구체 주체): Subject를 구현한 실제 클래스이다. 구체적인 상태 데이터(예: 게임 캐릭터의 체력, 날씨 정보 등)를 보유하며, attach()/detach()/notify() 등의 메서드를 통해 Observer 목록을 관리하고 이벤트를 발생시킨다. 예를 들어 게임에서 Player 객체GameManager가 ConcreteSubject가 될 수 있다.
    • ConcreteObserver (구체 관찰자): Observer를 구현한 실제 클래스들이다. Subject의 변경 알림을 받아 처리하는 객체로, 각각 고유한 동작을 수행한다. 예를 들어 UI 요소, 로그 출력기, AI 캐릭터 등이 ConcreteObserver가 되어 동일한 Subject의 이벤트에 각기 반응할 수 있다. ConcreteObserver는 보통 초기화 시 Subject에 자신을 등록하며, 알림을 받으면 내부 로직을 통해 화면 갱신, 데이터 저장, 추가 액션 실행 등을 수행한다.

     

    예시 시나리오: Unity에서 플레이어 캐릭터(Subject)의 체력이 변할 때, 체력바 UI(Observer)게임 오버 매니저(Observer)가 즉시 통지받아 각각 화면 표시를 업데이트하거나 게임 오버 처리를 하는 구조를 생각해보자. 이러한 UML 구조 덕분에 플레이어 객체는 자신이 누구에게 통지하는지 몰라도 되고, UI나 매니저는 플레이어의 상태를 능동적으로 조회하지 않아도 된다. 새로운 Observer가 추가돼도 Subject의 코드에는 영향이 없으므로, 기능 확장에 유연한 설계를 얻을 수 있다.

    728x90

    Code (Unity C# 및 Unreal C++ 예제)

    아래에는 Unity 엔진(C#)Unreal 엔진(C++)에서 각각 옵저버 패턴을 활용하여 캐릭터 체력 변화 이벤트를 처리하는 간단한 예제 코드를 제시한다. 각 코드에서는 Subject와 Observer의 관계, 그리고 이벤트 통지 방식을 보여준다.

     

    Unity (C#) 예제

    using UnityEngine;
    using System;
    
    //// Subject (발행자): 플레이어 캐릭터의 체력 상태를 관리하고 변화 시 알림을 보냄
    public class Player : MonoBehaviour {
        // 체력 변화 이벤트 정의 (구독자들은 이 이벤트를 통해 알림을 받음)
        public event Action<float> OnHealthChanged;
        private float health = 100f;
    
        public void TakeDamage(float damage) {
            health -= damage;
            // 체력이 변하면 등록된 모든 Observer들에게 알림을 보낸다
            if (OnHealthChanged != null) {
                OnHealthChanged(health);  // 새 체력 값을 전달하며 이벤트 호출
            }
        }
    }
    
    //// Observer (관찰자) 1: 체력 바(UI). 플레이어의 체력 변경 이벤트를 구독하여 UI를 갱신
    public class HealthBarUI : MonoBehaviour {
        public Player player;  // 관찰할 Player 객체 (Inspector 등을 통해 할당)
    
        void OnEnable() {
            // Subject의 이벤트에 Observer의 처리 메서드 등록 (구독)
            player.OnHealthChanged += UpdateHealthBar;
        }
    
        void OnDisable() {
            // 객체 비활성화 시 이벤트 구독 해제 (메모리 누수 및 잘못된 참조 방지)
            player.OnHealthChanged -= UpdateHealthBar;
        }
    
        void UpdateHealthBar(float newHealth) {
            // 전달받은 체력 값으로 UI 요소를 업데이트
            Debug.Log($"플레이어 체력 업데이트: {newHealth}");
            // 예: 체력바 fillAmount를 newHealth 기반으로 변경하는 로직 등이 들어감
        }
    }
    
    //// Observer (관찰자) 2: 업적 시스템. 플레이어의 체력 변경을 구독하여 특정 조건 시 업적 처리
    public class AchievementSystem : MonoBehaviour {
        public Player player;  // 관찰할 Player 객체
    
        void OnEnable() {
            player.OnHealthChanged += OnPlayerHealthChanged;
        }
    
        void OnDisable() {
            player.OnHealthChanged -= OnPlayerHealthChanged;
        }
    
        void OnPlayerHealthChanged(float newHealth) {
            // 체력이 0 이하로 떨어지면 업적 달성 처리
            if (newHealth <= 0) {
                Debug.Log("업적 달성: '첫 사망' 해제!");
            }
        }
    }

    위의 Unity C# 코드에서 Player 클래스가 Subject로서 OnHealthChanged 이벤트를 제공하고, TakeDamage 메서드 내에서 체력 감소 시 OnHealthChanged(health)를 호출함으로써 구독자(Observer)들에게 알림을 보낸다. HealthBarUIAchievementSystem 클래스는 Player의 이벤트에 구독하여 각각 알림을 받고 자신들의 메서드(UpdateHealthBar, OnPlayerHealthChanged)를 실행함으로써 UI 갱신이나 업적 체크 등의 동작을 수행한다. 이러한 구조 덕분에 Player 객체는 자신의 체력 변화에 따른 후속 처리들을 몰라도 되며, Observer들은 필요할 때만 알림을 받아 작업을 처리하므로 효율적이고 구조적인 코드가 이루어진다.

     

    Unreal (C++) 예제

    #include "CoreMinimal.h"
    #include "GameFramework/Actor.h"
    #include "PlayerCharacter.generated.h"
    
    //// Subject (발행자): 플레이어 캐릭터 클래스, 체력 변화시 Observer들에게 이벤트를 전달
    UCLASS()
    class APlayerCharacter : public AActor {
        GENERATED_BODY()
    
    public:
        // 델리게이트 선언: 체력 변경 이벤트 (매개변수로 새로운 체력 값을 전달)
        DECLARE_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float);
        FOnHealthChanged OnHealthChanged;  // 델리게이트 인스턴스
    
        float Health = 100.0f;
    
        void TakeDamage(float Damage) {
            Health -= Damage;
            // 체력이 변경되면 이벤트 브로드캐스트: 등록된 모든 Observer들에게 알림
            OnHealthChanged.Broadcast(Health);
        }
    };
    
    //// Observer (관찰자): 체력 바 UI 위젯 (UUserWidget의 가상 시나리오)
    // 플레이어의 OnHealthChanged 델리게이트에 자신을 구독하고, 이벤트 발생 시 UI를 갱신
    class UHealthBarWidget /* : public UUserWidget (가정) */ {
    public:
        void SubscribeToPlayer(APlayerCharacter* Player) {
            if (Player) {
                // Subject의 델리게이트에 Observer의 멤버 함수 바인딩 (구독 등록)
                Player->OnHealthChanged.AddUObject(this, &UHealthBarWidget::UpdateHealthBar);
            }
        }
    
        // 체력 변경 알림을 받으면 호출될 함수
        void UpdateHealthBar(float NewHealth) {
            // 전달된 체력 값으로 UI 표시를 갱신
            UE_LOG(LogTemp, Log, TEXT("플레이어 체력 업데이트: %f"), NewHealth);
            // 예: 프로그레스 바의 퍼센티지 갱신 등의 로직
        }
    };

    위의 Unreal C++ 예제에서는 APlayerCharacter 클래스가 Subject 역할을 하며, DECLARE_MULTICAST_DELEGATE_OneParam 매크로를 통해 체력 변화 이벤트 델리게이트 OnHealthChanged를 정의하고 있다. TakeDamage 메서드에서 체력이 감소하면 OnHealthChanged.Broadcast(Health)를 호출하여 여러 Observer들에게 동시에 이벤트를 브로드캐스트한다. UHealthBarWidget 클래스는 UI 오브젝트를 가정한 Observer로서, SubscribeToPlayer 함수를 통해 특정 플레이어의 OnHealthChanged 델리게이트에 자기 자신의 UpdateHealthBar 함수를 AddUObject 방식으로 구독 등록한다. 그 결과 플레이어 체력 변동 시 UpdateHealthBar가 자동으로 호출되어 UI를 업데이트하게 된다. (참고로, 이와 동일한 방식으로 다른 Observer들도 Player->OnHealthChanged.AddUObject(...) 또는 AddLambda(...)를 사용하여 이벤트에 등록될 수 있으며, Subject인 플레이어는 등록된 Observer들의 존재를 신경 쓰지 않고 Broadcast만 호출하면 된다.) 이러한 Unreal의 델리게이트 메커니즘을 통해 Observer 패턴이 구현되어, 게임 플레이에서 발생하는 다양한 이벤트(캐릭터 상태 변화 등)를 다수의 컴포넌트가 실시간으로 처리하도록 만들 수 있다.

     

    마치며

    이상으로 옵저버 패턴에 대한 정리를 진행해보았다. 처음에는 이 디자인 패턴을 굳이 써야 하나 싶은 생각이 있었다. 필요한 클래스는 참조로 받으면 되니 말이다. 그러나, 프로그램 규모가 커지고 이벤트 방식의 프로그램 구조로 변경해 나가다보니 이러한 옵저버 패턴을 통해 필요할 때 등록하고, 더 이상 필요하지 않으면 해제하는 방식이 클래스 인스턴스의 관리를 보다 쉽게 해주어 너무나도 좋았던 기억이 난다.

    728x90
    728x90
    댓글