Unity

[Unity] MVC, MVP, MVVM 패턴

sunlight-dby 2025. 6. 17. 17:48

 

 

 

이 글은 해당 유튜브를 보고 진행한 것에 대해 공부한 것을 정리한 글입니다.

 


 

UI 아키텍처 패턴(MVC, MVP, MVVM)의 공통 목표

  • UI와 로직의 분리
    • UI 요소와 비즈니스 로직이 뒤엉키는 것을 방지해 유지보수를 용이하게 합니다.
  • 불필요한 종속 관계 감소
    • 컴포넌트 간 직접적인 의존을 줄여 유연성과 재사용성을 확보합니다.
  • Soc(Separation of Concerns), 관심사의 분리
    • 각 요소는 자신의 역할에만 집중하고, 서로 최소한으로 관여합니다.
  • 스파게티 코드 방지
    • 복잡하게 얽힌 코드 흐름을 방지하고 가독성과 안전성을 높입니다.
  • 아키텍처 패턴으로 분류
    • 소프트웨어의 구조를 체계적으로 설계하기 위한 고수준 설계 방법입니다.
  • UI 프레임워크 의존성 존재
    • 특히 MVVM은 데이터 바인딩 기능이 있는 UI 프레임워크(ex : Unity UI Toolkit, WPF 등)에 의존합니다.

MVC (Model - View - Controller)

MVC는 사용자 인터페이스(UI) 개발에서 널리 사용되는 패턴으로, 애플리케이션을 다음 3개의 책임 있는 컴포넌트로 분리합니다.

  • Model : 데이터와 그 처리
  • View : 사용자 인터페이스
  • Controller : 사용자 입력과 로직

MVC, Model View Controller


구성 요소

M : Model

  • 애플리케이션 데이터를 관리합니다.
  • 단순히 값을 저장하거나 제공하는 역할을 하며, 게임 플레이 로직을 수행하거나 계산을 포함하지는 않습니다.

V : View

  • 데이터를 사용자에게 표시하는 그래픽 인터페이스입니다.
  • 모델 데이터를 기반으로 화면을 갱신합니다.

C : Controller

  • 사용자의 입력을 처리하고 그에 따라 모델을 업데이트합니다.
  • 게임 데이터 및 로직을 처리하고 런타임에 값이 어떻게 변하는지 계산하는 것을 담당합니다.
  • Controller는 그 자체로 게임 데이터를 포함하지 않고, 뷰에도 포함되지 않으며 직접 뷰를 조작하지 않습니다.

특징

  • 각 레이어(M, V, C)의 기능을 제한하므로, 단일 책임 원칙(SRP)에 충실합니다.
  • 구조적이고 테스트 가능한 코드 구성이 가능합니다.
  • UI 프레임워크와의 연동이 용이합니다.
  • 불필요한 종속성과 스파게티 코드를 줄입니다.
  • View가 Controller와 Model을 모두 알아야 해서, 종속성이 발생할 수 있습니다.
  • MVC 적용 시, UI 프레임워크(UI ToolKit, Unity UI)를 활용하기 때문에 View 자체는 문제가 업지만, 모델 데이터의 변경 사항을 감지하기 위한 View 별 코드가 직접 필요할 수도 있어 복잡도가 증가할 수 있습니다.

MVP (Model - View - Presenter)

MVC를 개선한 형태로, View와 Model 사이에 Presenter를 추가해 각 요소 간 의존성을 줄이는 구조입니다.

MVP, Model View Presenter

구성 요소

M : Model

 •  데이터를 저장하고, 데이터 관련 로직을 처리합니다.

    • 게임의 현재 상태, 게임 속성, 데이터에 대한 로직 (캐릭터 레벨업, 체력, 점수 등)

        등이 이에 해당합니다.

 •  View에 대한 정보를 전혀 알지 못합니다.

V : View

 •  사용자에게 데이터를 표시하는 UI를 담당합니다.

 •  사용자 상호작용을 Presenter에 전달하고, Presenter로부터 받은 데이터만 표시합니다.

 •  MVC에서는 Controller가 사용자 입력을 처리하고 Model을 조정하는데 비해,

  MVP의 View는 Model과 직접 상호작용하지 않습니다.

 •  UI 로직에 대한 별도의 스크립트가 있을 수 있지만, 게임의 비즈니스 로직을 직접 처리하지 않습니다.

P : Presenter

 •  Model과 View 사이를 중재합니다.

      •  Model로부터 데이터를 검색한 다음 뷰에 표시할 수 있도록 형식을 지정합니다.

 •  View로부터 입력을 받고 Model을 변경한 뒤, View를 갱신합니다.

 •  Model의 변경 사항을 구독하여 View를 갱신합니다.

 


특징

  • 확장성
    • 특정 작업을 처리하는 부분들이 나누어 있어, 코드 베이스를 관리 및 확장하는 것이 용이합니다.
  • 모듈성
    • 한 구성 요소의 변경 사항이 전체 시스템에 영향을 미치지 않습니다.
      • 각 구성 요소는 독립적으로 개발, 테스트 및 수정될 수 있습니다.
      • 코드를 디버깅하기가 더 용이합니다.
  • 재사용성 및 유지보수 용이
    • 코드의 일부(예 : 모델 또는 프레젠터)는 애플리케이션의 다른 부분에서도 재사용이 가능합니다.
    • 스파게티 코드가 될 확률이 적습니다.
  • 클래스 수가 증가하여 복잡도가 증가할 수 있습니다.
  • 모든 View의 상태 변경을 Presenter가 처리해야 하므로 복잡도가 증가할 수 있습니다.

구현 예시

Model 역할 : Health 클래스

 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;
    
    public void Increment(int amount)
    {
        currentHealth += amount;
        currentHealth = Mathf.Clamp(currentHealth, minHealth, maxHealth);
        UpdateHealth();
    }
    
    public void Decrement(int amount)
    {
        currentHealth -= amount;
        currentHealth = Mathf.Clamp(currentHealth, minHealth, maxHealth);
        UpdateHealth();
    }
    
    public void Restore()
    {
        currentHealth = maxHealth;
        UpdateHealth();
    }
    
    public void UpdateHealth()
    {
        HealthChanged?.Invoke();
    }
 }

 

  • 값이 변경될 때마다 실제 체력 값을 저장하고 HealthChanged 이벤트를 호출합니다.
  • 게임 플레이 로직이 포함되지 않으며, 데이터를 늘이거나 줄이는 메서드만 있습니다.
  • 대부분의 오브젝트는 Health 자체를 조작하지 않고, HealthPresenter를 예약해 처리합니다.

Presenter 역할 : healthPresenter

 public class HealthPresenter : MonoBehaviour
 {
    [SerializeField] Health health;
    [SerializeField] Slider healthSlider;
    
    private void Start()
    {
        if (health != null)
        {
            health.HealthChanged += OnHealthChanged;
        }
        UpdateView();
    }
    
    private void OnDestroy()
    {
        if (health != null)
        {
            health.HealthChanged -= OnHealthChanged;
        }
    }
    
    public void Damage(int amount)
    {
        health?.Decrement(amount);
    }
    
    public void Heal(int amount)
    {
        health?.Increment(amount);
    }
    
    public void Reset()
    {
        health?.Restore();
    }
    
    public void UpdateView()
    {
        if (health == null)
            return;
        if (healthSlider !=null && health.MaxHealth != 0)
        {
            healthSlider.value 
                = (float) health.CurrentHealth / (float) health.MaxHealth;
        }
    }
    
    public void OnHealthChanged()
    {
        UpdateView();
    }
 }

 

  • 다른 게임 오브젝트는 Damage, Heal, Reset을 사용해 체력 값을 수정하려면 HealthPresenter를 사용해야 합니다.
  • HealthPresenter는 보통 Health가 HealthChanged 이벤트를 발생시킬 때까지 UpdateView로 사용자 인터페이스를 업데이트하기 위해 대기합니다.
    • 이 기능은 모델의 값을 설정하는 데 소요되는 기간이 짧은 경우에 유용합니다.

MVP 및 MVC의 장점

  • 원활한 업무 분배
    • 뷰를 프레젠터에서 분리했으므로, 사용자 인터페이스 개발과 업데이트를 나머지 코드 베이스와 거의 독립적으로 수행할 수 있습니다.
  • MVP 및 MVC로 간소화된 유닛 테스팅
    • 이 디자인 패턴은 게임 플레이 로직과 사용자 인터페이스를 분리합니다. 그렇기 때문에 에디터에서 플레이 모드를 사용하지 않아도 오브젝트를 코드와 연동해 시뮬레이션할 수 있으며, 이를 통해 상당한 시간을 단축할 수 있습니다.
  • 유지 가능하며 가독성이 높은 코드
    • 이 디자인 패턴을 활용하면 비교적 작은 클래스를 만들게 되므로 코드의 가독성이 높아집니다. 일반적으로 종속 관계가 적을수록 소프트웨어 오류가 발생할 수 있는 부분과 잠재적인 버그가 있는 부분이 줄어듭니다.

MVP 및 MVC의 단점

  • 사전 계획 필요
    • MVC와 MVP는 비교적 큰 규모의 아키텍처 패턴입니다. 해당 패턴을 사용하려면 책임에 따라 클래스를 나눠야 하며, 이 과정에서 약간의 정리 및 사전 작업이 필요합니다.
  • Unity 프로젝트의 모든 요소가 패턴에 적합하지는 않습니다.
    • 데이터, 로직, 인터페이스로 쉽게 분리되지 않는 Unity 컴포넌트도 있습니다.

MVVM (Model - View - ViewModel)

MVVM은 데이터 바인딩 기능을 제공하는 UI 프레임워크에서만 가능한 패턴입니다.

 

MVVM, Model View ViewModel

Model

 •  비즈니스 로직에서 사용되는 데이터를 포함하는 데이터 구조입니다.

 •  어떤 형태(ScriptableObject, MonoBehaviour)든 가능합니다.

View

 •  사용자에게 데이터를 표시하고 상호작용하는 UI입니다.

 •  일반적으로 UI Element로 구성됩니다.

 •  View에 포함된 로직은 예를 들어 UI Element의 스타일과 관련되어 있습니다.

 •  UI Toolkit에서는 USS 스타일 시트와 UXML파일로 구성됩니다.

ViewModel

 •  View와 Model 사이의 중재자 역할을 합니다.

 •  사용자가 View와 상호작용할 때, Model을 업데이트하는 역할을 합니다.

 •  Model이 변경될 때 View를 업데이트하는 로직도 포함됩니다.

 •  ViewModel과 View는 데이터 바인딩 기능을 통해 연결됩니다.

 

 


특징

  • 기존의 MVC / MVP에서 View와 Controller / Presenter 의존성을 약화한 패턴입니다.
  • View와 ViewModel 사이를 데이터 바인딩을 통해 양방향 자동 동기화를 시켜줍니다.
  • Soc가 잘 되어 있습니다.
    • ViewModel은 View에 대한 참조 없이 작동하므로, 테스트 용이성이 뛰어납니다.
  • 커맨드 패턴을 통해 ViewModel에서 UI 상호작용을 처리합니다.

장점

  • 데이터 바인딩을 사용하여 UI를 데이터와 동기화하는 데 필요한 코드의 양을 줄입니다.
  • 읽고 유지하기 쉬운, 보다 간결한 코드로 작성이 가능합니다.
  • 데이터 바인딩을 통해 오래되거나 잘못된 데이터가 표시될 위험을 줄여 UI 일관성을 향상시킵니다.
  • ViewModel을 테스트하기 쉽습니다.
  • 대규모 UI에서 생산성이 향상됩니다.

단점

  • 데이터 소스, 데이터 소스 경로, 바인딩 모드, 컨버터 등을 설정하려면 번거로울 수 있습니다.
  • 각 데이터 바인딩을 설정하는 데 따른 추가 오버헤드가 발생할 수 있습니다.
  • 초기 설계가 복잡하므로, 작은 프로젝트에서는 과한 구조일 수 있습니다.
    • MVVM 패턴은 추가 복잡성 비용보다 이점이 더 큰 대규모 사용자 인터페이스에 적합합니다.

데이터 바인딩 (Data binding)

MVVM의 주 핵심으로, 뷰와 뷰모델 간의 데이터 바인딩을 통해 뷰모델 속성 변경이 자동으로 뷰에 반영됩니다.

 

Unity 6의 모든 UI Tookit이 지원되며, 런타임에서도 C# 객체와 UI 요소 간의 바인딩을 지원합니다.

직렬화된 데이터 용이 아닌 이상 편집기 UI에서도 사용할 수 있습니다.

 

특징

  • UI가 아닌 개체의 속성과 UI 요소 간의 동기화를 보장합니다.
    • 예 : MonoBehaviour의 문자열 속성과 TextField의 값 속성
  • 바인딩은 본질적으로 UI가 아닌 속성과 이를 수정하는 UI 요소 간의 링크입니다.
  • 바인딩을 설정하면 기본 데이터와 해당 시각적 요소 간의 변경 사항이 자동으로 동기화됩니다.
    이렇게 하면 각 UI 업데이트에 대해 수동으로 이벤트 핸들러를 작성할 필요가 없습니다.

MVP vs MVVM

MVP

  • Presenter는 상태 변경을 감지하기 위해 Model의 이벤트를 구독합니다.
  • 변경 사항이 통보되면 Presenter는 데이터를 View에 업데이트합니다.

MVVM

  • Model에 필요한 Converter를 등록하는 것부터 시작하고, 그런 다음 ViewModel에서 데이터 바인딩을 설정합니다.
  • 이 설정을 사용하면 모델의 변경 사항이 기존 바인딩을 통해 자동으로 뷰를 업데이트합니다.

UI Toolkit Assets

UI Toolkit Asset은 Unity에서 UI를 구성할 때 사용하는 시각적 UI 구성 리소스입니다.

Unity의 UI Toolkit 시스템은 기존의 uGUI(Canvas 기반 UI)와 달리, 웹 기술에서 차용한 선언적 방식으로 UI를 작성할 수 있도록 설계되었습니다.

UI Toolkit Asset 구성


구성요소

UXML (Unity XML)

<ui:UXML xmlns:ui="UnityEngine.UIElements">
  <ui:VisualElement name="Root">
    <ui:Label text="Hello, World!" />
    <ui:Button text="Click Me" />
  </ui:VisualElement>
</ui:UXML>
  • UI의 구조(레이아웃)를 정의하는 XML 파일입니다.
  • HTML과 유사한 형식으로 UI 요소를 트리 구조로 배치할 수 있습니다.
  • VisualElement, Button, Label 등 다양한 요소를 선언적으로 정의할 수 있습니다.

USS (Unity Style Sheet)

.button-primary {
    background-color: #4CAF50;
    color: white;
    font-size: 14px;
    margin: 4px;
}
  • 스타일을 정의하는 CSS 유사 형식의 스타일 시트입니다.
  • 각 UI 요소의 색상, 글꼴, 간격, 테두리 등 스타일 속성을 설정합니다.
  • 클래스 선택자(.class-name)와 이름 선택자(#id)를 통해 요소에 스타일 적용이 가능합니다.

CS

public class MainMenu : MonoBehaviour
{
    public VisualTreeAsset uiAsset;

    void Start()
    {
        var root = uiAsset.CloneTree();
        var button = root.Q<Button>("StartButton");
        button.clicked += OnStartClicked;

        var uiRoot = GetComponent<UIDocument>().rootVisualElement;
        uiRoot.Add(root);
    }

    void OnStartClicked()
    {
        Debug.Log("게임 시작!");
    }
}
  • C# 코드에서 VisualTreeAsset을 통해 UXML을 로드하고 인스턴스화합니다.
  • 생성된 UI는 UIDocument 컴포넌트를 통해 Unity 씬에 렌더링됩니다.
  • 데이터 바인딩 기능을 활용하여, 런타임 시 C# 객체의 속성과 UI 요소 속성을 자동으로 동기화할 수 있습니다.