반응형
SMALL
옵저버 패턴이란?
객체간의 일대다 의존관계를 정의하는 패턴
- 옵저버 패턴의 구성요소
- Subject : 주체객체 또는 옵저버들을 등록하고 상태 변화를 알리는 메서드를 제공한다.
- Observer : 상태변화를 감지하고 처리하기 위한 메서드를 정의하는 인터페이스 또는 추상클래스.
- ConcreteSubject: Subject를 구체화한 클래스로, 상태 변화가 발생하면 등록된 옵저버들에게 알림을 전달.
- ConcreteObserver: Observer를 구체화한 클래스로, Subject에 등록되어 상태 변화를 감지하고 처리.
- 옵저버 패턴의 장점
- 느슨한 결합 : Subject와 Observer 간에는 인터페이스를 통해만 의존관계를 형성하므로 각 객체간의 결합도가 낮아진다.
- 확장성: 새로운 Observer를 추가하거나 기존 Observer를 제거하는 등의 확장이 용이. Subject는 새로운 Observer를 등록하기만 하면 되기 때문에 수정이 필요하지 않습니다.
- 이벤트 기반 시스템: 이벤트 기반 시스템을 구현할 수 있어, 한 객체의 상태 변화에 대한 처리를 다양한 객체들에게 분산시키는 것이 가능
- 옵저버 패턴의 단점
- 메모리 누수: Subject와 Observer 간의 순환 참조가 발생할 수 있어, 객체가 올바르게 해제되지 않으면 메모리 누수가 발생할 수 있음. 이를 방지하기 위해 약한 참조(Weak Reference)를 사용하거나 주의하여 구현해야 합니다.
여기서 Weak Reference 란?
Weak Reference(약한 참조)는 객체를 참조하지만 해당 객체의 수명에 영향을 주지 않는 참조 유형.
일반적으로 약한 참조는 가비지 컬렉터에 의해 수거될 수 있는 객체에 대한 참조를 유지하는 데 사용된다.
Weak Reference 예시 코드
public class ImageCache
{
private Dictionary<string, WeakReference<Texture2D>> cache;
public ImageCache()
{
cache = new Dictionary<string, WeakReference<Texture2D>>();
}
public Texture2D GetImage(string url)
{
if (cache.ContainsKey(url))
{
if (cache[url].TryGetTarget(out Texture2D image))
{
// 이미지가 캐시에 있고 약한 참조로 접근 가능한 경우 반환
return image;
}
else
{
// 이미지가 가비지 컬렉터에 의해 수거된 경우 캐시에서 제거
cache.Remove(url);
}
}
// 이미지가 캐시에 없거나 약한 참조로 접근할 수 없는 경우 로드 후 캐시에 추가
Texture2D newImage = LoadImageFromUrl(url);
cache[url] = new WeakReference<Texture2D>(newImage);
return newImage;
}
private Texture2D LoadImageFromUrl(string url)
{
// 이미지를 로드하는 로직
}
}
- 성능 저하: Subject의 상태가 변경될 때마다 모든 Observer에게 알림을 전달하므로, Observer의 수가 많을 경우 성능 저하가 발생할수 있음.
- 사용 예시
- 이벤트 핸들링: 이벤트 기반 시스템에서 Subject의 상태 변화를 이벤트로 처리하고, 등록된 Observer들에게 알림을 전달가능
- MVC 아키텍처: 모델(Model)과 뷰(View) 사이의 상호 작용에 사용되며, 모델의 상태 변화에 따라 뷰가 업데이트될 수 있게 하는 것이 가능
실제 프로젝트 사용 예시 (객체간의 통신을 위한 EventDispatcher Class)
* 정확히 얘기하자면 옵저빙 패턴은 아니지만 유사하게 이벤트를 등록하고 멀리떨어진 UI 객체들간의 통신 혹은 게임 오브젝트들간의 통신을 위해 이벤트를 등록하고 사용하는 용도로 클래스를 작성하였다.
*클래스는 enum명을 사용하여 팀원들간의 협업에서 중복되는 키가 발생하지 않도록 하였으며, 우리 프로젝트에서는 Init() 메서드를 사용, 객체들의 활성화 주기를 관리하였기에 객체의 Init() 단계에서 이벤트를 등록하도록 하여 이후 필요시 사용하도록 유도하였다.
using System;
using System.Collections.Generic;
public class EventDispatcher
{
/// <summary>
/// 메서드 사용키 (EventDispatcher enum 상수 등록후 사용 * 중복 메서드 등록 방지))
/// </summary>
public enum EventName
{
UINPCPopupUpdate,
UINPCPopupActive,
MainCameraControllerHitEffects,
SanctuarySceneMainIntotheDungeon,
DungeonMainToNextStage,
DungeonMainPlayerToNextRoom,
DungeonMainPlayerToSanctuary,
DungeonSceneMainPlayerExpUp,
UIInventoryAddCell,
UIInventoryAddEquipment,
UICurrentInventoryList,
UIGameOverPopUp,
UIDungeonDirectorUISetOff,
UIPortalArrowControllerInitializingArrows,
UIPortalArrowControllerStopAllArrowCorutines,
UIDialogPanelRandomWeaponDialog,
UIDialogPanelStartDialog,
UIShopGoodsCurrentGold,
ChestItemGeneratorMakeChest,
ChestItemGeneratorMakeItemForChest,
ChestItemGeneratorMakeItemForInventory,
ChestItemGeneratorMakeFieldCoin,
DungeonSceneMainTakeFood,
DungeonSceneMainTakeGun,
PlayerShellTakeRelic,
DungeonSceneMainTakeChestDamage,
UIRelicDirectorTakeRelic,
UICurrencyDirectorUpdateGoldUI,
UICurrencyDirectorUpdateEtherUI,
UIJoystickDirectorStopJoyStick,
UIJoystickDirectorActiveJoyStick,
UIAnnounceDirectorStartAnnounce,
UIDungeonLoadingDirectorStageLoading,
UIDungeonLoadingDirectorSanctuarytLoading,
LaserLineInactivateLaser,
LaserLineActivateLaser,
UIInventoryDirectorMakeFieldFullText,
UIInventoryDirectorMakeFullPopupText,
UIInventoryDirectorMakeFieldFullHealthText,
UIInventoryDirectorMakeHealthPopupText,
UIInventoryDirectorButtonScaleAnim,
UIFieldItemPopupDirectorUpdatePopup,
UIFieldItemPopupDirectorClosePopup,
PlayerDie,
Test,//테스트용 임시로 테스트 하시고 적용한 addlistner 코드 지워주세요.
}
private static EventDispatcher instance;
private Dictionary<EventName, Delegate> listeners = new Dictionary<EventName, Delegate>();
public static EventDispatcher Instance
{
get
{
if (instance == null)
{
instance = new EventDispatcher();
}
return instance;
}
}
/// <summary>
/// 반환값이 없는 메서드 혹은 인스턴스 메서드 등록시
/// </summary>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener(EventName eventName, Action listener)
{
if (listeners.ContainsKey(eventName))
{
UnityEngine.Debug.LogWarning("<color=white>이미 해당 키로 메서드가 등록된 상태입니다. 기존 메서드를 덮어 씌웠습니다.</color>");
listeners.Remove(eventName);
listeners.Add(eventName, listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 인자 1개를 받으며 반환값이 없는 메서드 혹은 인스턴스 메서드 등록시
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener<T>(EventName eventName, Action<T> listener)
{
if (listeners.ContainsKey(eventName))
{
UnityEngine.Debug.LogWarning("<color=white>이미 해당 키로 메서드가 등록된 상태입니다. 기존 메서드를 덮어 씌웠습니다.</color>");
listeners.Remove(eventName);
listeners.Add(eventName, listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 인자 2개를 받으며 반환값이 없는 메서드 혹은 인스턴스 메서드 등록시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener<T1, T2>(EventName eventName, Action<T1, T2> listener)
{
if (listeners.ContainsKey(eventName))
{
UnityEngine.Debug.LogWarning("<color=white>이미 해당 키로 메서드가 등록된 상태입니다. 기존 메서드를 덮어 씌웠습니다.</color>");
listeners.Remove(eventName);
listeners.Add(eventName, listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 인자 3개를 받으며 반환값이 없는 메서드 혹은 인스턴스 메서드 등록시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <typeparam name="T3"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener<T1, T2, T3>(EventName eventName, Action<T1, T2, T3> listener)
{
if (listeners.ContainsKey(eventName))
{
UnityEngine.Debug.LogWarning("<color=white>이미 해당 키로 메서드가 등록된 상태입니다. 기존 메서드를 덮어 씌웠습니다.</color>");
listeners.Remove(eventName);
listeners.Add(eventName, listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 반환 값이 있는 메서드 등록시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener<TResult>(EventName eventName, Func<TResult> listener)
{
if (listeners.ContainsKey(eventName))
{
UnityEngine.Debug.LogWarning("<color=white>이미 해당 키로 메서드가 등록된 상태입니다. 기존 메서드를 덮어 씌웠습니다.</color>");
listeners.Remove(eventName);
listeners.Add(eventName, listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 인자 1개를 받으며 반환 값이 있는 메서드 등록시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener<T1, TResult>(EventName eventName, Func<T1, TResult> listener)
{
if (listeners.ContainsKey(eventName))
{
UnityEngine.Debug.LogWarning("<color=white>이미 해당 키로 메서드가 등록된 상태입니다. 기존 메서드를 덮어 씌웠습니다.</color>");
listeners.Remove(eventName);
listeners.Add(eventName, listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 인자 2개를 받으며 반환 값이 있는 메서드 등록시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener<T1, T2, TResult>(EventName eventName, Func<T1, T2, TResult> listener)
{
if (listeners.ContainsKey(eventName))
{
UnityEngine.Debug.LogWarning("<color=white>이미 해당 키로 메서드가 등록된 상태입니다. 기존 메서드를 덮어 씌웠습니다.</color>");
listeners.Remove(eventName);
listeners.Add(eventName, listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 반환값이 없는 메서드 삭제시
/// </summary>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void RemoveListener(EventName eventName, Action listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Remove(listeners[eventName], listener);
if (listeners[eventName] == null)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 인자 1개를 받으며 반환값이 없는 메서드 삭제시
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void RemoveListener<T>(EventName eventName, Action<T> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Remove(listeners[eventName], listener);
if (listeners[eventName] == null)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 인자 2개를 받으며 반환값이 없는 메서드 삭제시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void RemoveListener<T1, T2>(EventName eventName, Action<T1, T2> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Remove(listeners[eventName], listener);
if (listeners[eventName] == null)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 반환 값이 있는 메서드 제거시
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void RemoveListener<TResult>(EventName eventName, Func<TResult> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Remove(listeners[eventName], listener);
if (listeners[eventName] == null)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 인자 1개를 받으며 반환 값이 있는 메서드 제거시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void RemoveListener<T1, TResult>(EventName eventName, Func<T1, TResult> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Remove(listeners[eventName], listener);
if (listeners[eventName] == null)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 인자 2개를 받으며 반환 값이 있는 메서드 제거시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void RemoveListener<T1, T2, TResult>(EventName eventName, Func<T1, T2, TResult> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Remove(listeners[eventName], listener);
if (listeners[eventName] == null)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 반환값이 없는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <param name="eventName"></param>
public void Dispatch(EventName eventName)
{
if (listeners.ContainsKey(eventName))
{
((Action)listeners[eventName])();
}
}
/// <summary>
/// 반환값이 없는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <param name="eventName"></param>
/// <param name="eventParams"></param>
/// <param name="eventParams2"></param>
public void Dispatch<T>(EventName eventName, T eventParams = default)
{
if (listeners.ContainsKey(eventName))
{
((Action<T>)listeners[eventName])(eventParams);
}
}
/// <summary>
/// 반환값이 없는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <param name="eventName"></param>
/// <param name="eventParams"></param>
/// <param name="eventParams2"></param>
public void Dispatch<T1, T2>(EventName eventName, T1 eventParams = default, T2 eventParams2 = default)
{
if (listeners.ContainsKey(eventName))
{
((Action<T1, T2>)listeners[eventName])(eventParams, eventParams2);
}
}
/// <summary>
/// 반환값이 없는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <param name="eventName"></param>
/// <param name="eventParams"></param>
/// <param name="eventParams2"></param>
public void Dispatch<T1, T2, T3>(EventName eventName, T1 eventParams = default, T2 eventParams2 = default, T3 eventParams3 = default)
{
if (listeners.ContainsKey(eventName))
{
((Action<T1, T2, T3>)listeners[eventName])(eventParams, eventParams2, eventParams3);
}
}
/// <summary>
/// 반환 값이 있는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <typeparam name="TResult"> 반환 타입</typeparam>
/// <param name="eventName">이벤트 등록 키</param>
/// <param name="result">반환값</param>
/// <exception cref="InvalidCastException"></exception>
public void Dispatch<TResult>(EventName eventName, out TResult result)
{
if (listeners.ContainsKey(eventName))
{
var listener = listeners[eventName];
if (listener is Func<TResult> func)
{
result = func();
}
else
{
throw new InvalidCastException($"Listener for event '{eventName}' is not a Func with the correct signature.");
}
}
else
{
result = default;
}
}
/// <summary>
/// 인자를 1개 받고반환 값이 있는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <typeparam name="TResult"> 반환 타입</typeparam>
/// <param name="eventName">이벤트 등록 키</param>
/// <param name="eventParams">매개변수</param>
/// <param name="result">반환값</param>
/// <exception cref="InvalidCastException"></exception>
public void Dispatch<T, TResult>(EventName eventName, T eventParams, out TResult result)
{
if (listeners.ContainsKey(eventName))
{
var listener = listeners[eventName];
if (listener is Func<T, TResult> func)
{
result = func(eventParams);
}
else
{
throw new InvalidCastException($"Listener for event '{eventName}' is not a Func with the correct signature.");
}
}
else
{
result = default;
}
}
/// <summary>
/// 인자를 2개 받고반환 값이 있는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <typeparam name="TResult"> 반환 타입</typeparam>
/// <param name="eventName">이벤트 등록 키</param>
/// <param name="eventParams">첫번째 인자</param>
/// <param name="eventParams2">두번째 인자</param>
/// <param name="result">반환값</param>
/// <exception cref="InvalidCastException"></exception>
public void Dispatch<T1, T2, TResult>(EventName eventName, T1 eventParams, T2 eventParams2, out TResult result)
{
if (listeners.ContainsKey(eventName))
{
var listener = listeners[eventName];
if (listener is Func<T1, T2, TResult> func)
{
result = func(eventParams, eventParams2);
}
else
{
throw new InvalidCastException($"Listener for event '{eventName}' is not a Func with the correct signature.");
}
}
else
{
result = default;
}
}
}
반응형
LIST