게임에서 디자인 패턴을 쓰면 좋다는 것은 대부분 아는 사실일 겁니다. 하지만 프로젝트의 상황에 맞게 사용하지 않으면 득보다는 실이 많을 수 있는 것도 사실입니다. 이번 시간에는 게임에서 자주 사용하는 MVC, MVP, MVVM패턴을 알아보고, 각 패턴을 비교 하고 어떤 상황에 써야 하는지 말씀드리겠습니다. 또한 각 패턴의 예시 코드를 작성하여 실제로 게임에는 어떻게 사용되는지 까지 알아보겠습니다.
목차
[MVC 패턴]
[무엇인가?]
- Model: 데이터와 데이터의 처리, 저장 등을 관리하는 곳입니다.
- 예시로는 캐릭터의 atk, speed등을 가지고 있는 CharacterData와 캐릭터가 장비를 장착 했을 때 데이터가 변경되는 것을 저장하고 서버에 보내주는 것까지 담당하는 것을 모델이라고 할 수 있습니다.
- View: UI를 통한 입력을 처리하는 곳 입니다. 처리 된 입력은 Controller에게 전달 합니다.
- 예시로는 게임에서 상점 npc를 클릭 했을 때 뜨는 UI가 있습니다.
- Controller: 뷰와 모델의 상호작용을 관리하는 곳입니다. 모델과 뷰가 커플링 될만한 부분을 컨트롤러 에서 모두 처리한다고 보면 됩니다. 또한 콘텐츠의 핵심 로직을 구성하고 있는 곳 이기도 합니다.
[예시 코드]
- 캐릭터와 상점 UI가 상호작용 하는 부분을 예시 코드로 작성해 보겠습니다.
// Model
public class Item
{
public string Name { get; set; }
public int Price { get; set; }
}
// View
public class ShopView : MonoBehaviour
{
public Text itemText;
public Text priceText;
public Button buyButton;
public void UpdateUI(Item item)
{
itemText.text = item.Name;
priceText.text = item.Price.ToString();
}
}
// Controller
public class ShopController : MonoBehaviour
{
public ShopView shopView;
public Item currentItem;
// 상점 초기화 메서드
public void InitializeShop()
{
// 데이터베이스나 다른 소스에서 아이템 데이터를 로드합니다.
currentItem = new Item { Name = "검", Price = 100 };
// 현재 아이템으로 UI 업데이트
shopView.UpdateUI(currentItem);
}
// 아이템 구매 버튼 클릭 시 호출되는 메서드
public void BuyItem()
{
// 플레이어의 통화량을 확인합니다.
int playerCurrency = GetPlayerCurrency(); // 플레이어의 통화량을 가져오는 메서드
if (playerCurrency >= currentItem.Price)
{
// 아이템 가격을 플레이어의 통화량에서 차감합니다.
playerCurrency -= currentItem.Price;
UpdatePlayerCurrency(playerCurrency); // 플레이어의 통화량을 업데이트하는 메서드
// 아이템을 플레이어의 인벤토리에 추가합니다.
AddItemToInventory(currentItem); // 아이템을 플레이어의 인벤토리에 추가하는 메서드
// 구매 성공 메시지 표시 또는 다른 작업 수행
Debug.Log("아이템을 성공적으로 구매했습니다!");
}
else
{
// 에러 메시지 표시 또는 다른 작업 수행
Debug.Log("통화량이 부족하여 아이템을 구매할 수 없습니다!");
}
}
}
- Item 클래스는 구매가능한 아이템을 나타냅니다.
- ShopView 클래스는 아이템 세부정보를 표현하는 역할을 합니다.
- ShopController클래스는 아이템 구매와 관련된 로직을 처리하는 부분입니다. 또한 UI의 Update를 하는 부분이기도 합니다.
[장점]
- 각 역할이 나누어져 있어서 확장성과 유지 보수성이 뛰어납니다.
- 만약 뷰 클래스를 바꾸고 싶다면 서로 역할이 나누어져 있어서 쉽게 바꿀 수 있습니다.
- 3개의 역할이 한 클래스에서 담당한다면 UI 부분만 변경하기는 쉽지 않습니다.
- 테스트의 용이성
- 다른 데이터로 게임을 테스트 하고 싶을 때 모델 부분만 바꿔 주면 돼서 테스트하기가 쉽습니다.
[단점]
- 복잡합니다.
- 초반 설계를 잘 하지 않으면 모델, 뷰, 컨트롤러의 역할이 명확하지 않게 되어 오히려 코드의 복잡성이 증가하여 생산성이 떨어질 우려가 있습니다.
- 소규모 프로젝트의 오남용
- 소규모 프로젝트에서도 상황에 맞게 사용하면 개발 시간을 대폭 줄일 수 있습니다 하지만 깊이 이해 하지 않고 사용하면 오히려 품질과 생산성 둘 다 떨어지는 코드를 작성할 우려가 있습니다.
- 파일 분할이 많아 집니다.
- 하나의 콘텐츠를 만들기 위해서는 최소 3개의 클래스가 필요합니다. 대규모 프로젝트에서는 병렬 작업이 용이하지만 소규모 프로젝트에서는 오히려 파일만 많아지고 관리하기가 어려워지는 상황이 발생 할 수 있습니다.
- 일관성을 유지하는 것에 대한 비용
- MVC라는 일관성을 유지 해야 하기 때문에 예기치 못한 상황에 대처하기 위해서 오히려 시간을 뺏기는 경우가 있습니다. 시간을 단축하기 위해서 썼지만 오히려 시간을 뺏기는 경우가 되는 셈입니다.
[MVP 패턴]
[무엇인가?]
- Model
- 데이터를 관리하고 처리하는 역할 입니다.
- 데이터의 변경 사항을 View 및 Presenter에 통지합니다
- View
- UI에서 입력을 받아 처리하고 Presenter에 전달합니다.
- 데이터가 변경되면 Presenter에 통지합니다.
- Presenter
- Model과 View 간의 중계자 역할을 합니다.
- View에서 발생한 이벤트를 수신하고, 필요한 데이터를 Model에서 가져와 View에 전달합니다.
- View와 Model 사이의 의존성을 없애고, 재사용 가능한 코드를 작성할 수 있도록 도와줍니다.
[MVC패턴과 차이점]
- MVC 패턴이 모델과 뷰의 간접적인 참조를 허용했다면 MVP의 Presenter는 간접적인 참조마저 허용하지 않는 것이 가장 큰 차이 입니다.
[예시 코드]
// Model
public class Item
{
public string Name { get; set; }
public int Price { get; set; }
}
// View
public interface IShopView
{
void UpdateUI(Item item);
}
public class ShopView : MonoBehaviour, IShopView
{
public Text itemText;
public Text priceText;
public Button buyButton;
private ShopPresenter shopPresenter;
private void Awake()
{
shopPresenter = new ShopPresenter(this);
}
public void UpdateUI(Item item)
{
itemText.text = item.Name;
priceText.text = item.Price.ToString();
}
public void OnBuyButtonClick()
{
shopPresenter.BuyItem();
}
}
// Presenter
public class ShopPresenter
{
private IShopView shopView;
private Item currentItem;
private List<Item> playerInventory = new List<Item>();
public ShopPresenter(IShopView view)
{
shopView = view;
}
// 상점 초기화 메서드
public void InitializeShop()
{
// 데이터베이스나 다른 소스에서 아이템 데이터를 로드합니다.
currentItem = new Item { Name = "검", Price = 100 };
// 현재 아이템으로 UI 업데이트
shopView.UpdateUI(currentItem);
}
// 아이템 구매 메서드
public void BuyItem()
{
// 플레이어의 통화량을 확인합니다.
int playerCurrency = GetPlayerCurrency(); // 플레이어의 통화량을 가져오는 메서드
if (playerCurrency >= currentItem.Price)
{
// 아이템 가격을 플레이어의 통화량에서 차감합니다.
playerCurrency -= currentItem.Price;
UpdatePlayerCurrency(playerCurrency); // 플레이어의 통화량을 업데이트하는 메서드
// 아이템을 플레이어의 인벤토리에 추가합니다.
AddItemToInventory(currentItem); // 아이템을 플레이어의 인벤토리에 추가하는 메서드
// 구매 성공 메시지 표시 또는 다른 작업 수행
Debug.Log("아이템을 성공적으로 구매했습니다!");
}
else
{
// 에러 메시지 표시 또는 다른 작업 수행
Debug.Log("통화량이 부족하여 아이템을 구매할 수 없습니다!");
}
}
private int GetPlayerCurrency()
{
// 플레이어의 통화량을 가져오는 로직 구현
return 0;
}
private void UpdatePlayerCurrency(int currency)
{
// 플레이어의 통화량을 업데이트하는 로직 구현
}
private void AddItemToInventory(Item item)
{
playerInventory.Add(item);
}
}
- 코드를 보시면 MVC의 View에서는 Item을 파라미터로 받았지만 presenter가 생김으로 인해서 view에서 Item을 간접 참조하는 부분이 사라졌습니다.
[장점]
- Presenter가 생김으로 인해서 MVC 패턴보다 모델과 뷰의 결합이 느슨해졌습니다.
- 유지 보수, 확장성이 MVC 패턴보다 더욱 좋다는 뜻입니다.
- Model과 View의 참조가 사라짐으로써 각 클래스 교체가 더욱 쉬워졌습니다.
[단점]
- MVC패턴 보다 더 복잡하여 오 남용될 가능성이 더욱 높습니다.
- MVC패턴 보다 학습하기가 어렵습니다.
[MVVM 패턴]
[무엇인가?]
- Model
- 데이터 및 데이터 처리를 담당 합니다.
- View
- 데이터를 통해 정보를 표시하고 UI에서 입력을 받는 역할을 합니다.
- ViewModel
- View와 Model 사이의 매개체 역할을 합니다.
- 사용자 인터페이스에 표시되는 데이터를 포함하고, View에 대한 이벤트 및 명령 처리 로직을 구현합니다.
- Model에서 가져온 데이터를 가공하거나 필요한 형식으로 변환하여 View에 제공합니다.
- View와의 양방향 데이터 바인딩을 통해 상호 작용하며, View의 상태를 변경하고 사용자 입력을 처리합니다.
- 데이터 바인딩을 통해 View와 ViewModel이 동기화되어 데이터의 변경 사항이 자동으로 반영되고, 사용자 입력은 ViewModel로 전달됩니다.
[예시 코드]
// Model
public class Item : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set
{
if (name != value)
{
name = value;
OnPropertyChanged(nameof(Name));
}
}
}
private int price;
public int Price
{
get { return price; }
set
{
if (price != value)
{
price = value;
OnPropertyChanged(nameof(Price));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// View
public class ShopView : MonoBehaviour
{
public Text itemText;
public Text priceText;
public Button buyButton;
private Item currentItem;
public void Initialize(Item item)
{
currentItem = item;
UpdateUI();
}
private void UpdateUI()
{
itemText.text = currentItem.Name;
priceText.text = currentItem.Price.ToString();
}
public void BuyItem()
{
// 플레이어의 통화량을 확인합니다.
int playerCurrency = GetPlayerCurrency(); // 플레이어의 통화량을 가져오는 메서드
if (playerCurrency >= currentItem.Price)
{
// 아이템 가격을 플레이어의 통화량에서 차감합니다.
playerCurrency -= currentItem.Price;
UpdatePlayerCurrency(playerCurrency); // 플레이어의 통화량을 업데이트하는 메서드
// 아이템을 플레이어의 인벤토리에 추가합니다.
AddItemToInventory(currentItem); // 아이템을 플레이어의 인벤토리에 추가하는 메서드
// 구매 성공 메시지 표시 또는 다른 작업 수행
Debug.Log("아이템을 성공적으로 구매했습니다!");
}
else
{
// 에러 메시지 표시 또는 다른 작업 수행
Debug.Log("통화량이 부족하여 아이템을 구매할 수 없습니다!");
}
}
}
// ViewModel
public class ShopViewModel : MonoBehaviour
{
public ShopView shopView;
public Item currentItem;
// 상점 초기화 메서드
public void InitializeShop()
{
// 데이터베이스나 다른 소스에서 아이템 데이터를 로드합니다.
currentItem = new Item { Name = "검", Price = 100 };
// View에 ViewModel 설정
shopView.Initialize(currentItem);
}
}
- Model (
Item
클래스):Item
클래스는 아이템의 속성을 나타냅니다.Name
과Price
는 아이템의 이름과 가격을 나타내는 속성입니다.INotifyPropertyChanged
인터페이스를 구현하여 속성 값이 변경될 때 이벤트를 발생시킵니다. - View (
ShopView
클래스): 상점을 나타내는 UI 요소들을 관리합니다. - ViewModel (
ShopViewModel
클래스):InitializeShop
메서드는 상점을 초기화하고, 데이터베이스나 다른 소스에서 아이템 데이터를 가져와 현재 아이템을 설정합니다. 그리고shopView
의Initialize
메서드를 호출하여 View에 ViewModel을 설정합니다. - Item의 데이터 바인딩 부분이 가장 큰 차이점 입니다.
[장점]
- ViewModel은 UI와 독립적으로 동작하므로, 여러 개의 View에서 재사용할 수 있습니다. 이는 생산성을 높이고 코드 중복을 줄이는 데 도움을 줍니다.
- Model과 View 사이의 데이터 동기화를 자동화합니다. 이로 인해 UI 업데이트가 간편해지고 개발 시간을 단축시킬 수 있습니다.
[단점]
- 데이터 바인딩이라는 개념으로 인해서 가장 학습하기 어렵습니다.
- 데이터 바인딩으로 인해서 UI 업데이트가 자동화 되지만 자동화하는 과정까지 시간이 꽤 걸립니다.
[상황에 따른 패턴 사용]
각 패턴을 결합도에 따라서 분리한다면 MVC -> MVP -> MVVM순으로 MVVM이 가장 결합도가 느슨합니다. MVVM쪽으로 갈 수록 설계비용이 많이 든다는 뜻입니다. 대신 결합도가 느슨하다는 것은 확장성과 유지 보수성이 높다는 뜻도 됩니다. 그렇기에 프로젝트의 규모가 크고 인원이 많을수록 MVVM이나 MVP를 선택하는 것이 좋습니다.
만약 3패턴을 처음 접하신다면 상황에 따라서 세 패턴을 선택해 사용하는 것이 어려우실 수 있습니다. 그러므로 제가 작성한 예시 코드를 보시고 토이 프로젝트를 만드셔서 직접 사용해보시면 언제 사용해야 할지 감이 잡히실 겁니다.
중요시 하셔야 할 부분은 위 패턴들은 결국 생산성과 버그를 줄이기 위해서 사용하기 위함 입니다. 사용하시다 보면 패턴을 사용하기만 급급하여 원래 목표를 망각하는 경우가 있습니다. 각 패턴은 도구일 뿐이며 원래 목표를 잊지 않으셔야 합니다.
[MVC, MVP, MVVM 패턴과 같이 쓰면 좋은 패턴 소개]
[참고 사이트]
그놈의 MVVM 패턴 EP 01 – MVC 패턴 살펴보자 – YouTube
안녕하세요, 몇달 지났지만 질문드려봅니다.
UI와 동작 클래스를 어떻게 분리할지 고민하다 디자인패턴을 찾게 되었는데요, MVC 코드 예시에서 BuyItem 메소드 같은 경우는 view쪽에서 controller를 참조해야 할까요? 그러면 view와 controller로 분리한 의미가 없을 것 같아서요.. UI를 먼저 상호작용해서 동작을 할 경우와 동작을 먼저 하고 UI를 업데이트하는 경우 두가지에 대해서 어떤식으로 참조를 해줘야 할지 잘 모르겠습니다.
댓글 작성자 분께서는 제가 이해한 바가 맞다면 ShopView에서 Controller를 참조하는 것에 대해서 고민하시는 것 같아요. 작성하신 것 처럼 참조를 하게 되면 분리하는 의미가 전혀 없습니다. 이 경우에는 ShopView에서 상호작용을 받아서 처리하는 방법이 있어요. 정확히는 옵저버 패턴을 사용합니다. 다른 말로는 이벤트 처리 패턴이라고도 합니다. 이벤트 중계자가 클릭이나 드래그 같은 이벤트를 받아서 controller에 넘겨주는 형식 이에요. 이렇게 코드를 작성하시면 참조 없이 이벤트만 보내주면 쉽게 처리가 가능하며, 훨씬 확장성이 좋아지게 됩니다. 클릭 이벤트를 BuyItem뿐 아니라 구독만 해주면 어디서든 사용이 가능하기 때문이죠. 좀 더 자세한 건 제가 쓴 글중에 “ScriptableObject를 활용한 이벤트 처리” 라는 글이 있을 겁니다. 한번 읽어 보시는 것도 도움이 될거 같아요 ㅎㅎ. 화이팅 하시길 바라며 또 궁금한 것이 생기면 언제든 답글 달아주세요~
넵 감사합니다.