[Unity] MVC 패턴

MVC 패턴 개념 정리

역할 책임
Model 데이터(상태) 보관 및 갱신 로직• 상태 변경 시 이벤트(옵저버) 발행
View 화면 렌더링·UI 표시 담당• Model 이벤트 구독 혹은 Controller 명령에 반응
Controller • 사용자 입력·AI·타이머 등 이벤트 수신Model 업데이트 및 View 제어 명령
  • 관심사 분리 : 데이터 <-> 로직 <-> 표현을 분리해 각 클래스가 단일 책임(SRP)을 갖도록함
  • 유닛테스트 가능 : Model과 Controller는 MonoBehaviour에 의존하지 않고 순수 클래스여서 테스트 작성 쉬움
  • 유연한 확장 : View를 바꿔도 Model-Controller 변경 없이 재사용 가능

 

간단 예시 (버튼 누르면 점수 +1)

  • ScoreModel 생성 -> ScoreView 바인딩 -> ScoreController가 Model, View 연결
  • 버튼 클릭 시 Controller -> Model -> 이벤트 발생 -> View 갱신

Model

/// <summary>
/// 게임 점수(Score)를 관리하는 순수 데이터 모델
/// </summary>
public class ScoreModel {
    public int Current { get; private set; } = 0;
    public event Action<int> OnScoreChanged;  // Score 변경 시 구독자에게 통보

    /// <summary>
    /// 점수 증가 처리
    /// </summary>
    public void AddPoint() {
        Current += 1;
        OnScoreChanged?.Invoke(Current);
    }
}

 

Controller

/// <summary>
/// ScoreModel과 ScoreView를 연결하고, 버튼 클릭 처리를 담당하는 컨트롤러
/// </summary>
public class ScoreController {
    private ScoreModel _model;
    private ScoreView _view;

    public ScoreController(ScoreModel model, ScoreView view) {
        _model = model;
        _view  = view;
        _model.OnScoreChanged += _view.UpdateScoreText;
        _view.OnButtonClicked += _model.AddPoint;
    }
}

 

View

/// <summary>
/// 씬 상의 버튼과 텍스트를 제어하는 뷰 컴포넌트
/// </summary>
public class ScoreView : MonoBehaviour {
    [SerializeField] private TextMeshProUGUI _scoreText;
    [SerializeField] private Button _addButton;

    public event Action OnButtonClicked;

    private void Awake() {
        _addButton.onClick.AddListener(() => OnButtonClicked?.Invoke());
    }

    /// <summary>
    /// 점수 변경 시 텍스트 갱신
    /// </summary>
    public void UpdateScoreText(int newScore) {
        _scoreText.text = $"Score: {newScore}";
    }
}

 

 

 

 

데이터 정의는 ScriptableObject랑 비슷한거 같은데 무슨 차이??

구분 ScriptableObject(SO) Model(MVC)
생명 주기 에디터에서 에셋으로 생성·저장 → 런타임에 로드 런타임에 인스턴스화되고, 게임 오브젝트별로 고유한 상태 유지
용도 게임 설정·상수·레벨 디자인 등 공유 가능한 고정 데이터 보관 개별 객체의 동적 상태(체력, 레벨, 골드 등) 관리 및 변경
이벤트/로직 보통 데이터 정의만, 이벤트 콜백·비즈니스 로직 최소화 상태 변경 시 옵저버 패턴·비즈니스 로직 포함 가능
재사용성 프로젝트 전반에서 재사용 가능한 에셋 각 컨트롤러·뷰에 바인딩된 런타임 객체
편집과 직관성 인스펙터로 바로 편집 → 디자이너·기획자 관리 용이 코드로 제어 → 테스트·유닛테스트 작성에 최적화

 

 

 

그럼 언제 ScriptableObject를 쓰고 언제 Model을 쓰나?

ScriptableObject

  • 변하지 않는 정적인 값 설정
  • 에셋 단위로 관리
  • 사운드, 이펙트 프리팹 참조, 테마 컬러 등 프로젝트 전역에서 재사용

Model

  • 동적으로 변하는 값
  • 개별 인스턴스의 실시간 상태
  • 이벤트 발행 같은 옵저버 패턴으로 View, Controller에 알림

 

 

ScriptableObject → Model → Controller → View 구조의 스크립트 예시

  • ScriptableObject로 기본 스탯만 관리 -> 디자이너가 인스펙터에서 쉽게 수정
  • Model은 (현재 상태 + 이벤트) -> 동적 변화와 알림을 담당
  • Controller가 Model <-> View 연결 및 중계 -> 관심사의 분리
  • View는 오직 화면 처리 로직만 담당

MonsterDataSO.cs

// 1) 정적 데이터 에셋
[CreateAssetMenu(menuName="TD/MonsterDataSO")]
public class MonsterDataSO : ScriptableObject {
    public string monsterName;
    public float baseHealth;
    public float baseSpeed;
    public float baseDamage;
}

 

MonsterModel.cs

// 2) 동적 상태 모델
public class MonsterModel {
    public float CurrentHealth { get; private set; }
    public float CurrentSpeed  { get; private set; }
    public float CurrentDamage { get; private set; }

    public event Action<float> OnHealthChanged;
    public event Action OnDead;

    public MonsterModel(MonsterDataSO data) {
        CurrentHealth = data.baseHealth;
        CurrentSpeed  = data.baseSpeed;
        CurrentDamage = data.baseDamage;
        OnHealthChanged?.Invoke(CurrentHealth);
    }

    public void TakeDamage(float amount) {
        if (CurrentHealth <= 0) return;
        CurrentHealth = Mathf.Max(CurrentHealth - amount, 0);
        OnHealthChanged?.Invoke(CurrentHealth);
        if (CurrentHealth == 0) OnDead?.Invoke();
    }
}

 

MonsterController.cs

// 3) 로직 중계 컨트롤러
public class MonsterController {
    public MonsterController(MonsterModel model, MonsterView view) {
        // 체력 변화 → View에 체력바 갱신 지시
        model.OnHealthChanged += view.UpdateHealthBar;
        // 사망 → View에 사망 애니메이션 재생 지시
        model.OnDead          += view.PlayDeathAnimation;
        // (추가) AI나 플레이어 충돌 시
        view.OnHitByPlayer   += model.TakeDamage;
    }
}

 

MonsterView.cs

// 4) 화면 처리 뷰
public class MonsterView : MonoBehaviour {
    [SerializeField] private Slider healthBar;
    public event Action<float> OnHitByPlayer;

    public void UpdateHealthBar(float currentHp) {
        healthBar.value = currentHp / healthBar.maxValue;
    }

    public void PlayDeathAnimation() {
        // 애니메이터 트리거, 파티클 플레이 등
    }

    private void OnTriggerEnter(Collider other) {
        if (other.CompareTag("PlayerAttack")) {
            float damage = other.GetComponent<Attack>().Damage;
            OnHitByPlayer?.Invoke(damage);
        }
    }
}
반응형