환경
GUI 주변의 디자인 패턴
Model-View-(Reactive)Presenter패턴(약칭하여 MV(R)P 패턴)이란 Unity에서 GUI 주변의 설계 패턴 의 일종입니다.
「GUI」란, 이른바 「유저 인터페이스」를 말해, 게임중에 있어서의 「화면상에 표시되는 정보」나 「메뉴」나 「버튼」이라고 하는 것을 가리킵니다.
(자꾸 말하면, uGUI일이라고 생각해 주세요)
GUI 주위의 구현수법이라고 하는 것은 Unity에 한정되지 않고, 복잡하게 되기 쉽다 어려운 부분입니다.
그 때문에 여러가지 설계 패턴이 고안되어 왔습니다.
대표적인 것으로 말하면 약간 MVC등 MVVM을 들 수 있습니다.
그 중에서도 MVP패턴이라는 것이 있고, Model-View-(Reactive)Presenter패턴은 이것을 바탕으로 한 설계 패턴입니다.
왜 디자인 패턴이 필요한가?
GUI 주위의 구현시에 이러한 복수의 설계 패턴이 왜 등장하는가 하면, GUI 주위를 깨끗하게 구현하는 것이 중요하기 때문 입니다.
GUI 주변은 그 용도에서 매우 복잡해지기 쉽습니다.
- 화면상의 오브젝트에 표시하는 데이터는, 그 뒤편에서 서로 연동하고 있다
- 인간에게 실시간으로 정보를 표시해야 함
- 사람이 GUI를 통해 데이터 처리를 방해할 수 있음

GUI는 데이터 구조가 복잡하게 되기 쉬운 부분이 있는데, 그 이외에 실시간성, 상호 작용성이 필요한 부분입니다. 그래서 빠르게 구현하다보면 “상호 참조”, “순환 참조”, “비지웨이트”, “이벤트의 무한 루프”등이 발생할 우려가 매우 높은 곳이다.
- 비지웨이트: 단지 대기만 하는 것에 불과한데 쓸데없이 CPU를 소비하여 다른 프로그램이나 스레드에 영향을 미치게 되는 현상
이처럼 복잡하게 되기 쉬운 GUI를 “책임을 분리하여 객체 간의 상호 작용을 구축하면 좀더 나아지지 않을까?”라는 생각에서 MVC, MVVM, MVP 설계가 나온 배경입니다.
빠르게 구현한 예
설계패턴을 사용하지 않고 우직하게 uGUI를 사용하면 이렇게 됩니다는 예입니다.
엉성하게 만들면 다음과 같이 “게임 중에 사용하는 중요한 수치”와 “uGUI에 접근하는 부분”이 같은 클래스 안에 그대로 쓰여지거나 하게 됩니다.
using UnityEngine;
using UnityEngine.UI;
namespace Yabai
{
/// <summary>
/// UI와 데이터와 엉망이 된 클래스
/// </summary>
public class Data : MonoBehaviour
{
[SerializeField] private Slider _slider;
// 프로퍼티 (외부에서 읽고, 쓰기 가능)
public float CurrentValue { get; set; }
private void Update()
{
// Update로 매 프레임 쓸데없이 값을 체크 한다 (최악).
if (_slider.value != CurrentValue)
{
_slider.value = CurrentValue;
}
}
// 코드에서는 참조가 없지만 Unity에서 호출되는 public 메서드
public void OnSliderValueChanged()
{
CurrentValue = _slider.value;
}
}
}

Unity 상에서 객체의 상호 참조가 발생하는 어느 한쪽이 부족하면 오류가 발생하여 게임이 시작되지 않거나 하는 경우가 생길 수 있습니다.
이렇게 되는 것을 피할 목적으로 Unity에서 흔히 쓰이는 것이 MV(R)P 패턴입니다.
Model-View-(Reactive)Presenter 패턴을 말하기 전에 이것의 기초가 되는 Model-View-Prsenter 패턴에 대해 먼저 설명하겠습니다.
Model-View-Presenter패턴
Model-View-Presenter(MVP) 패턴은 GUI의 설계 패턴 중 ‘ Presenter‘이라는 개념을 사용한 것입니다.
GUI 주변의 구성 요소를 다음의 3개로 나누어 생각합니다.
- Model: 데이터의 실체. GUI와는 직접 적인 관계가 없는 어플리케이션의 본체의 요소 부분.
- View: GUI를 제어하는 부분. 데이터를 화면에 표시하거나 반대로 사용자의 조작을 받는 부분.
- Presenter: Model과 View를 연결하는 중재자 역할.

MVP에서 가장 중요한 부분은
- “Presenter가 존재하지 않으면 View와 Model은 완전히 독립된 상태가 된다”는 것입니다.
View와 Model을 연결하는 존재는 Presenter 뿐이어서 Presenter를 배제하면 이 2개는 완전히 독립적이게 됩니다.

즉 “View는 Model을 완전히 모르는 상태”, “Model은 View를 완전히 모르는 상태”가 됩니다.
그리고 이 MVP 패턴은 실제로 코드를 구현시 문제가 발생합니다.
그것은 “Model과 View를 실시간 연동시키려면 어떻게 하면 좋을까?”입니다.
“Model의 변화를 View에 즉각 반영했다” “유저로부터 View의 입력을 Model로 즉시 전달”이란, 실시간 동작을 별도의 방법을 사용하여 구현할 필요가 생긴 것입니다.
이처럼 “실시간으로 동작을 하는 것”을 별도로 마련할 필요가 있는 것이 MVP 패턴의 결점이었습니다.
MVP 으로 MV(R)P
“실시간으로 동작하는 것”에 대해 별도의 처리가 필수 였던 Model-View-Presenter 패턴에 UniRx를 추가함으로써 해당 문제를 보완한 것이 Model-View-(Reactive)Presenter 패턴 입니다.
Unirx가 가진 리액티브 객체, 특히 ReacrtiveProperty의 편리성에 주목하여 MVP 패턴을 응용한 것이 MV(R)P 패턴입니다.
Model-View-(Reactive)Presenter 패턴 설명
다시 「Model-View-(Reactive)Presenter패턴( MV(R)P패턴)」의 해설을 합니다.
개요
Model-View-(Reactive)Presenter패턴은 MVP 패턴과 UniRx를 병용한 Unity에서의 GUI 주위의 구현 기법 입니다.
GUI의 구성 요소를 Model, View의 Presenter세 가지로 분해하고 그들을 UniRx (의 ReactiveProperty)를 사용하여 협력하는 기술이되었습니다.

(Model의 변화를 Presenter를 통해 View에 반영. View의 변화를 Presenter를 통해 Model에 반영. 그 중개에 UniRx를 사용한다.)
MV(R)P패턴이 말하는 것 / 말하지 않은 것
이런 설계 패턴을 이용할 때에 조심해야 하는 부분은 “이 디자인 패턴은 무엇을 말하고 있는가” 입니다.
가끔 해당 패턴에 깊게 빠져 들어 본래의 의미와는 전혀 다른 것을 주장하는 사람이 있거나 하므로 주의가 필요합니다.
MV(R)P패턴이 말하는 것, 말하지 않은 것을 아래에 정리 했습니다.
MV(R)P패턴이 말하는 것
- GUI(특히 uGUI)의 구현 패턴이다.
- View와 Model을 Presenter라는 얇은 레이어로 연결 한다.
- 각 객체 연결에는 Observable(ReactiveProperty)을 활용한다.
MV(R)P패턴이 말하지 않은 것
- Unity
GameObject등은 모두 View이다 (← 말하지 않는다.Model하지만GameObject사용해도 별로 좋다) MV(R)P패턴은 Unity 종속 계층과 Unity 종속 계층을 분리하는 것입니다 (← 말하지 않음).MV(R)P패턴을 사용하면 게임을 무엇이든 깨끗하게 구현할 수 있다(← 말하지 않는다.)- 인게임에
MV(R)P패턴을 사용하면 설계를 잘 할 수 있다(← 말하지 않는다. 클린 아키텍처라든지와 이야기가 섞이지 않는다?)
MV(R)P 패턴은 GUI 구현을 이렇게 만들면 좋아 라는 내용밖에 말하지 않았습니다.
그 이상의 것은 MV(R)P 패턴에 기대를 지나치게 기대한 억측으로 보입니다.
다양한 객체의 역할
MV(R)P패턴에는 다음의 3개의 오브젝트가 등장합니다.
ModelViewPresenter
강조하면 좋은 부분은 이들의 객체는 실제 구현자의 관점에서 상대적으로 결정되는 것 이라는 점입니다.
즉 “앞으로 Model을 만들어야지!”라는 역할을 정하고 객체를 만드는 것이 아닌” 이 객체는 이 GUI에게 Model 역할을 해라”라고 결정하는 형태가 맞습니다.
요약하면 Model과 View등이 엄격하게 정해진 부분이 아닌 시스템 전체의 흐름을 보고 결정하는 것입니다.
Model은
- GUI에 대해서 표시하는 데이터의 실체를 가지는 부분
MV(R)P 패턴에서의 Model은 데이터의 실체를 가진 부분을 말합니다.
바꾸어 말하면 GUI에 표시하는 데이터의 실체를 가진다 입니다.
액션 게임의 예를 들면 “플레이어의 좌표”, “플레이어의 체력”, “적의 체력”, “현재 스테이지 정보”, “나머지 제한 시간”등을 들 수 있습니다.
조금 더 깊이 있는 표현을 한다면 Model은 **“게임의 구성에 필수 불가결한 정보(도메인)”**을 다루고 있는 오브젝트 입니다.
그리고 MV(R)P 패턴에서의 Model의 중요한 부분은 ReactiveProperty를 사용하여 Presenter의 접근을 가능하게 하고 있다는 점입니다.
ReactiveProperty을 공개함으로써 Model의 내부 상태가 변화했을 때 그것이 Observable로서 외부에 통보할 수 있습니다.
Model의 구현 방법
한 객체를 MV(R)P 패턴 Model로 다루기 위해서 필요한 절차는 다음과 같습니다.
- 상태를 IObservable<T> 또는 ReactiveProperty<T>로 공개한다.
- (필요하면) Model 내부 상태를 수정하는 프로퍼티나 메서드를 준비한다.
요약하면 UniRx를 사용하고 상태를 공개한다 입니다.
예를 들면, 다음의 Player는 “체력”을 ReactiveProperty로 공개하고 있습니다. (또 동시에 Enemy와 충돌하면 체력이 줄어듭니다)
using UniRx;
using UnityEngine;
namespace MVRP.Models
{
/// <summary>
/// 액션 게임의 플레이어
/// "화면에 체력을 나타낸다"라는 시점에서 보면 모델
/// </summary>
public sealed class Player : MonoBehaviour
{
/// <summary>
/// 체력
/// Reactive Property로서 외부에 상태를 ReadOnly로 공개
/// </summary>
public IReadOnlyReactiveProperty<int> Health => _health;
private readonly IntReactiveProperty _health = new IntReactiveProperty(100);
/// <summary>
/// 충돌 이벤트
/// </summary>
private void OnCollisionEnter(Collision collision)
{
// Enemy와 충돌하면 체력을 줄인다
if (collision.gameObject.TryGetComponent<Enemy>(out var _))
{
_health.Value -= 10;
}
}
private void OnDestroy()
{
_health.Dispose();
}
}
}
덧붙여, 체력을 Reactive Property를 public으로 공개하여 값의 Read/Write를 직접 가능하게 하거나, Read 전용으로 IReadonly Reactive Property<T>만을 공개하고 Write는 다른 메소드를 경유 시킬지는 어느 쪽이든 상관없습니다.
이 부분은 팀의 설계 규칙이나 개인 취향 등에 따라 정해도 무방 합니다.
View는
MV(R)P 패턴에서 View는 “사용자(유저)에 정보를 표시하거나 사용자의 입력을 받는 부분”을 말합니다.
더 간단하게 말하면, uGUI를 사용하는 부분이라고 생각해도 무방합니다(물론 uGUI 이외를 사용해도 괜찮습니다)
간단하게 uGUI의 “Text”, “Button”, “Slider”의 컴포넌트를 그대로 View로 다룰 수 있습니다.
또한 Tween을 추가하는 등 UI 요소가 애니메이션하는 경우에도 그 애니메이션을 관리하는 기능도 View로 봐도 무방합니다.
View의 구현 방법
한 객체를 View로 다루도록 하기 위해 필요한 것은 Presenter에서 참조 가능하게 하도록 하면 됩니다.
uGUI의 컴포넌트를 직접 View로 본다면 Presenter부터 uGUI 컴포넌트를 직접 참조할 뿐입니다.
무언가 자기가 만든 컴포넌트를 참조시킨다면 Presenter에서 보이는 위치에 그 컴포넌트를 배치할 필요가 있습니다.
예를 들어 다음의 컴포넌트는 uGUI의 Slider를 제어하는 컴포넌트입니다.
이 컴포넌트는 틀림없이 View 요소이므로, Presenter에서 참조해도 문제 없습니다.
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
namespace MVRP.Views
{
/// <summary>
/// uGUI의 Slider를 애니메이션으로 만드는 컴포넌트(View)
/// </summary>
public sealed class AnimationSlider : MonoBehaviour
{
[SerializeField] private Slider _slider;
public void SetValue(float value)
{
// 애니메이션하는 동안 슬라이더를 움직입니다.
DOTween.To(() => _slider.value,
n => _slider.value = n,
value,
duration: 1.0f);
}
}
}
(슬라이더를 애니메이션화 하면서 변화시킨다)
Presenter는
MV(R)P 패턴에서의 Presenter는 Model과 View를 중개하는 객체입니다.
- Model → View: Model 내부 상태의 변화를 ReactiveProperty 혹은 Observable를 통해서 감지하고, 그것을 View에 반영
- View → Model: View 상태 변화를 감지해서 그것을 Model에 반영
- View와 Model 사이에 필요한 데이터의 변환 ( string → int 등의 형 변환이나 값의 표현 범위의 조정 등)
같은 역할을 갖습니다.
(경우에 따라서는 Model → View의 흐름으로 끝나는 경우도 많습니다)
Presenter는 데이터의 변환 정도의 논리 밖에 없는 매우 얇은 레이어 입니다.
Presenter에는 게임 로직과 View의 관리 로직을 가지고 있으면 안되며, 기본적으로 데이터의 교환과 간단한 데이터 변환만 해야 합니다.
Presenter의 구현 방법
Presenter은 인스펙터의 연결을 통해 Model 및 View 양쪽을 모두 참조할 필요가 있습니다.
그래서 이들 2개가 참조할 수 있는 계층 및 모듈에 정의하고 각각을 연결시킵니다.
실제 구현은
Model의 Observable 혹은 ReactiveProperty을 Subscribe() 하고 그 상태 변화를 View에 전달한다.
View의 상태 변화를 Model에 전달한다. 이러한 처리를 구현할 뿐입니다.
다음 구현은 앞의 Player와 AnimationSlider를 연결한 Presenter 입니다.
using MVRP.Models;
using MVRP.Views;
using UniRx;
using UnityEngine;
namespace MVRP.Presenters
{
/// <summary>
/// 플레이어의 체력을 View에 반영하는 Presenter
/// </summary>
public sealed class PlayerHealthPresenter : MonoBehaviour
{
// Model
[SerializeField] private Player _player;
// View
[SerializeField] private AnimationSlider _animationSlider;
private void Start()
{
// Player의 Health 모니터링
_player.Health
.Subscribe(x =>
{
// View에 반영
_animationSlider.SetValue((float)x / _player.MaxHealth);
}).AddTo(this);
}
}
}
Presenter를 잘 만드는 요령은 “Presenter로 복잡한 논리를 작성하지 않는다”, “Presenter에 상태를 가지지 못하게 한다” 입니다.
Presenter는 어디까지난 Model과 View를 잇는 만큼의 존재이며, Presenter의 여부가 게임의 동작에 영향을 주어서는 안됩니다.
(Presenter를 배치하지 않으면 Model의 동작이 바뀌거나 에러를 내어 동작하지 않는다는 등은 좋지 않습니다)
“Presenter와 View는 미 구현으로도 Model만 있으면 게임 자체는 에러를 내지 않고 동작한다(제대로 플레이는 되지 않을 지라도)”라고 생각하세요.
샘플의 동작 예
위에서 작성한 Player와 AnimationSlider의 조합은 실제로 동작시키면 아래 처럼 됩니다.

Player(캡슐)에 Enemy(상자)가 충돌 할 때마다 체력이 감소하고, 그것이 Slider에 반영된다)샘플의 장면 구현

Presenter라는 GameObject에 PlayerHealthPresenter가 부착되어 있습니다.
이것이 유니티의 hierarchy 윈도우상에서 Player와 Animation Slider를 직접 참조하고 있습니다.
별첨 예: Model→View/View→Model의 상호 교환이 있는 사례
아까의 예는 Model→View의 일방 통행이었습니다.
아래는 View→Model의 경로도 있는 패턴 입니다.

using UniRx;
using UnityEngine;
namespace MVRP.Models
{
/// <summary>
/// 데이터의 실체를 가진 클래스
/// </summary>
public sealed class SampleModel : MonoBehaviour
{
// Count 수
public IReadOnlyReactiveProperty<int> Count => _count;
// 최대값
public readonly int MaxCount = 100;
private readonly IntReactiveProperty _count = new IntReactiveProperty(0);
public void SetCount(int value)
{
// 숫자 범위 조정
value = Mathf.Clamp(value, 0, MaxCount);
_count.Value = value;
}
private void OnDestroy()
{
_count.Dispose();
}
}
}
using MVRP.Models;
using UniRx;
using UnityEngine;
using UnityEngine.UI;
namespace MVRP.Presenters
{
/// <summary>
/// Count의 Presenter
/// </summary>
public sealed class CountPresenter : MonoBehaviour
{
// Views
// uGUI 구성 요소를 직접 참조
[SerializeField] private Slider _slider;
[SerializeField] private Text _text;
// Model
[SerializeField] private SampleModel _model;
private void Start()
{
// Model -> View
_model.Count.Subscribe(v =>
{
// Slider
_slider.value = ((float)v / _model.MaxCount);
// Text
_text.text = $"{v}%";
})
.AddTo(this);
// View -> Model
_slider.OnValueChangedAsObservable()
.Subscribe(x =>
{
// Slider는 0.0~1.0이므로 보정
// 이와 같은 Model-View간의 값의 범위 보정도
// Presenter의 책임
var value = (int)(100 * x);
// 모델에 반영
_model.SetCount(value);
})
.AddTo(this);
}
}
}
MV(R)P 패턴의 흔한 질문과 답변
MV(R)P 패턴이 잘 이해가 가지 않는 다는 사람이 모아준 질문과 답변을 정리했습니다.
MV(R)P 패턴은 MonoBehaviour를 사용해야 하나요?
MV(R)P 패턴은 MonoBehaviour의 사용 여부를 말하고 있지 않습니다.
MonoBehaviour는 모든 객체에서 사용해도 좋고 쓰지 않아도 됩니다. 즉 아무래도 좋습니다.
가끔 Model은 Unity에 비 의존할 필요라고 주장하는 사람도 있지만 그런 일은 없습니다.
원래 MV(R)P 패턴은 “GUI 구현을 깨끗하게 하는 것”을 목적으로 설계 한 패턴입니다.
그래서 “GUI를 깨끗하게 구현할 수 있다면 세부 구현은 아무래도 괜찮다” 입니다.
예를 들어 액션게임에서 ‘게임 중에 존재하는 캐릭터의 상태를 화면에 그린다’라는 패턴은 빈번합니다. 이 경우는 게임 중 캐릭터도 GameObject 위에 올려 움직이는 경우가 많겠죠.
이때 MV(R)P패턴이 이루고 싶은 것은 ‘캐릭터의 상태를 GUI에 표시하고 싶다’입니다.

만약 여기서 “Model에 MonoBehaviour를 사용하면 안 된다”라는 제약을 추가해 버리면 “GUI에 정보를 표시 하고 싶을 뿐인데 캐릭터를 MonoBehaviour 비 의존에으로 다시 설계 해야 한다”라는 상황이 되어 버립니다.
“이렇게 하면 이야기가 너무 커져서” “GUI 구현을 깨끗하게 하고 싶다” 라는 목적에서 벗어나 버립니다.
몇 번이나 말할게요. MV(R)P패턴의 목적은 “GUI 구현을 깨끗하게 하는 것”이 목적입니다.
MonoBehaviour의 유무나 무엇이 유니티에 의존하고/하지 않는다는 논의는 말도 안됩니다.
각 객체의 초기화 방법을 모르겠다.
Model, View, Presenter이들 초기화는 고민의 문제입니다.
여러가지 방식은 있지만, 최종적으로 “Presenter가 View 와 Model를 참조하고 있다”라는 형태를 만들 수 있으면 뭐든지 좋습니다.
지금 떠오르는 한에서 3개의 패턴 정도 있으니 참고하세요.
초기화 방법 A: 인스펙터 윈도우 화면 위에서 인스펙터에 할당
Model, View, Presenter가 모두 처음부터 장면에 배치되어 있는 경우에 사용할 수 있는 패턴입니다.
동적으로 생성되거나 삭제되는 일이 없다면 유니티 인스펙터 윈도우상에서 처음부터 연결해 두면 됩니다.

Presenter에 미리 묶어 두는 것이 가장 간단)초기화 방법 B: DIContainer를 사용
방법A와 별로 다르지 않지만 동적으로 오브젝트가 생성/삭제되지 않는다면 DIContainer(Zenject나 VContainer)를 통해 오브젝트를 Presenter에게 전달해 버리는 방법도 있습니다.
초기화 방법 C: 동적으로 바인딩 한다.
이것이 가장 까다로운 패턴입니다. 모델이 나중에 동적으로 추가되거나 삭제되는 것을 어떻게 다루는가입니다.
제가 자주 사용하는 방법은 별도 Dispatcher라는 객체를 배치하고 그것이 Model의 생성/삭제의 이벤트를 구독하는 방법입니다.
예를 들어 다음과 같은 것을 생각해 봅시다.
- Player 체력 바 표시
- Player는 동적으로 나중에 늘어난다.
이런 상황을, Dispatcher를 사용하고 구현해 보도록 하겠습니다.
Model
(위의 Player와 마찬가지 코드)
using System;
using UniRx;
using UnityEngine;
namespace MVRP.Models
{
/// <summary>
/// 액션 게임의 플레이어
/// 「화면에 체력을 나타낸다」라고 하는 관점에서 보면 Model
/// </summary>
public sealed class Player : MonoBehaviour
{
/// <summary>
/// 체력
/// ReactiveProperty로 외부에 상태를 ReadOnly로 공개
/// </summary>
public IReadOnlyReactiveProperty<int> Health => _health;
// 체력 최대치
public readonly int MaxHealth = 100;
private readonly IntReactiveProperty _health = new IntReactiveProperty(100);
private void Start()
{
_health.Value = MaxHealth;
}
/// <summary>
/// 충돌 이벤트
/// </summary>
private void OnCollisionEnter(Collision collision)
{
// Enemy와 충돌 시 체력 감소
if (collision.gameObject.TryGetComponent<Enemy>(out var _))
{
_health.Value -= 10;
}
}
private void OnDestroy()
{
_health.Dispose();
}
}
}
Model을 동적으로 생성하는 Manager
Player를 동적으로 생성하는 Manager가 있고 Player 생성 이벤트를 발행합니다.
using System;
using Cysharp.Threading.Tasks;
using UniRx;
using UnityEngine;
using Random = UnityEngine.Random;
namespace MVRP.Models
{
public sealed class PlayerManager : MonoBehaviour
{
/// <summary>
/// 플레이어 목록
/// ReactiveCollection으로 인해 플레이어 수가 증가하면 알림
/// </summary>
public IReactiveCollection<Player> Players => _players;
private readonly ReactiveCollection<Player> _players = new ReactiveCollection<Player>();
// Player의 Prefab
[SerializeField] private Player _playerPrefab;
private async UniTaskVoid Start()
{
// 초기화 처리
// Player를 시간차로 4개 생성
await UniTask.Delay(TimeSpan.FromSeconds(1));
_players.Add(CreatePlayer(1));
await UniTask.Delay(TimeSpan.FromSeconds(1));
_players.Add(CreatePlayer(2));
await UniTask.Delay(TimeSpan.FromSeconds(1));
_players.Add(CreatePlayer(3));
await UniTask.Delay(TimeSpan.FromSeconds(1));
_players.Add(CreatePlayer(4));
}
private Player CreatePlayer(int id)
{
// Player 생성
var player = Instantiate(_playerPrefab, Vector3.right * Random.Range(1f, 2f), Quaternion.identity);
player.name = $"Player{id}";
return player;
}
}
}
Presenter
Presenter는 인수로 밖에서 Player와 View의 조합을 받아 바인딩할 수 있도록 해 둡니다.
이번에는 단일 Presenter로 복수의 Player와 View를 다루게 합니다.
using MVRP.Models;
using MVRP.Views;
using UniRx;
using UnityEngine;
namespace MVRP.Presenters
{
// Player의 Presenter
public sealed class PlayerPresenter : MonoBehaviour
{
// Player가 생성되면 Bind
public void OnCreatePlayer(Player player, PlayerView view)
{
view.SetName(player.name);
// Player의 Health 모니터링
player.Health
.Subscribe(x =>
{
// View에 반영
view.SetHealth((float)x / player.MaxHealth);
}).AddTo(this);
}
}
}
View
이번에는 View로 ‘이름표시공간’과 ‘체력바’가 있습니다.
이 UI를 다루는 Canvas와 각 요소를 제어하는 컴포넌트를 준비해 Prefab화해 둡니다.
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
namespace MVRP.Views
{
/// <summary>
/// Player 상태 표시 View
/// </summary>
public sealed class PlayerView : MonoBehaviour
{
[SerializeField] private Text _nameplate;
[SerializeField] private Slider _healthSlider;
public void SetName(string name)
{
_nameplate.text = name;
}
public void SetHealth(float value)
{
// 애니메이션하는 동안 슬라이더를 움직입니다.
DOTween.To(() => _healthSlider.value,
n => _healthSlider.value = n,
value,
duration: 1.0f);
}
}
}

Dispatcher
PlayerManager 이벤트를 감지하고 View를 생성하고 Presenter에 바인딩하는 Dispatcher를 준비합니다.
이번에는 이 Dispatcher를 장면에 미리 배치하고 있는데 DIContainer등을 사용해도 무방합니다.
using MVRP.Models;
using MVRP.Presenters;
using MVRP.Views;
using UniRx;
using UnityEngine;
namespace MVRP.Dispatchers
{
/// <summary>
/// Player 생성을 감지하고 View 할당
/// </summary>
public sealed class PlayerDispatcher : MonoBehaviour
{
// Model을 제공하는 Manager
[SerializeField] private PlayerManager _playerManager;
// Player의 Presenter
[SerializeField] private PlayerPresenter _presenter;
// View의 Prefab
[SerializeField] private PlayerView _viewPrefab;
private void Start()
{
// 지금 목록에 있는 Player을 Dispatch
foreach (var p in _playerManager.Players)
{
Dispatch(p);
}
// 이후 신규 작성된 것을 Dispatch
_playerManager.Players.ObserveAdd().Subscribe(x => Dispatch(x.Value)).AddTo(this);
}
private void Dispatch(Player player)
{
// Player의 자식 요소로 View 만들기
var view = Instantiate(_viewPrefab, player.transform, true);
// 위치 조정
view.transform.localPosition = Vector3.up * 1.5f;
// Presenter와 결합하여 알림
_presenter.OnCreatePlayer(player, view);
}
}
}

이번에는 Presenter를 미리 장면에 배치하고 있지만, Presenter도 또한 prefab로 만들어 버려서 때마다 생성하는 형태로 해도 문제 없습니다(어떤 방법을 사용해도 무방합니다)
동작의 모습

( Player이 동적으로 생성되어도 체력 바가 각각 표시되고 각각의 체력이 반영됩니다)
Presenter를 어디에 두면 될까요?
Presenter를 장면의 어디에 배치할 것인가. 아니면 MonoBehaviour를 사용하지 않고 퓨어 클래스로 DIContainer에 관리해야 하는가. 라는 질문입니다.
대답은 자유롭게 해도 된다는 것입니다.
관리할 수 있다면 어디에 Presenter를 배치해도 됩니다. uGUI의 캔버스의 게임 오브젝트에 Presenter를 붙여도 되고, 빈 게임 오브젝트를 만들어 거기에 붙여도 됩니다.
자신이 관리만 할 수 있다면 어디에 어떻게 놓아도 좋습니다.
Presenter와 View의 규모감을 모르겠습니다.
이것은 “하나의 Presenter가 복수의 View를 관리해도 되는가?”, “View 내부에서 상태를 가지고 있어도 되는가?”라는 질문과 같습니다.
Presenter의 규모감
“1개의 Presenter가 어떻게 View를 관리하느냐인데 이것의 답은 “자신이 관리할 수 있다면 자유롭게 해도 된다” 입니다.
- 1개의 게임 오브젝트에 1개의 Presenter만 할당하는 방법
- 1개의 게임 오브젝트에 복수의 Presenter를 할당하는 방법
- 1개의 Presenter로 복수의 모델이나 뷰를 할당하는 방법



(어떻게 해도 무방하다.)
몇 가지 패턴은 들 수 있지만 어떤 것을 사용해도 무방합니다.
View의 규모감
“View 내부에서 상태를 가져도 되는가”, “View를 관리하는 컴포넌트도 View로 취급해도 되는지”이지만
View 자신이 상태를 가지고 View가 자기 자신을 관리하는 것은 MV(R)P패턴적으로는 문제가 없습니다.
예를 들어 다음과 같은 “RGB의 삼색 슬라이더”가 있다고 합시다.

여기에는 “3개의 슬라이더를 관리하는 컴포넌트”가 부착되어 있습니다.
using UniRx;
using UnityEngine;
using UnityEngine.UI;
namespace MVRP.Views
{
/// <summary>
/// RGB 슬라이더를 관리하는 뷰 컴포넌트
/// </summary>
public sealed class ColorSlider : MonoBehaviour
{
/// <summary>
/// 지금의 슬라이더가 가리키는 색
/// </summary>
public readonly ColorReactiveProperty Color
= new ColorReactiveProperty(UnityEngine.Color.white);
[SerializeField] private Slider _red;
[SerializeField] private Slider _green;
[SerializeField] private Slider _blue;
private void Start()
{
Color.Subscribe(c =>
{
_red.value = c.r;
_green.value = c.g;
_blue.value = c.b;
})
.AddTo(this);
// 슬라이더가 어느 하나라도 변화하면 반영
Observable.Merge(
_red.OnValueChangedAsObservable(),
_green.OnValueChangedAsObservable(),
_blue.OnValueChangedAsObservable())
.Subscribe(_ =>
{
Color.Value = new Color(_red.value, _green.value, _blue.value);
})
.AddTo(this);
}
}
}
using MVRP.Models;
using MVRP.Views;
using UniRx;
using UnityEngine;
using UnityEngine.UI;
namespace MVRP.Presenters
{
// Presenter
public class ColorPresenter : MonoBehaviour
{
[SerializeField] private ColorModel _model;
[SerializeField] private ColorSlider _colorSlider;
[SerializeField] private Text _text;
private void Start()
{
// Model -> View
_model.Color.Subscribe(x =>
{
_text.text = x.ToString();
_text.color = x;
_colorSlider.Color.Value = x;
})
.AddTo(this);
// View -> Model
_colorSlider.Color.Subscribe(x =>
{
_model.Color.Value = x;
})
.AddTo(this);
}
}
}
using UniRx;
using UnityEngine;
namespace MVRP.Models
{
// Model
public class ColorModel : MonoBehaviour
{
public readonly ColorReactiveProperty Color
= new ColorReactiveProperty();
}
}
이 값은 View나 Model 중 어느 쪽이 가져야 할 상태인가요?
이 질문은 예컨대 “클릭되면 색이 변경되는 버튼이 있을 때 이 “색” 의 정보는 View와 Model어느 쪽이 가지는 것입니까?” 같은 질문입니다.

답은 “**그 색 정보가 게임의 도메인 요소에 해당하는지 아닌지”**로 결정됩니다.
(‘도메인’이란 ‘게임의 핵심 요소’와 같은 의미라고 생각해 주세요)
그 ‘색 정보”가 게임 성립에 필수불가결한 요소라면 그것은 Model에서 관리해야 될 정보가 됩니다.
한편 게임의 성립에 직접 관계없이 어디까지나 연출의 목적으로 버튼의 색을 바꾸는 정도라면 그것은 View에서 관리할 정보가 됩니다.
예를 들어 “3색 리버시” 등은 색정보가 게임 성립에 절대 필수적인 요소입니다.
그렇기 때문에 3색 리버시를 MV(R)P패턴으로 구축한다면 “색상 정보는 Model이 관리해야 한다”가 됩니다.
- 3색 리버시 = 3색 오셀로 색으로 상대방을 구별한다.
“한편으로 “좀 보기에 풍부한 토글 버튼”이라는 취급이라면 이것은 단지 연출일 뿐이고, 최악의 경우 버튼 색의 변화가 없어도 게임으로서 성립합니다.
이런 경우는 View로 관리해야 된다고 생각 합니다.
Model 요소가 되는 경우
- 그 정보(데이터)가 게임의 성립에 필수 불가결하다.
- 게임의 진행이나 상태 관리에 필수적이다
View 요소가 되는 경우
- 그 정보(데이터)가 없어도 게임으로서 성립된다.
- 있다면 연출 적인 요소가 가미 되겠지만 최악의 경우 없어도 괜찮은 정도.
View→Presenter→Model→Presenter→View라는 값이 루프 하지 않나요?
- View 변화를 검사하고 Presenter가 Model에 반영
- Model의 상태가 변한다
- Model의 상태가 변화하는 것을 감지하고 Presenter가 View에 반영
- View의 상태가 변한다
- View 변화를 감지하고 Presenter가 Model에 반영
- (이하 무한 루프)
라는 현상이 일어나지 않느냐는 질문입니다.
답은 “ReactiveProperty”를 사용하는 경우에 대해서는 루프 하지 않습니다”
ReactiveProperty는 “직전과 같은 값이 오는 경우 이벤트를 발행하지 않는” 구조로 되어 있습니다. 그래서 이벤트가 1번은 와도 2번째 루프에서는 실행되지 않도록 되어 있습니다.
다만 ReactiveProperty.SetValueAndForceNotify()를 쓰는 경우에는 강제 메시지 발행이 실행 되기 때문에 무한 루프를 일으킬 가능성이 있습니다.
Model/View/Presenter마다 Assembly Definition Files를 나누는게 좋나요?
답은 “마음대로 하세요” 입니다.
엄격하게 하여 나누어도 좋고, 귀찮아서 나누지 않아도 됩니다.
이 부분은 정말 마음대로 해도 문제 없습니다.
의존 관계 역전의 법칙을 사용하고 의존 관계를 View → Presenter → Model로 하는게 좋을까요?
참조 관계는 view ← Presenter → Model 이지만 모듈로서의 의존 관계는 View → Presenter → Model이 되는 건가요? 라는 질문입니다.
의미를 모르는 분들은 아래 슬라이드를 참고하세요.
결론은 “복잡하게 되므로 할 필요는 없다”고 나는 생각합니다.
엄밀하게 하고 싶으면 해도 되는데 수고에 비해 이득이 많지 않을 겁니다.
MVP 패턴을 사용하면 Unity 비 의존에서 깨끗하게 만들 수 있다고 들었습니다.
답은 “못할 것은 없으나 그 이전에 생각할 일이 많다” 입니다.
MVP 패턴(MV(R)P패턴)은 원래 GUI용 설계 패턴입니다.
View(GUI)를 어떻게 관리하면 깨끗하게 구현할 수 있느냐는 얘기였습니다.
그러나 이 View라는 개념을 확대 해석하고 View(Unity)와 Model(Unity 비의존)을 나누어 예쁘게 만들기 위한 설계 패턴이라고 착각 하고 있는 사람이 가끔 있습니다.
- MVP 패턴을 사용하면 GameObject를 사용하지 않고 게임 로직이 설계 가능
- MVP 패턴을 사용하면 액션 게임을 깨끗하게 만들 수 있다
- MVP 패턴을 사용하면 설계가 모두 아름다워진다.
이들은 모두 MVP 패턴에 지나치게 기대하여 만들어진 이야기 입니다.
다시 말하지만 “MVP 패턴이 말하는 View란 GUI를 말한다”. GameObject나 MonoBehaviour를 말하는 것이 아닙니다. MVP패턴은 “GUI를 구현하기 위한 패턴”입니다.
“프레임워크(Unity)”와 “도메인(게임 코어 논리)”를 분리한 설계는 확실히 이상적입니다.
하지만 이를 실현하기 위해서는 “MVP패턴”만으로는 부족하고 다른 설계 패턴이나 아키텍쳐의 지식도 필요로 합니다 (예를 들어 최근 화제의 “클린 아키텍쳐”는 바로 “복잡화된 도메인에서 체제 의존을 어떻게 분리하는가”를 말하고 있는 아키텍쳐가 있습니다)
(참고: Unity에서의 설계 레벨을 정의)
그럼 MVP패턴을 확대해석해서 ‘프레임워크와 도메인 분리’에 이용하는 것이 안되는가 하면 딱히 그렇지 않습니다. 상황에 따라서는 MVP패턴을 사용할 수 있는 장면도 있기도 합니다.
예를 들어 ‘서버 사이드에 로직과 데이터가 있다’ ‘Unity는 그 Viewer로 사용한다’처럼 분명하게 구분이 되어 있다면 MVP 패턴을 사용해도 잘 될 가능성이 있습니다.
하지만 한편으로 ‘액션 게임’과 같은 ‘Unity에 밀접하게 의존한 구조 위에서 움직이는 게임’ 등은 애초에 ‘프레임워크와 도메인 분리’ 자체가 어렵기도 합니다. 이런 애초에 분리가 어려운 종류의 것을 억지로 분리해서 만드는 것은 매우 구현이 어려워서 잘 되지 않을 가능성이 높습니다.
그래서 ‘프레임워크와 도메인의 분리’를 하는 경우는 우선 애초에 그것을 하는 의의가 있는지 생각할 필요가 있습니다. “프레임워크와 도메인 분리’는 확실히 설계상 깨끗해질 것 같지만 실제로는 실시 비용이 높고 개발 속도가 늦어진다는 단점이 있습니다.
- 만들려고 하는 게임의 규모감
- 개발자의 인원 수
- 일정
- 게임의 장르
- 시스템 아키텍쳐(클라이언트 만인지 서버와 연계할지 등)
“위 내용을 판단 근거로 ‘애초에 프레임워크와 도메인을 완전히 분리해서 만드는 것에 정말 장점이 있는가”, “어떤 아키텍쳐를 채택할것인가?”를 생각할 필요가 있습니다.
그래서 답은 “(MVP 패턴을 사용하여 Unity 비 의존적인 구현은) 할 수 없는 것은 아니지만 그 이전에 생각할 일이 있다” 입니다
다시 말하지만 “MVP 패턴은 GUI 설계를 위한 패턴”입니다.
이를 응용하고, “프레임워크와 도메인의 분리”에 사용하는 것 자체는 나쁜 것이 없습니다. 하지만 그것을 하기에는 설계에 대한 다른 지식이나 그만한 각오가 필요합니다.
그리고 그것을 초심자에게 요구하는 것은 가혹해서, MVP패턴은 순순히 GUI에만 사용하는 것이 안전하다는 말이 됩니다.
정리
- MV(R)P 패턴은 Unity에서 GUI 구현을 위한 설계 패턴
- Model과 View를 Presenter라는 얇은 레이어로 연결 한다.
- Model과 View의 연결에는 UniRx를 활용 (ReactiveProperty는 편리하다)
- 위 내용 이외에는 자유롭게 사용해도 문제 없다.
- 절대라는 것 없어서 자기만의 방식으로 해석해서 사용해도 무방하다.
- 다만 어디까지나 GUI 설계 패턴이다 라는 것을 염두해 두자.