본문 바로가기

[Unity] 디자인 패턴 : 옵저버 패턴(Observer Pattern)

앤디가이 2022. 6. 17.

Unity와 C#을 통한 옵저버 패턴(Observer Pattern)의 정의와 사용 방법에 대해 알아보자.

 

1. 옵저버 패턴(Observer Pattern)이란?

옵저버 패턴(Observer Pattern)은 한 객체(주제)의 상태가 바뀌면 그 객체에 의존하는 다른 객체(옵저버들)에게 메시지를 전달하고 옵저버들은 각자의 내용을 갱신할 수 있는 디자인 패턴으로 일대다(one-to-many) 의존성을 가진다.

옵저버 패턴의 핵심은 '느슨한 결합'이다. 결합이긴 하지만 강한 결합이 아닌 느슨하게 결합되어, 연결을 해지하기 유용하다. 느슨한 결합을 사용하면 객체 사이의 상호 의존성을 최소화할 수 있기 때문에 유연한 객체지향 시스템을 구축할 수 있다.

 

옵저버 패턴은 요즘 구독 서비스 같은 개념으로 이해하면 편하다.

예를 들어 넷플릭스가 주제 객체라고 정의하고 구독자들을 옵저버로 정의해보자. 

구독자들은 구독을 통해 넷플릭스 서비스를 이용할 수 있게 연결이 되며, 구독 해지를 통해 넷플릭스 서비스와 연결이 해지된다. 

구독 시 넷플릭스의 정보(신규 시리즈 정보, 시청 리스트 등)를 받을 수 있으며, 구독 해지 시에는 아무런 정보를 받지 못한다.

옵저버 패턴 개념도
옵저버 패턴 개념도

 

 

2. 옵저버 패턴 어디서 사용 중일까?

옵저버 패턴(Observer Pattern)은 유니티에서 많이 사용하는 디자인 패턴이기 때문에 알아두면 상당히 유용하다. 이미 상당수 모듈이 해당 패턴으로 구현되어 있으며, 우리가 알게 모르게 잘 사용하고 있다.

 

흔한 사용 예로 유니티의 Button 이벤트를 활용할 때 옵저버 패턴이 사용된다.

유니티의 Button 클래스(주제)의 버튼 클릭 이벤트를 받기 위한 구성 클래스에서 Button.AddListener()를 통해 버튼 클릭에 대한 이벤트를 기다리고 버튼이 클릭되면 이벤트를 받아 처리하는 로직이 대표적인 유니티에서의 옵저버 패턴의 예이다.

버튼 하나에 대해, 다양한 곳에서 해당 버튼 클릭 이벤트를 등록하여 이벤트를 받았을 때 처리할 수 있다.

public class Observer1 : MonoBehaviour
{
    [SerializeField]
    private Button btnClick;

    private void Awake()
    {
        btnClick.onClick.AddListener(() => { Debug.Log("버튼 클릭"); });
    }
}

 

3. 유니티에서 옵저버 패턴 사용 예제

유니티에서 옵저버 패턴을 활용하는 예를 하나 만들어 보자.

자신의 자동차의 마일리지(주행거리)와 연료량을 볼 수 있는 Diplay 애플리케이션을 만든다고 가정해보자.

Display는 모바일, PC, Web 등 다양한 단말기에서 정보를 받을 수 있어야 한다.

위와 같은 경우 옵저버 패턴을 활용하면 어떻게 구현할 수 있을까?

 

우선 인터페이스부터 제작해보자.

주제 객체에 붙어야 하는 ISubject 인터페이스와 옵저버 객체에 붙어야 하는 IObserver 인터페이스를 만들 수 있을 것이다.

ISubject 인터페이스에서는 옵저버의 등록, 해제, 메시지 전달 기능을 함수로 구성한다.

아래 코드 및 주석 정보를 참고하자.

namespace ObserverPattern
{
    public interface ISubject
    {
        //옵저버 등록
        void ResisterObserver(IObserver observer);
        //옵저버 제거
        void RemoveObserver(IObserver observer);
        //옵저버들에게 내용 전달
        void NotifyObservers();
    }

    public interface IObserver
    {
        //주행거리, 연료량 정보 업데이트
        void UpdateData(float mileage, float amountFuel);
    }

}

 

자동차의 정보를 가지고 있는 CarData 클래스를 만들고, ISubject 인터페이스를 상속 받아보자.

상속받으면 ResisterObserver, RemoveObserver, NotifyObservers를 재 구현해야 하고, 각각 아래와 같이 구현해볼 수 있다.

(주석을 참고하면 좋다)

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

namespace ObserverPattern {
    public class CarData : MonoBehaviour, ISubject
    {
        private List<IObserver> list_Observers = new List<IObserver>();

        //마일리지 데이터.
        private float mileage;
        //연료량 데이터.
        private float fuelAmount;

        /// <summary>
        /// 옵저버 등록 함수.
        /// </summary>
        /// <param name="observer"></param>
        public void ResisterObserver(IObserver observer)
        {
            list_Observers.Add(observer);
        }

        /// <summary>
        /// 옵저버 해지 함수.
        /// </summary>
        /// <param name="observer"></param>
        public void RemoveObserver(IObserver observer)
        {
            list_Observers.Remove(observer);
        }

        /// <summary>
        /// 옵저버들에게 정보 전달 함수.
        /// </summary>
        public void NotifyObservers()
        {
            foreach(IObserver observer in list_Observers)
            {
                observer.UpdateData(mileage, fuelAmount);
            }
        }

        /// <summary>
        /// 자동차 소프트웨어로부터 업데이트된 정보를 받는 함수.
        /// </summary>
        /// <param name="newMileage">갱신된 마일리지 정보.</param>
        /// <param name="newFuelAmount">갱신된 연료량 정보.</param>
        public void UpdateData(float newMileage, float newFuelAmount)
        {
            mileage = newMileage;
            fuelAmount = newFuelAmount;
            NotifyObservers();
        }
    }
}

 

주제는 구성이 되었으니, 옵저버인 모바일용 Diplay 클래스를 만들어 보자.

Display 클래스는 IObserver 인터페이스를 상속받는다.

Display 클래스는 CarData 클래스에 자신을 옵저버로 등록하고, 자동차 정보 갱신을 기다린다.

코드로 구성하면 다음과 같다.


using UnityEngine;
using UnityEngine.UI;

namespace ObserverPattern
{
    public class MobileDisplay : MonoBehaviour, IObserver
    {
        [SerializeField] CarData data;
        [SerializeField] private Text txtMileage;
        [SerializeField] private Text txtAmountFuel;

        private void OnEnable()
        {
            //본인을 옵저버로 등록한다.
            data.ResisterObserver(this);
        }

        private void OnDisable()
        {
            //객체 삭제 시 옵저버를 해지한다.
            data.RemoveObserver(this);
        }

        /// <summary>
        /// 정보를 전달받으면 Diplay에 Update 해준다.
        /// </summary>
        /// <param name="mileage">갱신된 마일리지 정보</param>
        /// <param name="amountFuel">갱신된 연료량 정보</param>
        public void UpdateData(float mileage, float amountFuel)
        {
            txtMileage.text = mileage.ToString();
            txtAmountFuel.text = amountFuel.ToString();
        }
    }
}

 

다른 단말기도 MobileDisplay처럼 옵저버 등록을 한다면 동일하게 업데이트된 데이터를 받을 수 있게 된다.

이렇게 계속 확장해 나간다면, 다양한 단말에서 정보를 기다릴 수 있으며, 연결 관계도 느슨하게 연결되기 때문에 연결 해지도 쉽다.

 

 

4. 정리

유니티를 통한 옵저버 패턴의 사용 예를 살펴보았다.

옵저버 패턴의 장점은 일대다 구성을 정의할 때 효율이 좋은 점을 볼 수 있다. 또한 느슨한 연결이기 때문에 클래스간 결합도를 낮출 수 있다.

옵저버 패턴의 단점은 코드 간 연결을 파악할 때 코드 찾기가 어려울 수 있다.

 

옵저버 패턴은 이벤트 시스템에서 활용도가 높으며, 안드로이드에서 주로 사용하는 모델-뷰-컨트롤러(MVC)에서 옵저버 패턴이 잘 활용된다.

 

 

댓글