[WPF] MVVM 패턴 이해하기 – 1

[WPF] MVVM 패턴 이해하기 – 1

이번 글에서는 WPF에서 MVVM에 대해 알아보겠습니다. MVVM 패턴은 Model, View, View Model 단어를 합쳐 만든 용어입니다. 이러한 패턴을 사용하는 이유는 UI와 로직을 명확하게 분리하고, 유지보수성과 코드 재사용성을 높이고 책임 영역을 명확히 하며 테스트를 하기 쉬운 구조로 만들기 위함입니다. MVVM 패턴을 사용하면 UI 디자이너는 View, 개발자는 View Model과 Model을 맡아 역할을 명백히 분리할 수 있어 분업에 용이합니다.

각 단어에 대한 설명은 아래와 같습니다.

Model실제 데이터와 비즈니스 로직(ex. 사용자, 제품 정보, 데이터 저장 등)
View사용자에게 보여지는 UI(ex. XAML)
View ModelView와 Model 사이에서 중간다리 역할(데이터를 바인딩하고 명령을 연결)

Model, View, View Model의 관계를 한 눈에 보면 아래와 같습니다.

Binding

디자이너는 View(xaml 파일)만 작업 합니다. 디자이너가 작업한 View에서 데이터를 입력 받고 ViewModel에 전달 할 땐, Binding을 이용하여 속성을 연결하여 전달 하시면 됩니다.


데이터 바인딩 글에서 본 것처럼 요소의 Value나 Text, Command 속성에 {Binding 바인딩 할 속성}을 입력 하시면 됩니다.

<TextBox Text="{Binding Name}" />
<Button Content="Save" Command="{Binding SaveCommand}"/>

디자이너가 화면을 만들 때엔 TextBox와 Button이 화면에 표시되는 것만 고려하면 됩니다. 디자이너는 위 예시에서 TextBox 연결 된 속성 Name이나 Button이 클릭 될 때 실행될 속성 SaveCommand는 크게 신경 쓰지 않아도 됩니다.

INotifyPropertyChanged, ICommand

개발자는 디자이너가 만든 화면이 어떻게 구성되어 있는지 크게 신경 쓰지 않아도 됩니다. TextBox 크던 작던, Button이 어느 위치에 있던 상관이 없습니다. 다만 TextBox에 표시할 Name과 Button이 클릭했을 때 동작할 SaveCommand만 구현하면 됩니다.

Name 속성이 변경되면 UI에게 속성이 변경되었다고 알려주고, SaveCommand 속성을 정의하여 버튼 클릭에 대한 동작을 정의하면 됩니다. 이 두 가지 경우는 INotifyPropertyChangedICommand를 사용하여 구현합니다.

우선 INotifyPropertyChanged에 대해 알아보겠습니다. INotifyPropertyChanged는 속성이 변경되었음을 WPF의 데이터 바인딩 시스템에 알리는 역할을 합니다.
INotifyPropertyChanged를 상속받아 PropertyChangedEventHandler로 이벤트를 발생시키면 UI에 전달하게 됩니다.

위 내용을 코드로 구현하면 아래 Notifier과 같은 클래스로 구현됩니다.

// INotifyPropertyChanged 인터페이스를 구현하여 속성 변경 알림 기능을 제공하는 클래스
public class Notifier : INotifyPropertyChanged
{
    // 속성이 변경되었을 때 호출되는 이벤트
    public event PropertyChangedEventHandler PropertyChanged;

    // 속성이 변경되었을 때 호출되는 메서드
    protected void OnPropertyChanged(string propertyName)
    {
        // 이벤트를 발생시켜서 변경된 속성 이름을 포함한 알림을 보냄
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Button Command에 연결된 SaveCommand도 MVVM 패턴에 맞게 처리해줘야 합니다. 만약 디자이너가 아래처럼 Click 이벤트에 SaveFunction을 연결하게 되면 비하인드 코드에 SaveFunction을 정의해줘야 합니다.

// MainWindow.xaml
<Button Content="Save" Click="SaveFunction"/>
// MainWindow.xaml.cs
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    public void SaveFunction()
    {
        // todo .. : 
    }
}

이 경우 MVVM 패턴에 위배되므로 디자이너는 Click을 사용하지 않고 Command에 Binding을 하여 사용합니다. 개발자는 동작에 대한 Binding을 처리하기 위해 ICommand를 사용합니다. ICommand는 버튼 클릭과 같은 액션을 ViewModel에 정의하기 위한 인터페이스입니다.

이 방법을 사용하게 되면 비하인드 코드에 SaveFunction을 정의해줘야 합니다. 이 경우 MVVM 패턴에 위배되므로 Click을 사용하지 않고 ICommand를 사용합니다. ICommand는 버튼 클릭과 같은 액션을 ViewModel에서 처리하기 위한 인터페이스입니다.

WPF에서는 Binding이 될 때 이 인터페이스의 CanExecute 함수를 사용하여 Execute 함수 호출 유무를 판단하고 Execute를 호출하게 됩니다.

이벤트/메서드설명
Execute()명령 실행 (버튼 클릭 시 실행됨)
CanExecute()명령 실행 가능 여부 판단 (false면 버튼 비활성화)
CanExecuteChanged실행 가능 여부가 바뀌었을 때 UI에 알림

이 인터페이스의 CanExecute, Execute를 이용하면 MVVM 패턴에 알맞는 코드를 작성할 수 있습니다.

우선 아래와 같이 ICommand 인터페이스를 상속 받은 Command 클래스를 생성해줍니다.

// ICommand 인터페이스 구현
public class Command : ICommand
{
    private readonly Action execute;         // 명령 실행 메서드
    private readonly Func<bool> canExecute;  // 명령 실행 가능 여부 판단

    // 생성자: 실행 메서드와 실행 가능 조건을 받음
    public Command(Action execute, Func<bool> canExecute = null)
    {
        this.execute = execute;
        this.canExecute = canExecute;
    }

    // 버튼 등 UI 요소가 명령 실행 가능한지 판단
    public bool CanExecute(object parameter) => canExecute?.Invoke() ?? true;

    // 명령 실행
    public void Execute(object parameter) => execute();

    // CanExecute가 바뀔 때 UI에 알림
    public event EventHandler CanExecuteChanged;

    // 외부에서 실행 가능 여부를 재평가하라고 알릴 때 사용
    public void RaiseCanExecuteChanged() =>
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

ViewModel에서 이 클래스를 사용하여 동작을 정의합니다.

// ViewModel
public class MainWindowViewModel : Notifier
{
    public Command SaveCommand { get; }

    public MainWindowViewModel()
    {
        SaveCommand = new Command(OnSave, CanExecuteSave);
    }

    private void OnSave()
    {
        // todo .. : 
    }

    private bool CanExecuteSave()
    {
        // todo .. : 
        return true;
    }
}


다음 글에서 여기서 만든 Notifier 클래스와 Command 클래스를 사용하여 실제 예시를 알아보겠습니다.