참고용 예제 저장소: Unity Design Patterns (Sungkuk Park)
최종 업데이트: 2019년 1월 3일

옵저버 (Observer) 패턴

의도 (Intention)

객체 사이에 일 대 다의 의존 관계를 정의해 단일 객체의 상태가 변할 때 해당 객체에 의존하는 다른 객체들이 이를 통지받고 자동으로 갱신될 수 있게 한다.

다른 이름 (Also Known As)

종속자(Dependent), 게시-구독(Publish-Subscribe)

동기 (Motivation)

시스템을 여러 개의 협력하는 클래스들로 분할하는 것에 따르는 흔한 부수효과(side-effect)로는 객체 간의 일관성(consistency)을 유지할 필요가 생기는 것이다. 하지만 객체들의 결합도를 높이는 방식으로 일관성을 유지하는 것은 권장되지 않는다. 왜냐하면 이는 객체들의 재사용(reusability)을 감소시키기 때문이다.

예컨대, 많은 사용자 인터페이스 도구들은 사용자 인터페이스의 표현 부분을 그 내부 어플리케이션의 데이터와 분리한다. 즉, 표현부와 데이터부는 상호협력하면서 독립적으로도 사용될 수 있는 것이다. 말하자면, 스프레드시트(spreadsheet) 객체와 바(bar) 객체는 같은 어플리케이션 객체를 다르게 표현할 수 있다. 스프레드시트와 바 차트는 서로에 대해 전혀 모르기 때문에, 사용자는 필요한 객체만을 선별적으로 재사용할 수 있다. 사용자가 스프레드시트 상에서 정보를 변경하면, 바 차트는 즉시 해당 변경을 반영하며 그 역도 마찬가지이다.

이 동작은 스프레드시트와 바 차트가 데이터 객체에 종속적이며, 이때문에 해당 객체의 상태 변경에 대해 통지받는다는 것을 의미한다. 예시로 든 것처럼 종속적인 객체들의 수를 2개로 한정할 필요는 없다. 같은 데이터에 대한 사용자 인터페이스의 개수는 얼마든지 많아질 수 있다.

옵저버 패턴은 이러한 관계를 어떻게 수립할지에 대해 기술한다. 옵저버 패턴에서의 핵심 객체는 주체(subject)감시자(observer)이다. 단일한 주체는 여러 개의 감시자들을 가질 수 있다. 모든 감시자는 주체가 상태의 변화가 있을 때마다 매번 이를 통지받는다. 그 반응(response)으로서, 각 감시자는 자신의 상태를 주체의 상태와 동기화시키기 위해 주체의 상태를 알아볼 것이다.

이러한 관계를 게시-구독(Publish-Subscribe) 관계라고도 한다. 주체는 통지(notifications)의 게시자가 된다. 주체는 구독자가 누구인지 전혀 모르고도 통지를 발송한다. 구독하는 감시자의 수는 얼마든지 많아질 수 있다.

구조 (Structure)

Observer

구현 (Implementation)

만약 아래와 같이, 주체(subject)에 해당하는 큐브(cube)가 활성화되거나 비활성화되면 나머지 감시자(observer)에 해당하는 큐브들이 모두 동시에 해당 큐브의 상태에 따라 활성화되거나 비활성화되어야 한다고 가정하자. 이는 마치 중앙에 있는 전구를 켜면 나머지 전구들이 동시에 따라 켜지는 것에 비유할 수 있다. 현재는 옵저버 패턴이 구현되어 있지 않기 때문에 중앙에 있는 큐브를 켜도 나머지 큐브들의 상태는 변하지 않는다.

SubjectOn

주체 (Subject) 객체 구현

using System.Collections.Generic;
using UnityEngine;

public interface ISubject
{
    void RegisterObserver(IObsersver o);
    void UnregisterObserver(IObsersver o);
    void Notify();
}

public class SubjectCube : Cube, ISubject
{   
    private List<IObsersver> _obsersvers = new List<IObsersver>();
    
    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(ray, out var hit))
            {
                if (hit.transform.name == "SubjectCube")
                {
                    InvertCubeState();

                    Notify();
                }
            }
        }
    }

    public void RegisterObserver(IObsersver o)
    {
        _obsersvers.Add(o);
    }

    public void UnregisterObserver(IObsersver o)
    {
        _obsersvers.Remove(o);
    }

    public void Notify()
    {
        foreach (var o in _obsersvers)
        {
            o.UpdateSelf();
        }
    }
}

감시자 (Observer) 객체 구현

using UnityEngine;

public interface IObsersver
{
    void UpdateSelf();
}

public class ObserverCube : Cube, IObsersver
{
    private static SubjectCube _cachedSubject;

    protected override void Init()
    {
        base.Init();
        
        if (_cachedSubject == null)
        {
            _cachedSubject = GameObject.FindObjectOfType<SubjectCube>();            
        }
        Debug.Assert(_cachedSubject != null);
        
        _cachedSubject.RegisterObserver(this);
    }

    public void UpdateSelf()
    {
        Debug.Assert(_cachedSubject != null);

        SetCubeState(_cachedSubject.State);
    }
}

큐브 객체 구현

using UnityEngine;

public enum CubeState
{
    Off,
    On,
}

public class Cube : MonoBehaviour
{
    public CubeState State
    {
        get
        {
            return _state;
        }
        set
        {
            _state = value;
        }
    }
    [SerializeField]
    private CubeState _state;
    
    private Renderer _renderer;

    private void Awake()
    {
        Init();
    }

    protected virtual void Init()
    {
        if (_renderer == null)
        {
            _renderer = GetComponent<Renderer>();
        }
        
        SetCubeState(CubeState.Off);        
    }

    protected void InvertCubeState()
    {
        var nextState = (_state == CubeState.Off) ? CubeState.On : CubeState.Off;
        SetCubeState(nextState);
    }
    
    protected void SetCubeState(CubeState state)
    {
        _renderer.material.color = (state == CubeState.Off) ? Color.white : Color.yellow;
        _state = state;
    }
}

구현 결과, 다음과 같이 주체에 해당하는 큐브가 활성화되면 감시자에 해당하는 큐브들이 동시에 활성화되는 것을 볼 수 있다.

SubjectAndObserversOn

<끝>