디자인패턴

Unity MVP 패턴

SniKuz 2024. 6. 24. 21:11

목차

 

MV + * 패턴

개발을 하면서 조금 더 구조화된 환경, 유연하며 협업에도 도움이 되는 환경을 모두 원할 것입니다. MVC, MVP, MVVM 패턴을 찾아보며 '실제로 개발하면서 어떻게 패턴을 적용할까?'에 대한 정리입니다.

 

Unity에 적합한 MV + * 패턴 

Unity에 경우 자체적으로 MVVM에 필수적인 *데이터 바인딩 기능을 제공하지 않고(Unity UI Toolkit으로 데이터 바인딩 지원), MVVM 패턴 설계가 복잡하다는 단점이 있습니다.

View를 담당할 컴포넌트에서 버튼과 같은 입력을 처리하기에 View에서 입력을 처리하는 MVP가 더 적합하다 생각됩니다. 또한 * UniRX와 같은 라이브러리를 통해 Observable 패턴을 써서 MVP 구현을 효율적으로 할 수 있습니다.
무엇보다 Unity에서 작성한 'Level up your code with game programming patterns' 전자책 예시 중 MVP 패턴이 들어가 있어 유니티 오피셜(?)이라 생각합니다. [Unity6가 나오며 위 전자책이 Update되며 MVVM 패턴도 추가되었습니다. 24.8.22]

* 데이터 바인딩을 구현할 수 있는 디자인 패턴 중 한 방법으로 옵저버 패턴 사용 가능.
* UniRx : Reactive Extensions for Unity의 약자로 Unity용으로 최적화해 Rx기능을 쓸 수 있게 만든 라이브러리

 

 

 

MVP in Unity

 'Level up your code with game programming patterns'에 소개되어 있는 예제입니다.

Health (Model)에서는 Health에 대한 정보만 담고 있습니다. 생략되어있는 부분은 Presenter가 Model을 조작할 수 있게 적절한 명령을 제공합니다. (데이터 조작 + 변경 알림)

더보기
public class Health : MonoBehaviour
{
    public event Action HealthChanged;
    
    private const int minHealth = 0;
    private const int maxHealth = 100;
    private int currentHealth;

    public int CurrentHealth { get => currentHealth; set => currentHealth = value; }
    public int MinHealth => minHealth;
    public int MaxHealth => maxHealth;
    
    /*
    	control the fields
        (update data + UpdateHealth();)
    */
    
    public void UpdateHealth()
    {
    	HealthChanged.Invoke();
    }
}

Health Presenter는 Model과 View를 알고있는 상태로 Model에 명령을 매핑하는 함수를 가지고 있는 형태입니다.
사용자 입력을 적절히 해석해 매핑함수를 실행시킬 수 있으며, 예제의 경우 버튼에 인스펙터로 넣어놓기도 했습니다.

더보기
public class HealthPresenter : MonoBehaviour
{
    [Header("Model")]
    [SerializeField] Health health;

    [Header("View")]
    [SerializeField] Slider healthSlider;
    [SerializeField] Text healthText;
    
    void Start()
    {
    	if(health != null)
        	health.HealthChanged += OnHealthChanged;
        
        Reset(); // health?.Restore();
    }
    
    /*
    	control the model
        mapping to appropriate model func
    */
    
    public void UpdateView()
    {
    	// update view codes
    }
    
    public void OnHealthChanged()
    {
    	UpdateView();
    }
}

View는 오직 UI 와 관련한 로직과 입력을 Presenter에 전달하고, Presenter에서 대부분을 처리하기에 Passive View 형태라고 생각됩니다. 

 

 

 

MV + Reactive Presenter 

MVP패턴을 구현할 때 Reactive Programming을 이용해 구현한 형태를 MVRP로 부르는 것 같습니다. 이 이름이 언제 처음 시작된 것인지 알 수 없으나 Unity에서는 UniRx에 적혀있는 예제가 계기인 것 같습니다. UniRX에 MVRP가 처음 기록된 것은 12.Mar.2015 commit 기록이며 이전 시기 글이 많지 않기에 2015년도 이전에 시작된 용어라 생각합니다.

MVRP의 장점은 다음과 같습니다. ReactiveX를 사용하면 Presenter 내부에 ChangeHealth()와 같은 메서드를 사용하는 대신, View의 인터페이스로 이동해 Observable하게 만듭니다. 이를 통해 Presenter는 필요한 것과 시기를 요청할 수 있습니다. View는 더 이상 Presenter에 메서드를 호출하거나 결정할 필요가 없습니다. 이를 통해 다음과 같은 문제를 해결할 수 있습니다.

  • 필요한 모든 메서드는 View 인터페이스와 함께 적용되므로 호출하지 않는 실수를 범할 수 없습니다.
  • Presenter는 Observables에 구독(subscribe) / 취소(unsubscribe) 시기를 담당합니다. View는 Presenter와 적극적으로 상호작용하지 않습니다.
  • View에서 할 수 있는 실수에 더 이상 영향을 받지 않아 테스트에 대한 신뢰를 높일 수 있습니다. 대부분에 동작을 단위테스트로 테스트할 수 있어 View 테스트의 필요성이 줄어듭니다.

다음은 UniRx에 예제를 조금 바꾼 예입니다.

Reactive Model은 데이터를 담고 있습니다.

더보기
public class ReactiveModel
{
    public ReactiveProperty<int> ClickCount {get; private set;}
    public IReadOnlyReactiveProperty<bool> IsOverTen {get; private set;}
    public ReactiveModel(int initCount)
    {
        ClickCount = new ReactiveProperty<int>(initCount);
        IsOverTen = ClickCount.Select(v => v >= 10).ToReactiveProperty();
    }
}

Reactive Presenter는 Model과 View를 가진 상태로 이 둘을 UniRX를 통해 바인딩합니다.

더보기
namespace Snikuz.DesignPattern.MVP
{
    public class ReactivePresenter : MonoBehaviour
    {
        [Header("Model"), SerializeField]
        ReactiveModel model = new(0);
        [Header("View"), SerializeField]
        Button countButton;
        [Header("View"), SerializeField]
        TextMeshProUGUI countText;

        void Start()
        {
            countButton.OnClickAsObservable().Subscribe( _ => model.ClickCount.Value += 1);
            model.ClickCount.Subscribe(_ => countText.text = "" + model.ClickCount);
            model.IsOverTen.Where(isOverTen => isOverTen == true)
                .Subscribe( _ => 
                {
                    iLLiLogger.D($"It over ten");
                });
        }
    }
}

V → RP → M → RP → V 흐름으로 완전히 Reactive 방법으로 연결됩니다. 또한 적응이 된다면 매우 간결하게 패턴을 구현할 수 있어보입니다.

 

 

- 정리

MVC, MVP, MVVM 패턴을 고민했을때 Unity에서 MVP가 가장 접근하기 쉽다는 결론으로 정리됐습니다.  Unity에 데이터 바인딩 에셋이 있는걸로 알고 UI Toolkit에서 제공한다고 하지만 소개와 예제가 부족하다 생각하고, UniRx만큼 대중적이고 무료인 라이브러리는 없다고 생각하며, MVVM 구조는 설계하기 어렵기 때문입니다. 남은 MVC와 MVP 중에서는 각 패턴이 생긴 계기를 생각하면 기존 GameObject-based  UI System에서 View역할을 할 Component들이 버튼 등에 입력 상호작용을 하며, 편의성을 생각할 때도 MVP가 더 적합합니다.

예제를 볼 때 MVP 패턴을 사용한다면 UniRx를 사용한 방식이 간결하고 효과적입니다. 일본 개발자 사이트에 2022년도 UniRx에 대한 글을 번역한 포스팅에서 'Unity 버전이 올라가며 여러 최신 C# 기능도 사용할 수 있게 되면서 UniRx 사용처가 좁아졌지만, 이벤트 통지 등 UniRx가 효율적인 부분이 있고 그 중 MVRP 또한 아직까지 효율적'으로 생각한다는 점에서 MVP 패턴을 구현하기 위해서 UniRx가 효과적이라는게 사람들의 보편적인 생각같습니다.

Unity UI Toolkit은 웹(html, css, javascript)와 유사하게 (uxml, uss, cs)라는 3가지로 구성해 만든다고 합니다. 기존 Unity UI가 GameObject-based UI system이면 UI Toolkit은 웹에서 영감을 얻은 UI 방식이라는데, 데이터 바인딩을 제공해서 MVVM 패턴을 사용한다면 이쪽으로 공부해서 사용해야 하나 나중에 확인해봐야 할 것 같습니다.

 

*참고자료

더보기