반응형

Observer Pattern과 Publish/Subscribe

옵저버 패턴 Obserever Pattern은 객체의 상태가 바뀌면 (그 객체에 등록되어 있던) 옵서버들이 객체로부터 알림을 받게 되는 패턴입니다. 유튜브 채널(객체)에 새로운 영상이 올라오면(객체의 상태가 바뀌면) 채널 구독자들(옵서버들)이 알람을 받는 패턴으로 비유할 수 있습니다. 게임에서 예시를 들어보겠습니다. 실제로

  • 좀비(객체)가 플레이어가 쏜 총알을 맞아 사망하게(객체의 상태가 바뀌면) 되었습니다.
  • 좀비 사망 애니메이션이 실행되고(옵저버1), 플레이어의 점수가 증가하며(옵서버 2), 전리품이 드롭되는(옵서버 3) 메서드들이 실행되어야 할 것입니다.

이 패턴은 매우 유명하고 유니티에서도 자체적으로 제공되는 옵저버 패턴도 있습니다. 그래서 이 패턴을 들어본 적이 없다고 하더라도 유니티로 스크립트를 작성해보았다면 사실 이 패턴을 적용한 적이 많을 것입니다. 옵저버 패턴은 기본적으로 클래스 사이에 디커플링을 하게 해줍니다. 그러나 옵저버들이 실행해야 할 이벤트들이 작성된 스크립트를 참조해야 하는 형태라면 완전히 디커플링 되어 있는 것은 아닙니다.

 

아래 사진의 오른쪽처럼 이벤트 매니저를 따로 두거나 static을 활용하여 디커플링을 시킬 수 있습니다. 이렇게 중간에 이벤트 채널을 두는 형태를 Publish/Subscribe 패턴이라고 부릅니다.

이미지 출처 : developers-club

이 차이로 인해 Publish/Subscribe 패턴이 더 loosely coupled 됩니다. 옵저버 패턴의 경우 대부분 동기적으로 구현되는 반면 Publish/Subscribe 패턴의 경우 cross-application이 가능하기 때문에(유니티 밖의 어플리케이션과도 패턴 형성 가능) 비동기적으로 많이 구현됩니다.

 

유니티에서 Observer 패턴과 Publish/Subscribe 패턴 예시와 활용법

예시1 __ Unity UI 이벤트 시스템

모든 게임에서 UI 이벤트는 거의 필수적인 요소입니다. 첫 번째 예시로 유니티에서 제공되는 UI 이벤트 시스템을 활용하는 코드를 살펴보겠습니다. 우리가 구현하려고 하는 기능은 카드를 클릭했을 때(Publish Event) 카드 디테일 팝업이 나타나며(Subscriber1) 지금까지 유저의 총 클릭 횟수를 화면에 표기(Subscriber2)하는 기능입니다. 

 

Observer 패턴으로 먼저 아래와 같이 구현해보았습니다. 클릭 발생 시 실행해야 하는 코드들이 OnPointerClick과 섞여 있습니다.

public class CardIcon : MonoBehaviour, IPointerClickHandler
{
    // ...
    
    // 이 함수는 유니티에서 제공되는 IPointerClickHandler 인터페이스에서 구현해야 하는 메서드입니다.
    public void OnPointerClick(PointerEventData eventData)
    {
        ShowCardDetailPopup(eventData)
    }
    
    public void ShowCardDetailPopup(PointerEventData evt) 
    { 
        // ...
    }
    
    // ...
}

public class ClickCount : MonoBehaviour, IPointerClickHandler
{
    public void OnPointerClick(PointerEventData eventData)
    {
        ShowTotalClick(eventData)
    }
    
    public void ShowTotalClick(PointerEventData evt) 
    { 
        // ...
    }
}

 

Publish/Subscribe 패턴은 아래와 같이 구현해보았습니다. EventHandler라는 이벤트 채널을 만들어 위 방법보다 느슨한 결합을 하도록 코드를 작성하였습니다.

public class CardIcon : MonoBehaviour
{
    Start()
    {
        gameObject.GetComponent<EventHandler>().OnClickHandler += ShowCardDetailPopup;
    }
    
    public void ShowCardDetailPopup(PointerEventData evt) 
    { 
        // code
    }
}

public class ClickCount : MonoBehaviour
    Start()
    {
        gameObject.GetComponent<EventHandler>().OnClickHandler += ShowTotalClick;
    }
    
    public void ShowTotalClick(PointerEventData evt) 
    { 
        // ...
    }
}

public class EventHandler : IPointerClickHandler
{
    public Action<PointerEventData> OnClickHandler = null;
    
    public void OnPointerClick(PointerEventData eventData)
    {
        if (OnClickHandler != null)
            OnClickHandler.Invoke(eventData);
    }
}

 

예시2 __ 커스텀 이벤트

이번에는 유니티에서 제공되는 이벤트가 아니라 직접 구현한 이벤트로 예시를 들어보겠습니다. 아래는 적이 피해를 입을 때마다 플레이어의 점수가 올라가고 적이 죽으면 플레이어의 점수가 대폭 올라가며 아이템이 드롭되는 코드입니다. Action을 사용해도 무관하지만 여기서는 delegate, event으로 구현해보겠습니다.

// 이벤트를 발행하는 클래스입니다.
public class Enemy: MonoBehaviour
{
    public delegate void PublishEventOnDamaged();
    public delegate void PublishEventOnDeath();
    public static event PublishEventOnDamaged onEnemyDamaged;
    public static event PublishEventOnDeath onEnemyDeath;

    void OnDamaged()
    {
        onEnemyDamaged?.Invoke();
    }
    
    void OnDeath()
    {
    	onEnemyDeath?.Invoke();
    }
}
// pubsub 모델에서 브로커 역할을 하는 클래스입니다.
public class EventHandler
{
    public static void BindEventOnEnemyDamaged(Enemy.PublishEventOnDamaged evt)
    {
        Enemy.onEnemyDamaged += evt;
    }
    
    public static void BindEventOnEnemyDeath(Enemy.PublishEventOnDeath evt)
    {
        Enemy.onEnemyDeath += evt;
    }
}
// 이벤트를 구독할 두 클래스입니다.
public class Player : MonoBehaviour
{
    void GetSmallScore() {}
    
    void GetBigScore() {}
    
    void Start()
    {
    	EventHandler.BindEventOnEnemyDamaged(GetSmallScore);
        EventHandler.BindEventOnEnemyDeath(GetBigScore);
    }
}

public class ItemSpawner : MonoBehaviour
{
    void DropItem() {}
    
    void Start()
    {
    	EventHandler.BindEventOnEnemyDeath(DropItem);
    }
}

 

물론 상황에 따라 코드의 응집도와 가독성을 위해 pubsub를 활용하는 것보다 중간 매개체 없이 Observer 패턴이 더 유리할 수도 있습니다. 즉, 패턴은 말 그대로 패턴이니 내가 구현하려는 것이 무엇인지에 따라 적절한 패턴을 사용하여 코드를 작성하는 것이 바람직합니다.

반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기