[Unity] Observer Pattern
Observer Pattern
관찰하는 대상이 있고, 관찰 중 이벤트 발생 시 역할을 수행하는 패턴이다. 청취자(Observer)들에게 신호를 보내는 라디오 송신탑(Subject)과 유사하다. 오브젝트의 상태가 변경되면 종속된 모든 오브젝트들에게 알림이 간다. 1:N 종속 관계를 사용해 오브젝트가 통신하되 낮은 결합도를 유지한다.
Subject는 Observer를 알지 못하거나, 신호를 수신한 Observer가 무엇을 하든 관여하지 않는다. 또한 Observer는 Subject에 대해 종속 관계를 갖고있긴 하지만, 서로에 대해서는 모른다. 그래서 확장과 유지보수에 용이하다. 이는 주로 이벤트 핸들링이나 MVC 패턴에서 활용된다.
Subject 추제가 존재하고, Observer들은 Subject를 주시하고 있다. 이 말인 즉슨 콜백을 등록했다는 의미이다. 그리고 주체가 액션을 취하게 되면 콜백을 등록해놨던 Observer1, Observer2가 호출이 된다. 위의 그림은 기본 방식이고, 구독자를 등록하여 설계를 바꿀 수도 있다.
Delegate
델리게이트는 대표적인 Observer Pattern의 예시이다. 델리게이는 대리자라는 의미를 지니고 있으며, 어떤 함수를 대신해주는 의미이다. 함수를 담는 변수로 생각하고 이를 호출해주면 함수가 호출된다.
public class Delegate Script : Monobehaviour {
delegate void MyDelegate(int num);
MyDelegate myDelegate;
void Start() {
myDelegate = PrintNum; // PrintNum 등록
myDelegate(50); // PrintNum 호출
myDelegate = DoubleNum; // DoubleNum 등록
myDelegate(50); // DoubleNum 호출
}
void PrintNum(int num) {
print("Print Num : " + num);
}
void DoubleNum(int num) {
print("Double Num : " + num * 2);
}
}
위의 예시가 간단한 델리게이트의 예시이다. 간혹 멀티캐스트를 사용하는 경우가 있는데, 아래의 예시를 살펴보자.
public class multicastScript : MonoBehaviour {
delegate void MultiDelegate();
MultiDelegate myMultiDelegate;
void Start() {
myMultiDelegate += PowerUp;
myMultiDelegate += TurnRed;
if (myMultiDelegate != null)
myMultiDelegate(); // PowerUp, TurnRed가 동시에 호출
// myMultiDelegate?.Invoke();
}
void TurnRed() {
GetComponent<Renderer>().material.color = Color.red;
}
void PowerUp() {
print("Orb is powering up!);
}
}
하나의 델리게이트만 저장되는 것이 아닌, += 연산자를 통해 함수를 추가해주는 것이다.
델리게이트는 자신의 클래스 내에서 호출하고 저장하는 것이 아닌, 다른 클래스끼리 연결해주는 것이 용이하다.
Event / Action
델리게이트는 많이 사용되다 보니까 델리게이트의 기능에서 부가적으로 키워드가 추가제공된다. 그것이 event와 Action이다. 이는 유니티의 기능이 아닌 C#에서 제공하는 기능이다.
Action은 반환값이 없는 메소드(void, 즉 액션만 취한다는 의미)를 참조하는 델리게이트이며, 0개 이상의 매개 변수를 가질 수 있다. event는 델리게이트를 기반으로 하여 이벤트를 정의한다. event 키워드는 클래스 외부에서 호출하지 못하도록 보호하고, 이벤트 구독 및 구독 해지에 대한 제어를 제공해준다.
using System;
public class EventManager : MonoBehaviour {
public static event Action OnClicked;
void OnGUI() {
if(GUI.Button(new Rect(Screen.width / 2 - 50, 5, 100, 30), "Click")) {
if (OnClicked !=null) OnClicked(); // 등록된 이벤트 호출
}
}
}
구독을 위한 EventManager를 구축한다. 이는 아래의 코드에서 사용된다.
public class TeleportScript : MonoBehaviour {
void OnEnable() { // 이벤트 등록
EventManager.OnClicked += Teleport;
EventManager.OnClicked += PrintLogMessage;
}
void OnDisable() { // 이벤트 해제
EventManager.OnClicked -= Teleport;
EventManager.OnClicked -= PrintLogMessage;
}
void Teleport() {
Vector3 pos = transform.position;
pos.y = Random.Range(1.0f, 3.0f);
transform.position = pos;
}
void PrintLogMessage() {
Debug.Log("Teleport!");
}
}
앞서 구축한 EventManager를 통해 함수를 등록한다. OnClicked가 실행됐을 때, 구독된 함수들이 호출된다.
코드 예시 및 특징
처음에 설명했던 그림과 같은 예시의 코드를 확인해보면, Action과 event 부분에서 설명했던 코드와 크게 다르지 않다.
public class Observer : MonoBehaviour {
[SerializeField] private Subject subjectToObserve;
private void OnThingHappend() {
Debug.Log("Observer responds");
}
private void Awake() {
if(subjectToObserve != null) subjectToObserve.ThinsHappened += OnThingHappened;
}
private void OnDestroy() {
if(subjectToObserve != null) subjectToObserve.ThingHappend -= OnThingHappened;
}
}
// ---
using System;
public class Subject : Monobehaviour {
public event Action ThingHappened;
public void DoThing() {
ThingHappened?.Invoke();
}
}
ThingHappened에 함수를 등록하고, DoThing 함수가 호출되면 해당 함수를 Invoke를 하여 함수를 호출해주면 된다.
이렇게 되면 기존 코드를 따로 건들이지 않고 확장을 할 수 있다.
- 장점
- 느슨한 결합으로 Subject와 Observer간의 결합도를 낮출 수 있음
- Subject와 Observer를 재사용에 용이
- 주체 클래스나 기존 Observer를 수정할 필요 없이 새로운 Observer를 추가하여 확장성에 용이
- Subject와 Observer가 각자 자신의 역할에 집중 : Subject는 상태 변화를 관리하고, Observer는 그 변화를 처리 → 단일 책임 원식 준수
- UI 이벤트(MVP, MVC), 네트워크 이벤트, 데이터 변경 이벤트 처리 등 이벤트 기반 시스템 구현 용이
- 런타임에 Observer를 추가/제거 용이 → 실시간 업데이트
- C#및 유니티에서 손쉽게 구현 가능
- 단점
- 다수의 Observer가 존재시 시스템 복잡성 증가
- Subject가 이벤트 발생 시킬 떄 Observer간 호출 순서 보장X
- Observer가 Subject의 이벤트 구독 후 해지하지 않으면 메모리 누수 발생
- Observer의 수가 많거나 각 Observer가 처리해야 할 로직이 복잡해지면 성능 저하
- Subject와 Observer간 순환 참조 발생 가능 → 설계에서 신경써야 함
- Subject와 Observer 간의 강한 결합 발생 가능, 남용하면 시스템 모듈화가 어려워짐
- 이벤트가 객체에 의해 처리되므로 디버깅과 로그 추적이 힘들어짐
UnityEvent / UnityAction
Unity용 Event와 Action을 제공해주고 있다.
- UnityAction : System.Action의 Unity버전이며, 델리게이트 타입. 인스펙터에서는 설정이 불가능하고 코드에서만 설정이 가능 (ex> UnityEvent.AddListener(Events.UnityAction call) → Unity API에서 호출하기 위한 용도
- UnityEvent : 이벤트를 관리하기 위해 Unity에서 제공하는 클래스이고, Unity의 인스펙터를 지원. 쉽게 설정하고 연결할 수 있는 이벤트 시스템이므로 비프로그래머에게 유용하나, 쉽게 사용할 수 있다보니 사용처 제한됨