Unity에서 MVP 패턴으로 UI를 깔끔하게 관리하기

2025. 1. 28. 15:58유니티 unity

최근 게임 개발에서 유지보수성과 확장성이 중요한 요소로 부각됨에 따라, Unity에서도 아키텍처 패턴을 적절히 적용해 UI와 로직을 분리하려는 시도가 늘고 있습니다.
그 중에서 MVP(Model-View-Presenter) 패턴은 비교적 간단한 구조로 UI와 비즈니스 로직을 깔끔하게 분리할 수 있어 널리 사용됩니다.
이번 글에서는 Unity에서 MVP 패턴을 적용하는 방법과 예시 코드를 살펴보며, 장점과 주의할 점까지 살펴보겠습니다.


MVP 패턴의 개념 & 장점

MVP 패턴이란?

  • Model: 데이터(상태)와 해당 데이터에 대한 모든 비즈니스 로직을 담고 있는 영역입니다. 보통 Unity API에 의존성을 갖지 않으며, 게임의 핵심 규칙이나 연산 등을 책임집니다.
  • View: 사용자가 직접 보고 상호작용하는 화면 요소를 담당합니다. Unity에서는 MonoBehaviour를 통해 UI(Text, Button 등)를 관리하고, 사용자 입력을 Presenter로 전달하는 역할을 합니다.
  • Presenter: Model과 View 사이를 연결하는 중재자(mediator)입니다. View로부터 입력 이벤트를 받으면 Model을 업데이트하고, Model의 결과를 다시 View로 전달해 화면을 갱신하는 일을 맡습니다.

MVP 패턴의 장점

  1. 유지보수성 증가
    View와 Model을 명확히 분리하면, UI가 수정되어도 Model에 직접적인 영향을 주지 않으며, Model이 변경되어도 UI 로직이 혼란스러워지지 않습니다.
  2. 테스트 편의성
    Presenter와 Model은 Unity 엔진을 거치지 않고도 독립적인 단위 테스트가 가능합니다. UI를 띄우지 않고도 Model과 Presenter가 제대로 동작하는지 확인할 수 있습니다.
  3. 확장성 및 재사용성
    동일한 Model을 여러 View에서 재활용하거나, Presenter 로직만 일부 교체하여 다양한 UI를 만들 수 있습니다.

MVP, MVC, 그리고 MVVM 비교

MVC(Model-View-Controller)

가장 전통적인 구조. Controller는 사용자 입력을 받아 Model에 반영하고, Model이 변경되면 View에 반영됩니다. Unity에서는 UI와 Controller를 같은 GameObject에서 관리하게 되거나, 코드 분리가 중구난방이 될 여지가 있습니다.

MVVM(Model-View-ViewModel)

View와 ViewModel 사이에 데이터 바인딩을 강력하게 활용하는 패턴입니다. WPF나 모바일 개발(Xamarin, Flutter) 등에서 자주 쓰이지만, Unity 표준 UI에는 바로 적용하기가 조금 번거롭습니다(별도의 바인딩 시스템 필요).

MVP(Model-View-Presenter)

View는 Presenter에만 의존하고, Model도 Presenter에만 의존합니다. 즉 View ↔ Presenter ↔ Model 간의 양방향 의존이 아니라 Presenter 중심의 단방향 흐름이 생기므로, 코드 구조가 단순화되는 이점이 있습니다.


Unity에서의 MVP 예시 코드

이번 예시에서는 버튼을 누를 때마다 카운트가 증가하고, 그 값을 UI 텍스트에 출력하는 간단한 시나리오를 구성해봅니다.

 

Model: CounterModel.cs

public class CounterModel
{
    private int _count;

    public int Count => _count;

    public void Increment()
    {
        _count++;
    }

    public void Reset()
    {
        _count = 0;
    }
}
  • CounterModel은 데이터(_count)와 이를 조작하는 로직(Increment, Reset)만 가집니다.

 

View 인터페이스: ICounterView.cs

public interface ICounterView
{
    void UpdateCountText(int count);
}

 

 

  • Presenter가 “현재 카운트를 화면에 업데이트해 주세요”라고 요청할 때 호출할 함수를 정의합니다.
  • 실제 UI 표시 방법은 CounterView가 담당하지만, Presenter는 ICounterView 인터페이스만 알고 있으면 충분합니다.

 

 

 

 

View 구현체: CounterView.cs

using UnityEngine;
using UnityEngine.UI;

public class CounterView : MonoBehaviour, ICounterView
{
    [SerializeField] private Text _countText;
    [SerializeField] private Button _incrementButton;
    [SerializeField] private Button _resetButton;

    private CounterPresenter _presenter;

    private void Awake()
    {
        // Presenter 초기화 (View 자신과 Model 주입)
        _presenter = new CounterPresenter(this, new CounterModel());

        // 버튼 클릭 시 Presenter로 이벤트 전달
        _incrementButton.onClick.AddListener(_presenter.OnIncrementButtonClicked);
        _resetButton.onClick.AddListener(_presenter.OnResetButtonClicked);
    }

    public void UpdateCountText(int count)
    {
        if (_countText != null)
        {
            _countText.text = $"Count: {count}";
        }
    }
}

 

 

  • MonoBehaviour 상속을 통해 Unity에서 UI 요소를 다룹니다.
  • Presenter를 만들어 연결(this는 ICounterView로, new CounterModel()은 Model로).
  • 버튼 클릭 이벤트가 발생하면 직접 로직을 처리하지 않고 Presenter에게 넘깁니다.
  • Presenter가 호출하는 UpdateCountText(int count) 메서드를 통해 UI를 갱신합니다.

 

Presenter: CounterPresenter.cs

 

public class CounterPresenter
{
    private readonly ICounterView _view;
    private readonly CounterModel _model;

    public CounterPresenter(ICounterView view, CounterModel model)
    {
        _view = view;
        _model = model;

        // 초기 UI 상태 업데이트
        _view.UpdateCountText(_model.Count);
    }

    public void OnIncrementButtonClicked()
    {
        _model.Increment();
        _view.UpdateCountText(_model.Count);
    }

    public void OnResetButtonClicked()
    {
        _model.Reset();
        _view.UpdateCountText(_model.Count);
    }
}

 

  • View와 Model을 주입받아 보관합니다.
  • View의 이벤트를 받아 Model을 변경하고, 변경된 Model의 상태를 다시 View에 반영하는 흐름을 구현합니다.
  • MVP의 핵심: View는 UI 표현과 이벤트 전달만 담당하고, Model은 데이터만 관리하며, Presenter가 그 둘을 이어주는 중심 축이 됩니다.

 

 

 

  1. 게임 실행 시, CounterView 컴포넌트(MonoBehaviour)가 활성화됩니다.
  2. Awake()에서 CounterPresenter와 CounterModel이 생성되어 서로 연결됩니다.
  3. 버튼이 눌리면 Presenter의 메서드를 호출하고, Presenter는 Model의 데이터(_count)를 변경합니다.
  4. Presenter는 변경된 데이터(Count)를 View에 전달해 UI를 갱신합니다.

이처럼 View ↔ Presenter ↔ Model 간의 역할이 명확히 분리되어, 코드 가독성과 유지보수성이 크게 향상됩니다.

 

 - Presenter의 단위 테스트

  • Presenter는 Model과 View 인터페이스만 참조하므로, View를 Mock(가짜 구현체)으로 대체하여 테스트할 수 있습니다.
  • 예: OnIncrementButtonClicked()를 호출했을 때, View의 UpdateCountText()가 올바른 파라미터로 호출되는지 체크 가능합니다.
  • 이렇게 하면 굳이 Unity Editor에서 씬을 실행하지 않고도 대부분의 로직을 검증할 수 있어, 개발 속도와 안정성을 모두 잡을 수 있습니다.

 - Model의 독립성

  • Model은 게임 전체에서 공유하거나, Presenter와 1:1 대응하도록 구성할 수 있습니다.
  • 중요한 것은 Model이 Unity 엔진에 의존하지 않는 독립적인 클래스여야 한다는 점입니다.
  • 필요하다면 ScriptableObject 등으로 확장하여 데이터를 직렬화/관리하는 방안을 고려할 수도 있습니다.

 - View는 무겁게, Presenter는 가볍게?

  • View는 UI 표시에 필요한 버튼, 텍스트, 애니메이션, 이펙트 등을 다루므로 상대적으로 많은 코드가 들어갈 수도 있습니다.
  • 그러나 핵심 로직(상태 변경, 조건 검증 등)은 Presenter에 두고, View는 “어떻게” 보일지만 집중하도록 해야 합니다.
  • “View에서 고민할 로직인가, Presenter에서 관리할 로직인가”를 끊임없이 고민하면서 구조를 개선해 나가는 것이 중요합니다.

 - 기능 확장 시 모듈화

  • UI 화면이 늘어나거나 기능이 복잡해질 때, Presenter를 단순하게 유지하기 위해 부 Presenter(하위 Presenter), Model 분할 등을 고려해야 합니다.
  • 예: 상점 화면, 메인 메뉴, 인벤토리 등 서로 다른 시스템을 독립적으로 동작하게 하고, UI 매니저나 Scene 전환 로직을 통해 Presenter를 스위칭하는 방식을 사용합니다.

MVP 패턴 사용 시 주의할 점

  1. Scene 전환 및 의존성 주입:
    Presenter를 생성할 때 필요한 Model을 어떻게 생성하거나 주입할지 고민이 필요합니다.
    • 규모가 커지면 의존성 주입(Dependency Injection) 프레임워크를 도입하는 것도 방법입니다(예: Zenject).
  2. 적절한 책임 분배:
    View가 너무 많은 로직을 가져가면 MVC처럼 변질될 수 있고, Presenter가 UI 로직까지 모두 담당하면 오히려 혼잡해질 수 있습니다.
    • “뷰는 보여주기와 사용자 입력에 대한 최소한의 책임만, 모델은 데이터와 로직, 프레젠터는 양쪽 연결”이라는 원칙을 지켜야 합니다.
  3. 테스트 전략:
    MVP를 도입하면 테스트 자체는 편해지지만, 실제 UI 환경(애니메이션, 터치 입력 등)은 별도의 통합 테스트 또는 수동 테스트로 확인해야 합니다.

결론 및 요약

  1. 뷰를 MonoBehaviour로, 프레젠터와 모델은 POCO(Plain Old C# Object) 형태로 분리해보세요.
  2. 뷰-프레젠터-모델 간의 의존성을 명확히 하여, 서로 “무엇을” 주고받는지 인터페이스를 통해 잘 정의하면 구조가 깔끔해집니다.
  3. MVP 도입으로 인해 초기 세팅이 다소 번거로울 수 있지만, 프로젝트가 커질수록 유지보수성과 확장성, 테스트 편의성 측면에서 큰 이점을 얻을 수 있습니다.

Unity에서 MVP를 적용하는 것은 복잡해 보이지만, 막상 시도해보면 UI 요소와 로직이 따로따로 굴러가는 모습을 보면서 코드가 한층 깔끔해졌음을 체감할 수 있을 겁니다.
**“UI는 화면 표시와 입력만, 로직은 Presenter와 Model에서 담당한다”**는 컨셉에 익숙해지면, 장기적으로 개발 생산성과 코드 품질이 모두 향상될 것입니다.