Priv's Blog
10. Model View Presenter (MVP) 본문
1. 모델 뷰 프리젠터 (MVP: Model View Presenter)
모델 뷰 컨트롤러(MVC: Model View Controller)는 사용자 인터페이스를 개발할 때 흔히 쓰이는 디자인 패턴 중 하나입니다.
MVC의 핵심 아이디어는 소프트웨어의 논리적 부분을 데이터와 프레젠테이션으로부터 분리하는 것입니다. 이는 불필요한 의존성을 줄이는 데 도움을 주고, 스파게티 코드가 탄생할 잠재적인 위험을 예방할 수 있습니다.
2. 모델 뷰 컨트롤러 디자인 패턴 (MVC: Model View Controller)
이름에서 유추할 수 있듯이, MVC 패턴은 여러분의 애플리케이션을 세 가지 레이어로 분할합니다:
- 모델 저장 데이터: 모델(Model)은 엄격하게 말하자면 값들을 가지고 있는 데이터 컨테이너를 의미합니다. 이는 게임 플레이 로직 또는 연산은 수행하지 않습니다.
- 뷰는 인터페이스입니다: 뷰(View)는 화면상에 표시되는 데이터의 그래픽 형식을 지정하고 렌더링합니다.
- 컨트롤러는 로직을 제어합니다: 뇌를 한번 생각해 봅시다. 이는 게임이 실행되는 동안 게임 데이터를 처리하고, 게임이 실행되는 동안 값이 어떻게 바뀌는지 연산합니다.
이와 같은 관심사(기능)의 분리는 어떻게 이 세 파트가 다른 파트와 상호작용할 것인지 또한 명확하게 정의합니다. 모델은 뷰가 사용자에게 데이터를 표시해 주는 동안, 애플리케이션의 데이터를 관리합니다. 컨트롤러는 입력을 처리하고, 게임 데이터에 대한 어떠한 결정 또는 연산을 처리합니다. 그런 다음, 연산된 결과는 다시 모델로 반환됩니다.
그러므로, 컨트롤러는 어떠한 게임 데이터도 가지고 있지 않습니다. 뷰 또한 마찬가지죠. MVC 패턴은 각 레이어가 어떠한 행동을 할 수 있는지 제한 범위를 설계합니다. 한 파트는 데이터를 저장만 하고, 다른 파트는 데이터를 연산만 하고, 마지막 파트는 데이터를 사용자에게 보여줄 뿐이죠.
표면적으로 보면, 단일 책임 원칙의 확장판처럼 생각해 볼 수 있습니다. 각각의 파트는 자기가 맡은 작업만 잘 수행하면 된다는 것이 MVC 아키텍처의 장점 중 하나입니다.
3. MVP 패턴과 Unity 엔진
MVC 패턴을 사용해 Unity 엔진으로 프로젝트를 개발할 때를 보면, 기존의 UI 프레임워크(UI Toolkit 또는 Unity UI)가 자연스레 뷰의 역할을 수행합니다. 이는 Unity 엔진이 여러분에게 사용자 인터페이스 구현이 끝난 완성본을 제공해 주기 때문입니다. 이 덕분에 여러분은 개별적인 UI 컴포넌트를 처음부터 다시 개발할 필요가 없습니다.
그러나, 전통적인 MVC 패턴을 따르게 되면 모델이 가지고 있는 데이터가 게임이 실행되는 동안 변경되는 것을 감지할 수 있는 뷰 특정 코드(View-specific code)가 필요합니다.
이는 유효한 접근 방식이지만, 많은 Unity 개발자 분은 컨트롤러가 중간에 배치되도록 변형한 MVC 패턴을 사용하는 경우가 많습니다. 여기서는 뷰가 직접 모델을 관찰하지 않습니다. 그 대신, 다음과 같은 방식으로 동작합니다.
이 변형된 MVC는 모델 뷰 프레젠터 디자인 또는 MVP라고 부릅니다. MVP 또한 세 가지로 분할된 애플리케이션 레이어 간의 관심사 분리를 유지하고 있습니다. 그러나, 각 파트의 책임을 일부 수정하였습니다.
MVP 패턴에서 프레젠터(Presenter)는 (MVC에서는 컨트롤러)다른 레이어 간의 중계자 역할을 수행합니다. 이는 모델에서 데이터를 검색하고 뷰가 데이터를 표시할 형식을 지정합니다. MVP는 어떤 레이어가 입력을 제어할 것인지를 조정할 수 있습니다. 컨트롤러 대신, 뷰가 사용자의 입력을 처리합니다.
여기서 주목할 점은 어떻게 이벤트들과 감시자 패턴이 이 디자인 패턴에 적용되었는가입니다. 사용자는 Unity 엔진의 UI 버튼, 토클, 슬라이더 컴포넌트 등을 사용하여 상호작용합니다. 뷰 레이어는 UI 이벤트를 통해 입력을 프레젠터에게 전달하고, 프레젠터는 모델을 조작합니다. 모델에서 상태 변화 이벤트가 발생하면 데이터가 업데이트되었음을 프레젠터에게 알려줍니다. 그러면 프레젠터는 수정된 데이터를 뷰에게 전달하여 UI가 갱신됩니다.
4. 예제: 체력 인터페이스
MVP 예제를 만들기 위해 캐릭터 또는 아이템의 체력을 보여주는 간단한 시스템을 상상해 보세요. 데이터, UI가 모두 뒤섞인 하나의 클래스를 만들 수도 있지만, 이는 확장성이 떨어집니다. 더 많은 기능이 추가된다면 확장이 필요하기 때문에 그만큼 코드가 더 복잡해지기 마련입니다.
그 대신, health 컴포넌트의 코드를 MVP 패턴을 중심으로 다시 작성할 수도 있습니다. 작성된 스크립트를 Health와 HealthPresenter로 분리하세요. 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 => current- Health = value; }
public int MinHealth => minHealth;
public int MaxHealth => maxHealth;
public void Increment(int amount)
{
currentHealth += amount;
currentHealth = Mathf.Clamp(currentHealth, minHealth, max- Health);
UpdateHealth();
}
public void Decrement(int amount)
{
currentHealth -= amount;
currentHealth = Mathf.Clamp(currentHealth, minHealth, max- Health);
UpdateHealth();
}
public void Restore()
{
currentHealth = maxHealth;
UpdateHealth();
}
public void UpdateHealth()
{
HealthChanged?.Invoke();
}
}
위 버전의 코드에서 Health는 모델로써 동작합니다. 이는 실제 체력 값을 저장하고, 매 순간 값이 변할 때마다 HealthChanged 이벤트를 깨웁니다(invoke). Health는 게임 플레이에 관한 로직은 포함하고 있지 않으며, 단순히 데이터의 증감을 다루는 메서드만 존재합니다.
그러나, 대부분의 오브젝트는 Health 그 자체를 조작하지 않습니다. 이러한 작업을 수행하기 위해 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를 가진 사용자 인터페이스가 업데이트되기를 기다립니다. 이는 모델에서 값을 설정하는 시간이 짧은 경우(예: 디스크 또는 데이터베이스에 값을 저장하는 경우)에 유용합니다.
샘플 프로젝트에서는 사용자가 타깃 오브젝트를 클릭하여 대미지를 주거나, 초기화 버튼을 눌러 체력을 초기화할 수 있습니다. 이는 Health에 직접 접근하여 값을 수정하는 것 대신, (Damage 메서드 또는 Reset 메서드를 깨우는) HealthPresenter를 사용합니다. UI 텍스트와 UI 슬라이더는 Health가 이벤트를 일으켜서 HealthPresenter에 값이 변경되었음을 알릴 때 업데이트됩니다.
5. MVP 패턴의 장단점
MVP(그리고 MVC) 패턴은 대규모 애플리케이션을 개발할 때 매우 유용합니다. 만약 여러분의 게임을 개발하는데 상당한 규모의 팀이 요구되고, 출시 이후로 오랫동안 유지보수를 해야 할 것으로 예상된다면 아래의 장점들을 참고해 보세요:
- 유연한 분할 작업 가능: 뷰를 프레젠터와 분리하였기 때문에 유저 인터페이스를 다른 코드 베이스와 독립적으로 개발 및 업데이트할 수 있습니다.
이는 여러분의 개발팀을 전문화된 소규모 그룹으로 나눌 수 있게 해줍니다. 프론트 엔드 개발을 전문으로 하는 개발자분들이 팀 내에 있나요? 그렇다면 그분들에게 뷰를 담당하도록 지시하세요. 그러면 다른 사람들의 업무와 독립된 환경에서 작업을 수행할 수 있습니다.
- MVP, MVC 패턴을 사용하면 유닛 테스트를 간단하게 수행할 수 있습니다: 이 디자인 패턴은 게임 플레이 로직을 사용자 인터페이스와 분리합니다. 그러므로, 일일이 Unity 엔진의 에디터 상에서 플레이 모드에 진입하지 않고도 여러분의 코드를 통해 동작할 오브젝트들을 시뮬레이션 할 수 있습니다. 이는 상당한 양의 시간을 절약할 수 있게 해줍니다.
- 유지보수가 가능한 가독성 있는 코드: 여러분은 이 디자인 패턴을 통해 가독성을 높일 수 있도록 더 작은 크기의 클래스를 만드실 것입니다. 더 적은 의존성은 일반적으로 소프트웨어의 안정성을 높이며, 숨겨진 버그가 나타날 가능성도 줄여줍니다.
MVC와 MVP 패턴은 웹 개발과 전사적 소프트웨어(Enterprize software)를 개발할 때 널리 사용되지만, 종종 애플리케이션이 충분한 크지 않거나, 일정 수준 이상의 복잡성에 도달하기 전까지는 이점이 드러나지 않을 것입니다. 그러므로 이 패턴을 Unity 프로젝트에 적용하기 전에 여러분은 아래의 요소들을 먼저 고려해야 합니다:
- 계획이 우선입니다: 이 책에서 소개된 다른 패턴들과 달리, MVC와 MVP 패턴은 규모가 큰 아키텍처 패턴입니다. 이 중의 하나를 사용하고자 한다면, 여러분의 클래스들을 의존성에 따라 분리해야 합니다. 이는 기존보다 더 많은 인원과 선행 작업을 요구합니다.
- 모든 Unity 프로젝트가 이 패턴에 적합한 것은 아닙니다: ‘순수한’ MVC 또는 MVP 패턴의 구현에서 화면상에 렌더링 되는 모든 것들은뷰의 일부분에 해당합니다. 모든 Unity 컴포넌트가 간단하게 데이터, 로직, 인터페이스로 분리되는 것은 아닙니다. (예: MeshRenderer) 또한, 간단한 수준의 스크립트들은 MVC/MVP 패턴의 많은 이점을 얻지 못할 수 있습니다.
어디에 이 패턴을 적용해야 가장 큰 이득을 얻을 수 있는지 판단하는 것은 연습이 필요합니다. 일반적으로, 유닛 테스트를 통해 연습해 볼 수있습니다. 만약 MVC/MVP 패턴이 테스트를 용이하게 해줄 수 있다면, 애플리케이션에 이를 적용하는 것을 고려해 보세요. 그렇지 않다면, 굳이 여러분의 프로젝트에 이 패턴을 억지로 적용하려고 하지 마세요.
수고하셨습니다!
'Unity Learn > Game Programming Patterns' 카테고리의 다른 글
11. Conclusion (0) | 2023.12.01 |
---|---|
9. Observer pattern (0) | 2023.11.23 |
8. State pattern (0) | 2023.11.20 |
7. Command pattern (0) | 2023.11.12 |
6. Singleton pattern (0) | 2023.10.31 |