[Unity] 옵저버(Observer) 패턴에 대해

조재훈·2024년 3월 25일

개요

옵저버 패턴은 일단 이번 포스트에서 카테고리를 Unity로 분류하긴 했는데 소프트웨어 분야에서 널리 쓰이는 디자인 패턴이므로 알아두는 것이 좋을 것 같아 Unity에서 옵저버 패턴을 구현하기 위해 카테고리를 Unity로 정했다

옵저버(Observer) 패턴

옵저버 패턴에 대해 알아보자. 관찰자 패턴, 발행-구독 모델이라고도 한다

옵저버

옵저버란? 관찰자라는 뜻으로 무언가를 지켜보는 오브젝트를 의미한다. 소프트웨어에서 무언가는 어떤 이벤트를 의미하는데 뭐 사용자의 입력이 될 수 있고, 어떤 객체의 상태가 변할 수 있고 하니까 이런 이벤트들을 지켜본다

Subject(관찰 대상자)

말 그대로, 관찰자의 관찰 대상이 되는 객체이다. 객체의 상태가 바뀌면 관찰자에게 이를 알려야한다

예시

예를 들어 유튜브를 생각해보자

우리는 유튜브에서 어떤 채널에 있는 재미있는 동영상을 보는데 계속 보고 싶은 경우 구독을 해서 영상이 올라올 때마다 새 동영상 알림이 와서 볼 수 있는데 만약 재미없게 되어 구독을 해지하면 이제 알림이 오지 않는다

이와 비슷하게 유튜브 채널은 새로운 영상을 올리는 Subject, 발행자가 되고 우리는 구독을 해서 그 알림을 받는 Observer가 된다


그래서 옵저버 패턴은 1:1 또는 1:N의 관계를 가질 수 있어 주로 분산 이벤트 핸들링 시스템 구현에 사용된다

근데 예시만 보면 관찰자가 능동적으로 관찰 대상자를 선택하는 것 같아 보이지만 실제로 쓰임을 보면 관찰 대상자가 관찰자를 등록하는 수동적인 관계라 예시는 예시로만 기억하자

구조

옵저버 패턴은 관찰 대상자와 관찰자로 나누어져 있어야 한다

관찰 대상자(Subject)에는 자신을 관찰할 관찰자(Observer)를 등록하고 해제하는 함수가 있어야 하고 또 등록한 관찰자들을 자료구조에 저장해야 한다. 어떤 이벤트가 발생하면 그 관찰자들에게 알려야 하니까

관찰자에는 어떤 이벤트를 받으면 호출할 함수를 정의해야 한다

이를 위해 관찰자, 관찰 대상자의 기본 메서드들을 인터페이스로 정의하고 특정 클래스에서 이를 상속받도록 한다

옵저버 패턴의 특징

이 패턴을 언제 사용하면 좋을까?

  • 한정된 시간, 특정한 케이스에만 객체를 관찰해야 하는 경우
  • 대상 객체가 변경될 때마다 다른 객체가 동작하게 해야 하는 경우
  • 한 객체의 상태가 변하면 다른 객체도 변경해야 하는데 어떤 객체들이 변해야 하는지 몰라도 되는 경우
    • 예를 들어 유튜브 채널은 구독자 한명 한명을 알 필요 없이 영상만 올리면 구독자들에게 알림이 알아서 간다
  • MVC 패턴에도 사용됨(Model, View, Controller)
    • Model, View는 각각 Subject와 Observer의 역할에 대응된다
    • 하나의 Model에 복수의 View가 대응됨

장점

  • Subject의 상태를 주기적으로 조회안해도 변경됨을 알 수 있다
  • 런타임 시점에서 발행자와 구독자의 관계를 맺을 수 있다
  • 느슨한 결합이 가능하다
  • 발행자(Subject)의 코드를 변경하지 않고도 새 구독자(Observer)의 클래스를 도입할 수 있어 개방 폐쇄 원칙(Open-Close Principle)을 지킬 수 있다

단점

  • 구독자(Observer)는 알림의 순서를 제어할 수 없고, 무작위로 알림을 받는다
  • 옵저버 패턴을 남발하면 코드의 구조와 동작의 이해가 어려워져 유지보수가 어려워짐
  • 다수의 옵저버 객체를 등록후 해지하지 않으면 메모리 누수가 발생할 수도

예제

유튜버가 영상을 업로드하면 구독자가 그 알림을 받는 과정을 유니티로 구현해보자

먼저 Observer와 Subject의 인터페이스를 구현해보자

namespace ObserverPattern
{
    public interface IObserver
    {
        void OnUpload(string channel);
    }

    public interface ISubject
    {
        void RegisterObserver(IObserver observer);

        void RemoveObserver(IObserver observer);

        void NotifyObservers();
    }
}

위 인터페이스는 먼저 유튜버에 해당하는 Subject 인터페이스에는 구독자를 등록, 해제하는 함수와 영상을 업로드하면 NotifyObservers 메서드가 실행돼 구독자들에게 알림이 간다

구독자에 해당하는 Observer 인터페이스는 업로드가 됐을 때 실행할 함수인 OnUpload를 선언해준다

그래서 YoutuberSubscriber 클래스를 만들자

먼저 Youtuber이다

public class Youtuber : MonoBehaviour, ISubject
{
    [SerializeField] Button uploadBtn;

    public string channel;

    private List<IObserver> observers = new List<IObserver>();

    void Start()
    {
        uploadBtn.onClick.AddListener(NotifyObservers);
    }

    public void RegisterObserver(IObserver observer)
    {
        observers.Add(observer);
    }

    public void RemoveObserver(IObserver observer)
    {
        observers.Remove(observer);
    }

    public void NotifyObservers()
    {
        foreach (IObserver observer in observers)
        {
            observer.OnUpload(channel);
        }
    }
}

ISubject 인터페이스를 상속받아 구독자(Observer)들을 저장하는 변수를 두고 인터페이스 함수들을 정의해준다

다음 Subscriber이다

public class Subscriber : MonoBehaviour, IObserver
{
    public Youtuber youtuber;
    public string userName;

    void OnEnable()
    {
        youtuber.RegisterObserver(this);
    }
    
    void OnDisable() 
    {
        youtuber.RemoveObserver(this);
    }

    public void OnUpload(string channel)
    {
        Debug.Log("[" + userName + "]님 " + channel + "에서 새 영상을 업로드했습니다");
    }
}

오브젝트가 활성화/비활성화될 때 구독할 youtuber에 구독, 구독해제 해준다
그리고 유튜버가 영상을 업로드하면 OnUpload 함수가 실행이 될 것이다

다음의 오브젝트들을 선언하고 오브젝트에 해당하는 스크립트를 컴포넌트로 등록해준다


게임을 시작하고 비활성화되어 있는 Subscriber를 활성화하고 게임 화면에 있는 버튼을 누르게 되면? 다음과 같이 로그가 뜬다

다음과 같이 순서는 무작위인것으로 보인다

그리고 다시 비활성화하고 버튼을 누르면 아무것도 안 뜬다

정리

위와 같이 간단한 예제를 통해 옵저버 패턴을 알아보았다.
이 패턴과 비슷한 일을 하는 델리게이트와 액션도 있는데 둘 다 쓰임새가 다른 것 같은데 이것도 나중에 정리해보면 좋을 것 같다

참고를 많이 한 블로그는 적어두겠습니다 감사합니다
Inpa Dev

profile
나태지옥

0개의 댓글