[디자인 패턴] MVC, MVP, MVVM 패턴

 

사용자의 체력, 스탯등 게임 데이터가 UI를 통해 화면상에도 표시되는 경우는 굉장히 흔합니다.

이렇게 게임 데이터 - UI의 관계를 연결하는 방법 중에 직접 연결해주는 방법도 있지만,

추후 확장성과 유지보수 등을 위한 디자인 패턴이 있습니다.

대표적으로 MVC, MVP, MVVM 패턴들이 존재합니다.

 

⭐ MVC(Model-View-Controller) 패턴

MVC 패턴은 데이터를 담는 Model, 화면에 보여주는 View, 둘을 관리하는 Controller로 이루어진 패턴입니다.

데이터와 뷰를 분리한 패턴 중 가장 초기 패턴으로

데이터와 UI를 분리해 확장성 및 유지보수에 적합한 패턴입니다.

View와 Controller가 서로 참조하여 양방향 관계가 될 수도 있습니다.

 

● Model

    ○ 데이터와 로직을 담당합니다.

    ○ 데이터의 상태를 관리하고 데이터의 변경을 알리는 역할을 수행합니다.

    ○ ex) 플레이어 체력, 경험치, 점수 등의 데이터 수치를 관리

● View

    ○ UI(사용자 인터페이스)를 담당합니다.

    ○ Model에서 변경된 데이터 수치를 시각적으로 나타냅니다.

    ○ ex) 플레이어의 체력바, 스태미너 등을 표현

● Controller

    ○ 입력 및 로직 연결을 담당합니다.

    ○ 사용자의 입력을 받아 Model과 View에 전해주는 역할을 수행합니다.

    ○ ex) 키 입력이나 버튼 클릭을 통한 Model과 View를 조작

 

 

MVC 패턴 예제


아래와 같은 기능을 간단하게 구현해보는 예제입니다.

스페이스키 입력 → 점프, 점프 카운트 UI 증가

A키 입력 → 체력 회복, 체력 UI 증가

D키 입력 → 대미지 입음, 체력 UI 감소

 

● Model

using System;
using UnityEngine;

public class Player_Model : MonoBehaviour
{
    //컴포넌트
    [SerializeField] private Rigidbody playerRigid;

    //체력
    private int currentHp = 100;
    public int CurrentHp
    {
        get { return currentHp; }
        set
        {
            if (value < 0)
                value = 0;

            currentHp = value;
        }
    }

    public event Action<int> onHealthChanged;

    //점프
    [SerializeField] private float jumpForce = 10f;
    public int JumpCount { get; private set; } = 0;
    public event Action<int> onJump;

    public void Jump()
    {
        JumpCount++;
        playerRigid.AddForce(jumpForce * Vector3.up, ForceMode.Impulse);
        onJump?.Invoke(JumpCount);
    }

    //amount만큼 대미지가 차거나 줄어듬
    public void UpdateHp(int amount)
    {
        CurrentHp += amount;
        onHealthChanged?.Invoke(CurrentHp);
    }
}

● 체력과, 점프 횟수 필드를 가지고 있으며, 점프, HP 변경 로직을 갖는 메서드가 있습니다.

● 플레이어와 관련된 데이터 및 로직을 수행하는 메서드를 가지고 있습니다.

● 점프를 하거나, 체력이 변경되었을 때 외부에 알려줄 델리게이트가 있습니다.

 

 

● View

using TMPro;
using UnityEngine;

public class Player_View : MonoBehaviour
{
    [SerializeField] private TMP_Text text_Hp;
    [SerializeField] private TMP_Text text_JumpCount;

    public void UpdateHpUI(int currentHp)
    {
        text_Hp.text = $"Hp: {currentHp}";
    }

    public void UpdateJumpCountUI(int jumpCount)
    {
        text_JumpCount.text = $"JumpCount: {jumpCount}";
    }
}

● 체력 텍스트, 점프 횟수 텍스트 변경 로직을 갖고 있습니다.

● UI 변경 관련 로직만을 가지고 있습니다.

 

 

● Controller

using UnityEngine;
using UnityEngine.InputSystem;

public class Player_Controller : MonoBehaviour
{
    [SerializeField] private Player_Model playerModel;
    [SerializeField] private Player_View playerView;

    private InputAction damageAction;       //체력이 깎이는 인풋액션
    private InputAction healAction;         //체력이 차는 인풋액션
    private InputAction jumpAction;         //점프하는 인풋액션

    private void Awake()
    {
        damageAction = InputSystem.actions["Damage"];
        healAction = InputSystem.actions["Heal"];
        jumpAction = InputSystem.actions["Jump"];
    }

    private void Start()
    {
        playerModel.onHealthChanged += playerView.UpdateHpUI;
        playerModel.onJump += playerView.UpdateJumpCountUI;
    }

    private void TakeDamage(InputAction.CallbackContext context)
    {
        playerModel.UpdateHp(-10);
    }

    private void Heal(InputAction.CallbackContext context)
    {
        playerModel.UpdateHp(10);
    }

    private void Jump(InputAction.CallbackContext context)
    {
        playerModel.Jump();
    }

    private void OnEnable()
    {
        damageAction.performed += TakeDamage;
        healAction.performed += Heal;
        jumpAction.performed += Jump;
    }

    private void OnDisable()
    {
        damageAction.performed -= TakeDamage;
        healAction.performed -= Heal;
        jumpAction.performed -= Jump;
    }
}

● Player_Model과 Player_View를 가지고 있습니다.

● 키 입력을 받으면 그에 맞는 메서드를 Player_Model에 접근해 호출합니다.

● Player_View의 UI 변경 메서드를 Player_Model의 델리게이트에 구독합니다.

 

 

 

 

⭐ MVP(Model-View-Presenter) 패턴

MVC에서 발전한 구조로 View와 비즈니스 로직을 더 깔끔하게 분리할 수 있습니다.

Model, View, Presenter로 구성되어 있으며, 유니티에서 가장 적합하고 자주 쓰이는 방식입니다.

View와 Model은 각각 Presenter에만 의존하므로 단방향 흐름이 생기며 코드 구조가 단순화되는 이점이 있습니다.

 

● Model

    ○ 데이터와 그 데이터를 다루는 로직을 갖고 있습니다.

● View

    ○ UI와 사용자 입력 이벤트를 받습니다.

● Presenter

    ○ Model과 View를 연결하고, View의 UI 업데이트를 직접 제어합니다.

    ○ View에서 입력 이벤트를 받으면 Model을 업데이트하고, 그 결과를 다시 View로 전달해 화면을 업데이트합니다.

 

 

 

MVP 패턴 예제


점수가 오를 때 텍스트의 점수도 오르는 매우 간단한 예제입니다.

 

● Model

using System;
using UnityEngine;

public class MVP_Model : MonoBehaviour
{
    public int Score { get; private set; }
    public event Action<int> scoreChanged;

    public void AddScore()
    {
        Score++;
        scoreChanged?.Invoke(Score);
    }
}

●  점수 데이터 프로퍼티와 점수가 오를 때 호출할 델리게이트, 점수를 올리는 메서드로 구성되어 있습니다.

 

 

● View

using TMPro;
using UnityEngine;

public class MVP_View : MonoBehaviour
{
    [SerializeField] private TMP_Text textScore;

    public void UpdateScoreText(int score)
    {
        textScore.text = $"Score: {score}";
    }
}

● TMP_Text의 text 내용을 바꾸는 로직만 담당합니다.

 

 

 

● Presenter

using UnityEngine;

public class MVP_Presenter : MonoBehaviour
{
    private MVP_Model model;
    private MVP_View view;

    public MVP_Presenter(MVP_Model model, MVP_View view)
    {
        this.model = model;
        this.view = view;
    }

    private void Start()
    {
        model.scoreChanged += view.UpdateScoreText;
    }

    public void OnAddScoreButtonClicked()
    {
        model.AddScore();
    }

}

● Model과 View를 받아와 둘을 연결만 해줍니다.

 

 

 

⭐ MVVM(Model-View-ViewModel) 패턴

데이터를 담는 Model, 화면에 보여줄 View, 데이터 바인딩을 이용해

View와 값을 동기화하는 ViewModel로 이루어져 있습니다.

MVP에서는 Presenter에서 직접 View를 업데이트 한다면 

MVVM은 ViewModel이 상태를 갖고 있고, View가 그 상태를 자동으로 반영합니다.

 

● Model

    ○ 비즈니스, 데이터 로직을 담당하며, UI와 직접적인 관계를 맺지 않습니다.

 

● View

    ○ UI를 담당합니다. ViewModel을 통해 필요한 데이터를 표시합니다.

 

● ViewModel

    ○ Model과 View의 중개자 역할을 하며, UI 로직을 포함합니다.

    ○ View에서 발생한 이벤트를 Model에 전달하고, View에 필요한 데이터를 제공합니다.

    ○ View와 ViewModel간의 데이터 바인딩을 통해 변화를 자동으로 감지합니다.

 

 

MVVM 패턴 예제


● Model

using UnityEngine;

public class MVVM_Model : MonoBehaviour
{
    public int Score { get; private set; }

    public void AddScore()
    {
        Score++;
    }
}

 

 

● View

using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class MVVM_View : MonoBehaviour
{
    [SerializeField] private TMP_Text textScore;
    [SerializeField] private Button addButton;
    private MVVM_ViewModel viewModel;

    public void BindingData(MVVM_ViewModel viewModel)
    {
        this.viewModel = viewModel;

        //등록
        viewModel.onScoredChanged += value => textScore.text = $"Score: {value}";

        //UI입력 → ViewModel 호출
        addButton.onClick.AddListener(() => this.viewModel.AddScore());

        //초기화
        textScore.text = $"Score: {this.viewModel.GetScore()}";
    }
}

 

● ViewModel

using System;
using UnityEngine;

public class MVVM_ViewModel : MonoBehaviour
{
    private MVVM_Model model;

    public event Action<int> onScoredChanged;

    public MVVM_ViewModel(MVVM_Model model)
    {
        this.model = model;
    }

    public void AddScore()
    {
        model.AddScore();
        onScoredChanged?.Invoke(model.Score);
    }

    public int GetScore()
    {
        return model.Score;
    }
}