전통적인 GUI 애플리케이션 개발에서는 아래와 같은 관념 모델을 주로 사용하셨을 겁니다.
버튼 객체 → 버튼 클릭 이벤트 → 핸들러 연결 → 이벤트 핸들러에서 컨트롤 속성 직접 수정
매우 직접적이고 직관적인 구현 방식이긴 하지만, 이 경우 화면의 요소가 어떻게 변경되는가에 따라 수정해야 할 코드의 양이 크게 늘어난다는 문제가 있습니다.
그래서 약간은 간접적이고 돌아가는 것 같은 느낌이 들 수 있지만, 이벤트 핸들러나 컨트롤 속성을 코드에서 직접 수정하지 않고, 별도의 바인딩 엔진이 대신 처리하도록 연결해주는 것이 MVVM의 기본 원리라고 보시면 어렵지 않습니다. (다만, 패턴이 생소하기 때문에 오는 러닝 커브는 분명 존재합니다. 그리고 뷰 모델을 따로 정의해주어야 한다는 점이 초반의 구현 상의 일거리를 늘어나게 만드는 느낌도 받으실 수 있고, 무엇보다도 화면의 논리적 구조를 기획 단계에서 정의해야 하는 행위가 부담스럽게 느껴질 수도 있습니다.)
예를 들어 버튼 클릭 이벤트는 버튼의 “명령(Command)” 속성으로 정의할 수 있고, 이렇게 드러난 명령 속성에 이벤트 핸들러 대신 명령 객체를 연결하게 됩니다.
이때 명령 속성에 연결되는 객체는 자신이 어떤 GUI 플랫폼—Windows Forms인지, WPF인지, Blazor인지, MAUI인지—에 연결될지 알 필요도 없고, 몰라도 잘 작동할 수 있어야 합니다.
그렇다면 명령 속성과 연결될 데이터는 어떻게 처리할 수 있을까요? 이 부분에서 뷰 모델(ViewModel)의 역할이 시작됩니다. 뷰 모델은 속성이 바뀔 때 MVVM 엔진에게 이를 알려줄 수 있어야 하고, 이를 위해 INotifyPropertyChanged 인터페이스를 뷰 모델에서 구현하게 됩니다.
public class DemoCustomer : INotifyPropertyChanged
{
// These fields hold the values for the public properties.
private Guid idValue = Guid.NewGuid();
private string customerNameValue = String.Empty;
private string phoneNumberValue = String.Empty;
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// The constructor is private to enforce the factory pattern.
private DemoCustomer()
{
customerNameValue = "Customer";
phoneNumberValue = "(312)555-0100";
}
// This is the public factory method.
public static DemoCustomer CreateNewCustomer()
{
return new DemoCustomer();
}
// This property represents an ID, suitable
// for use as a primary key in a database.
public Guid ID
{
get
{
return this.idValue;
}
}
public string CustomerName
{
get
{
return this.customerNameValue;
}
set
{
if (value != this.customerNameValue)
{
this.customerNameValue = value;
NotifyPropertyChanged();
}
}
}
public string PhoneNumber
{
get
{
return this.phoneNumberValue;
}
set
{
if (value != this.phoneNumberValue)
{
this.phoneNumberValue = value;
NotifyPropertyChanged();
}
}
}
}
예를 들어 위와 같은 코드가 있다고 했을 때, 값이 동일한지 아닌지 비교한 다음, NotifyPropertyChanged 메서드를 호출해서 어떤 프로퍼티가 바뀌었는지 이벤트를 발행하는 식입니다. MVVM 엔진은 PropertyChanged 이벤트 핸들러를 구독하고 있는 상태이므로 나머지는 이전에 선언적으로 연결해둔 속성 연결 관계에 따라서 알아서 속성을 업데이트할 것입니다. (NotifyPropertyChanged 메서드에 인자를 지정하지 않아도 되는 이유는 C# 언어 명세를 보시면 되겠습니다.
이렇게 하면 MVVM 엔진이 특정 속성의 변경을 감지하고, GUI도 연동해서 함께 업데이트되도록 동작하게 됩니다. 명령 객체는 GUI를 직접 제어하는 대신, 뷰 모델을 통해 필요한 동작을 간접적으로 구현하게 됩니다.
물론 이런 구현 방식이 항상 잘 작동하는 건 아닙니다. 버튼처럼 명확하고 대표적인 이벤트 속성이 있다면 통하지만, 그렇지 않은 경우 여전히 GUI에 직접 연결되는 제어 코드가 필요할 수 있습니다. 다만 핵심 비즈니스 로직과 데이터 모델을 GUI 코드에서 명확히 분리하여, 화면 구성이 어떻게 바뀌든, 어떤 GUI 프레임워크를 사용하든 관계없이 기술 스위칭이 가능하도록 만드는 것이 MVVM 구현을 택하는 가장 큰 이유이자 핵심이라고 보시면 좋겠습니다.
ps. 커맨드 패턴으로 애플리케이션의 기능들을 모두 분리시켜두었다면, MS Office의 사례처럼 프로그램 내 여러 편집 기능들을 파이썬이나 자바스크립트 등 애플리케이션 외부에서 호출할 수 있도록 내보내는 것도 가능하고, 더 나아가서는 생성형 AI와 MCP를 통합시킬 때 필요한 "애플리케이션 기능 명세"를 프롬프트에 만들어 넣고 AI가 만들어내는 출력을 따라가도록 만들 때에도 편리합니다. 