Unity에 맞춘 MVP 아키텍처
Source: Dev.to
Note: 이것은 엄격한 MVP 핸드북이 아니며, 클래식 MVP를 Unity에 일대일로 매핑한 완벽한 버전도 아닙니다. 여기서 보게 될 것은 우리 게임에 잘 맞는 적용 방식으로, 많은 메뉴와 다수의 미니게임이 있는 앱에 가깝습니다. 여러분의 프로젝트에 완벽히 맞을 수도, 아닐 수도 있지만, 유용한 아이디어를 제공하길 바랍니다.
제품
“모든 것을 지배하는 하나의 아키텍처”는 존재하지 않는다.
우리 앱 Magrid는 3 – 10세 어린이를 위한 학습 솔루션입니다(아이를 키우고 있거나 혹은 5세라면 한 번 확인해 보세요). 두 가지 주요 목표를 달성하도록 설계되었습니다:
- 언어‑프리 – 전 세계 어린이들이 사용할 수 있습니다.
- 포용성 – 장애가 있는 어린이도 지원합니다.
기술적 관점
- 다양한 메뉴(로그인/회원가입, 학생 관리, 성과 검토, 교육 과정 관리 등)
- 다양한 미니게임(패턴 인식, 시각 지각, 숫자 비교 등)
주요 기술적 관심사는 관심사의 분리였으며, 이는 메뉴 로직과 미니게임 모두에 적용됩니다.
Source: …
아키텍처 개요
아키텍처에는 두 종류의 엔터티가 있습니다:
- MVP 모듈
- 시스템
MVP 모듈
MVP 모듈은 세 가지 구성 요소로 이루어집니다:
| 구성 요소 | 역할 |
|---|---|
View (MonoBehaviour 상속) | 사용자 입력을 처리하고, 데이터를 표시하며, Presenter와 직접 통신 |
| Presenter (일반 C# 클래스) | 모든 비즈니스 로직을 포함하고, Model에서 데이터를 가져와 인터페이스를 통해 View를 업데이트 |
| Model (일반 C# 클래스) | 모듈에 필요한 데이터를 저장, 명확성을 위해 분리 |
흐름
- View가 MVP 모듈의 진입점입니다.
- View가 자신의 Presenter(s)를 생성합니다.
- 재사용성을 위해 하나의 View가 여러 Presenter를 가질 수 있습니다.
- View는 자신의 인터페이스를 Presenter에 전달합니다.
- Presenter가 필요에 따라 Model을 생성합니다.
- 모듈이 준비됩니다.
모든 비즈니스 로직은 Presenter에 존재합니다. View를 커맨드‑라인 구현으로 교체한다 해도, Presenter가 Unity API를 전혀 사용하지 않고 인터페이스만 통해 통신하기 때문에 애플리케이션은 정상적으로 동작합니다.
간단한 코드 예시
// View ---------------------------------------------------------
public class FooView : BaseScreen, IFooView
{
[SerializeField] private TMP_Text exposedInExpectorExample;
private FooPresenter _presenter;
private void Start()
{
_presenter = new FooPresenter(this);
}
public void SetName(string barName)
{
exposedInExpectorExample.text = barName;
}
}
// View interface ------------------------------------------------
public interface IFooView : BaseMvpView
{
void SetName(string barName);
}
// Presenter ----------------------------------------------------
public class FooPresenter : BaseMvpPresenter
{
private readonly IFooView _view;
private readonly FooModel _model;
public FooPresenter(IFooView view)
{
_view = view;
_model = new FooModel();
}
}
// Model ---------------------------------------------------------
public class FooModel : BaseMvpModel
{
public string BarName { get; set; }
}
설명
BaseScreen(View의 기본 클래스)는 캔버스 관리(Open,Close, …)를 담당하는MonoBehaviour입니다.BaseMvpView는 뷰 인터페이스가 상속하는 일반 인터페이스입니다:
public interface BaseMvpView where T : BaseMvpPresenter { }
BaseMvpModel은 간단한 인터페이스입니다:
public interface BaseMvpModel
{
int Version { get; }
}
Version 필드는 업데이트 후 영구 데이터를 로드할 때 데이터 마이그레이션을 돕습니다. 각 모듈은 자체 마이그레이션 로직을 관리할 수 있습니다.
시스템
시스템은 View가 없는 MVP 모듈이라고 볼 수 있습니다. 이들의 목적은 모든 MVP 모듈에 서비스를 제공하는 것입니다.
- 모든 시스템은 AppManager 라는 단일
MonoBehaviour에 의해 인스턴스화되며, MVP에서 접근 가능한 Unity‑전용 싱글톤입니다. - Zenject와 IL2CPP 문제 때문에 우리는 간단한 의존성 관리자를 직접 구현했습니다.
특성
- 각 시스템은 인터페이스를 노출합니다.
- 모든 시스템은 테스트에서 모킹할 수 있습니다.
- 시스템은 앱 로딩 중에 초기화됩니다.
예시 시스템
| 시스템 | 역할 |
|---|---|
| Profile System | 영구 데이터 저장 |
| API System | 서버 통신 |
| Purchase System | 인앱 구매 |
| Analytics System | 분석 제공자 및 이벤트 (GameAnalytics, Firebase, …) |
| UI System | 화면, 팝업, 네비게이션, 뒤로 가기 동작 |
PlayInfo – 중재자
PlayInfo는 MVP 모듈이 통신해야 하는 드문 경우를 처리합니다. 포함 내용은 다음과 같습니다:
- 모듈이 구독할 수 있는 이벤트 집합.
- 몇 가지 공유 변수.
원래는 모든 것이 정적 메서드를 통해 서로에 접근하던 레거시 코드베이스에서 전환을 돕기 위해 주로 존재했습니다. PlayInfo는 리팩터링된 MVP 모듈과 레거시 코드 사이의 중재자 역할을 했습니다. 결국 몇몇 이벤트가 실제로 유용하다는 것이 밝혀져 남겨두었습니다.
테스트 전략
우리는 두 가지 유형의 테스트를 사용합니다:
- Unit tests – 프레젠터와 시스템을 다룹니다. 최소한의 의존성을 가지고 있어 쉽게 모킹할 수 있습니다.
- Integration / UI tests – 사용자 상호작용을 시뮬레이션하고 여러 모듈을 함께 테스트합니다. 사용자 상호작용을 시뮬레이션하고 UI를 확인하는 것은
UnityTest와 리플렉션을 사용하여 수행할 수 있습니다(별도의 글이 필요할 정도입니다).
격리
- 각 모듈은 interfaces와 (드물게) events를 통해 완전히 격리됩니다.
- 이는 리팩토링/변경을 더 안전하게 만들고 의도치 않은 부작용을 줄여줍니다.
전형적인 모듈 구성
- View → 뷰
- View Interface → 뷰 인터페이스
- Presenter → 프레젠터
- Model → 모델
Unity 엣지 케이스
결국 이것은 여전히 게임입니다. 때때로 Coroutines(코루틴)이나 Physics(물리)와 같은 Unity API가 필요합니다.
- 코루틴을 시작하려면
AppManager(MonoBehaviour 싱글톤)를 사용합니다.
// Example: starting a coroutine from a presenter
public class FooPresenter : BaseMvpPresenter
{
private readonly IFooView _view;
private readonly FooModel _model;
private readonly AppManager _appManager;
public FooPresenter(IFooView view, AppManager appManager)
{
_view = view;
_model = new FooModel();
_appManager = appManager;
}
public void DoSomethingAsync()
{
_appManager.StartCoroutine(DoWork());
}
private IEnumerator DoWork()
{
// Unity‑specific logic here
yield return new WaitForSeconds(1f);
_view.SetName("Done");
}
}
다른 Unity‑특화 요구사항(물리, 애니메이션 이벤트 등)도 동일하게 처리됩니다: Presenter가 호출을 시스템이나 AppManager에 위임하여 Presenter 자체가 직접 Unity 의존성을 갖지 않도록 합니다.
TL;DR
- MVP는 테스트 가능하고 결합도가 낮은 UI 로직을 제공합니다.
- Systems는 View 레이어를 오염시키지 않으면서 공유 서비스를 제공합니다.
- PlayInfo는 레거시 방식 통신을 위한 경량 중재자입니다.
- AppManager는 실제로
MonoBehaviour가 필요한 모든 것을 위한 단일 Unity 진입점입니다.
아이디어를 여러분의 프로젝트에 자유롭게 적용하세요 – 목표는 관심사를 분리하고 테스트를 용이하게 하며, 필요할 때 Unity의 강력한 런타임 기능을 활용할 수 있도록 하는 것입니다.
MVP 아키텍처에서 중복 방지
These cases were rare for us. Although I’ll take maintainability over reusability any day, repeating yourself still hurts.
우리가 해결한 방법
-
일반 UI 컴포넌트 만들기
예시: 여러 뷰에서 재사용되는 학생 프로필 UI 요소.- 동일한 UI 코드를 반복할 필요가 없습니다.
- 컴포넌트(또는 프리팹)를 한 번 변경하면 이를 사용하는 모든 화면이 업데이트됩니다.
-
상태 없는 로직을 유틸리티 클래스에 넣기
예시: 어떤 모듈에서도 호출할 수 있는 이메일 검증 정규식. -
뷰당 여러 프레젠터를 사용하고 적절한 인터페이스를 주입하기 (인터페이스 분리)
예시: “고객 지원 연락” 로직이 이미 설정에 존재합니다. 코드를 중복하지 말고 해당 프레젠터를 상점 화면에서 재사용하세요. -
위의 두 경우에 맞지 않는 상황은 시스템으로 다루기
MVP 모듈이 사용할 수 있는 서비스를 제공하세요.
왜 MVP(또는 MVVM)가 Unity 프로젝트에 적합한가
Even though these patterns weren’t originally designed for games, they shine in menu‑heavy, logic‑driven Unity projects. Adopting MVP gave us:
- 테스트 용이성 – 프레젠터를 독립적으로 단위 테스트할 수 있습니다.
- 관심사의 분리 – UI, 비즈니스 로직, 데이터가 명확히 구분됩니다.
- 유지보수성 – 한 레이어의 변경이 다른 레이어에 거의 영향을 주지 않습니다.
피드백이 있나요?
If you have questions or a better idea to improve this approach, let me know. :D