본문 바로가기
Studies/게임 디자인 패턴

[게임 디자인 패턴] 옵저버 패턴

by NCTP 2024. 9. 24.

// 유니티로 배우는 게임 디자인 패턴 제 2판 (데이비드 바론, 구진수) 을 기준으로 작성되었습니다.

 

개요

  옵저버 패턴(Observer Pattern)은 객체의 상태 변화에 따라 다른 객체들에게 자동으로 통지할 수 있는 디자인 패턴이다. 이는 발행자-구독자 패턴(Publisher-Subscriber Pattern)이라고도 불리는데, 객체 간의 느슨한 결합을 구현하는 데에 유용하다. 

 

  옵저버 패턴은 주로 GUI, 이벤트 시스템, 또는 데이터의 상태 변화가 여러 객체에게 알려져야 하는 경우에 주로 사용된다. 

 

구성 요소

  • 주체 (Subject) : 상태 변화를 통지해야 하는 객체. 상태가 변경되면 옵저버에게 알리기 위해 옵저버를 등록하고 관리한다.
  • 옵저버 (Observer) : 주체의 상태 변화를 감지하여 반응하는 객체. 주체에서 통지를 받으면 이를 처리하는 메서드를 구현한다.

  옵저버 패턴은 한 객체가 주체 역할을 맡고, 다른 객체들이 관찰자 역할로써 주체를 감시하는 디자인 패턴이다. 하나의 주체와 여러 관찰자들이 일대다 관계를 설정하는 것이 옵저버 패턴의 핵심 목적이다. 주체는 내부의 변경을 옵저버들에게 수시로 알려야 하며, 주체와 관찰자는 아주 가볍게 연결되어있으며 서로의 존재를 알고 있다.

 

  주체는 옵저버를 등록하거나 제거하는 기능을 제공해야 하고, 옵저버 객체는 주체 객체의 상태가 변경될 때 호출되는 메서드가 구현되어 있어야 한다. 이 과정에서 주체의 상태가 변하면 옵저버들에게 자동으로 알림이 간다.

 

 

장점과 단점

장점

  • 느슨한 결합 : 주체와 옵저버는 독립적으로 존재할 수 있으며, 서로를 구체적으로 알 필요가 없다. 그저 서로의 존재만 확인한다.
  • 유연성 : 주체에 여러 옵저버를 추가하거나 제거가 가능하고, 런타임에 동적으로 제거 또한 가능하다. 상태 변화에 반응하는 방식도 다양하다.
  • 일대다 시스템 : 하나의 객체와 여러 객체가 서로 반응하는 문제를 우아하게 해결한다.

단점

  • 성능 문제 : 옵저버가 많아짐에 따라 통지에 걸리는 시간이 오래 걸린다.
  • 복잡성과 무질서 : 너무 많은 옵저버와 주체가 상호작용할 경우 관리가 복잡해진다. 둘 이상의 옵저버가 종속성을 공유하고 특정 순서에 따라 움직일 경우, 옵저버 패턴이 상당히 방해된다. 
  • 메모리 누수 : 주체는 옵저버에 대한 강한 참조를 가진다. 이 과정에서 메모리 누수가 발생할 수 있다. 옵저버의 제거 및 등록을 엄격하게 관리해야 하며, 옵저버 패턴이 필요하지 않은 경우 가비지 컬렉션에서 문제가 생길 가능성이 있다.

 

사용 예시

  • 이벤트 시스템 : GUI 시스템에서 버튼 클릭, 마우스 이동 등의 이벤트가 발생하면 옵저버(이벤트 리스너)에게 통지하여 관련 동작을 실행한다
  • 모델-뷰-컨트롤러 패턴 (MVC Pattern) : 모델이 업데이트되면 뷰에 통지하여 화면을 갱신하는 방식에서 옵저버 패턴을 사용한다.

 

 

구현 전 간단 요약

  구현에 앞서, 아주 간단하게 요약해보면 이렇다.

  • 옵저버는 서브젝트를 관찰한다. 서브젝트는 자신을 관찰하는 옵저버들의 리스트를 지니고 있다.
  • 서브젝트에 변화가 생기는 부분에서, 옵저버들의 Notify 메서드를 콜 한다.
  • Notify 메서드가 호출된 옵저버들은 각각 호출되었을 때의 동작을 수행한다.

  이 기능들에 중점을 두고, 어떠한 기능을 직접 개발해보자.

 

 


 

구현하기

  옵저버 패턴을 이용하여 여러가지 전구가 하나의 버튼에게 반응하는 기능을 만들어보자. 버튼을 누르면 전구가 켜지고, 다시 누르면 전구가 꺼지는 간단한 기능이다.

 

Subject.cs

  관찰이 되는 대상인 Subject 클래스를 작성한다. 서브젝트는 옵저버를 리스트에 추가하거나 빼는 메서드를 포함하고 있고, 리스트 안에 있는 모든 옵저버들에게 Notify를 보내는 메서드를 필수적으로 포함한다.

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

public abstract class Subject : MonoBehaviour
{
    private readonly ArrayList _observers = new ArrayList();

    // 옵저버를 리스트에 추가
    public void Attach(Observer observer)
    {
        _observers.Add(observer);
    }

    // 옵저버를 리스트에서 제거
    public void Detach(Observer observer)
    {
        _observers.Remove(observer);
    }
    // 서브젝트의 변화를 옵저버들에게 통지하는 메서드
    public void NotifyObservers()
    {
        foreach(Observer observer in _observers)
        {
            observer.Notify(this);
        }
    }
}

 

 Observer.cs

  관찰자는 추상 클래스로 구현한다. 관찰자로써 정의될 클래스들은 앞으로 Observer 클래스를 상속받아 관측자로 거듭난다.

  

  Notify는 Subject로부터 호출되고, 상세한 기능들은 각각의 스크립트에서 override하여 작성한다.

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

// 서브젝트를 관찰하는 옵저버 클래스
// 추상 클래스로 구현
public abstract class Observer : MonoBehaviour
{
    // 서브젝트의 변화를 알리는 메서드
    // 서브젝트에서 직접 콜하는 메서드임.
    public abstract void Notify(Subject subject);

}

 

 

LightController.cs

  빛들을 컨트롤 하는 중앙 전원의 역할을 하는 LightController 스크립트를 작성한다.

 

  LightController는 Subject를 상속받고, Light는 Observer를 상속받는다. 이를 통해 Light 오브젝트들은 LightController를 관찰하는 관계를 맺는다.

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

public class LightController : Subject
{
    private Light[] _lights; // 전구들을 저장할 리스트

    // 씬에 존재하는 모든 전구를 찾아 _lights 리스트에 저장하는 메서드
    void FindLightsInScene()
    {
        _lights = FindObjectsOfType<Light>(); // 씬 위의 모든 전구를 찾아 리스트에 저장.
        //AttachAllLights();
    }

    void AttachAllLights()
    {
        foreach(Light light in _lights)
        {
            Attach(light);
        }
    }
    void DetachAllLights()
    {
        foreach(Light light in _lights)
        {
            Detach(light);
        }
    }
    void OnEnable()
    {
        AttachAllLights();
    }
    void OnDisable()
    {
        DetachAllLights();
    }
    // Start is called before the first frame update
    void Awake()
    {
        FindLightsInScene();
    }

    void OnGUI()
    {
        if(GUILayout.Button("Notify!"))
        {
            NotifyObservers();
        }
    }
}

 

  스크립트에서는 씬 상의 모든 Light들을 찾아 리스트에 저장하고, 이들을 모두 옵저버 리스트에 추가한다. OnEnable() 메서드를 통해서  활성화됨과 동시에 모든 Light를 옵저버로써 지정한다.

 

 

Light.cs

  빛들을 컨트롤하는 LightController를 관찰하는 관찰자들이다. LightController에서 신호를 보냈을 때 불이 꺼져있으면 불을 키고, 불이 켜져있으면 불을 끈다.

 

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


public class Light : Observer
{
    public Color lightColor = Color.red;
    private SpriteRenderer _spriteRenderer;
    private Color _originalColor;
    private bool _isLightOn = false;
    
    public override void Notify(Subject subject)
    {
        Debug.Log(this.name + " is Notified!");
        if(_spriteRenderer != null)
        {
            if(!_isLightOn)
            {
                _spriteRenderer.color = lightColor;
                _isLightOn = true;
            }
            else
            {
                _spriteRenderer.color = _originalColor;
                _isLightOn = false;
            }
        }
    }

    
    // Start is called before the first frame update
    void Start()
    {
        _spriteRenderer = GetComponent<SpriteRenderer>();
        _originalColor = _spriteRenderer.color;
    }

}

 

 

구현 결과 확인하기

왼쪽 상단의 Notify 버튼을 누르면 동작한다.

 

  • 여러 게임 오브젝트가 복합적으로 동작하는 게임 씬에서 사용될 패턴보다는, 특정 객체를 감시하고 상태변화를 스크린에 띄워줄 GUI들을 옵저버로 사용하면 유용한 디자인 패턴인 것 같다!

댓글