MVVM 패턴

MVVM 패턴이란?

MVVMModel-View-ViewModel 패턴은 UI 애플리케이션의 설계를 구조화하는 디자인 패턴으로, 주로 WPFWindows Presentation Foundation, Xamarin, 또는 React와 같은 프레임워크에서 사용됩니다. MVVM 패턴은 모델Model, 뷰View, 그리고 뷰모델ViewModel의 세 가지 컴포넌트로 애플리케이션을 분리하여 UI 코드의 유지보수성과 확장성을 높이는 데 중점을 둡니다. MVVM 패턴은 특히 데이터 바인딩과 이벤트 처리에 강점을 가지고 있어, View와 ViewModel 간의 상호작용을 쉽게 처리할 수 있습니다.

MVVM 패턴의 구성 요소

Model

Model은 애플리케이션의 핵심 비즈니스 로직과 데이터 처리 기능을 제공하는 계층입니다. 데이터베이스에서 데이터를 가져오거나 비즈니스 규칙을 실행하는 등의 역할을 합니다.

public class ViewModel : INotifyPropertyChanged
{
    private Model _model;
    public ViewModel(Model model)
    {
        _model = model;
        _model.PropertyChanged += (s, e) =>
        {
            if (e.PropertyName == nameof(Model.Data))
            {
                OnPropertyChanged(nameof(Data));
            }
        };
    }
    public int Data
    {
        get => _model.Data;
        set
        {
            if (_model.Data != value)
            {
                _model.Data = value;
                OnPropertyChanged(nameof(Data));
            }
        }
    }
    public ICommand UpdateCommand { get; }
    private void UpdateData()
    {
        Data += 1; // Update the data and notify the view
    }
    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

View

View는 사용자에게 보여지는 UI를 담당합니다. XAMLExtensible Application Markup Language 또는 HTML과 같은 UI 레이아웃 언어를 사용하여 작성됩니다. View는 ViewModel과 데이터를 바인딩bind하여 동적으로 데이터를 표현합니다.

<Window x:Class="MVVMExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MVVM Example" Height="200" Width="400">
    <Grid>
        <TextBox Text="{Binding Data, UpdateSourceTrigger=PropertyChanged}" />
        <Button Content="Update" Command="{Binding UpdateCommand}" />
    </Grid>
</Window>

ViewModel

ViewModel은 View와 Model 사이의 중개자 역할을 합니다. View와 Model 사이에 데이터를 전달하고 UI 이벤트를 처리합니다. ViewModel은 INotifyPropertyChanged 인터페이스를 구현하여, 데이터가 변경될 때 View에 알리는 역할을 합니다.

using System.ComponentModel;
using System.Windows.Input;
public class BookViewModel : INotifyPropertyChanged
{
    private BookModel _book;
    public BookModel Book
    {
        get => _book;
        set
        {
            _book = value;
            OnPropertyChanged(nameof(Book));
        }
    }
    public ICommand BorrowCommand { get; }
    public BookViewModel()
    {
        Book = new BookModel
        {
            BookId = 1,
            Title = "Clean Code",
            Author = "Robert C. Martin",
            IsAvailable = true
        };
        BorrowCommand = new RelayCommand(BorrowBook);
    }
    private void BorrowBook(object parameter)
    {
        if (Book.IsAvailable)
        {
            Book.IsAvailable = false;
            OnPropertyChanged(nameof(Book));
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

RelayCommand

RelayCommand는 MVVM 패턴에서 사용되는 커맨드를 처리하는 방법입니다. View에서 이벤트가 발생하면 ViewModel의 커맨드를 통해 해당 이벤트를 처리합니다.

using System;
using System.Windows.Input;
public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Func<object, bool> _canExecute;
    public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
    }
    public bool CanExecute(object parameter)
        => _canExecute == null || _canExecute(parameter);
    public void Execute(object parameter) => _execute(parameter);
    public event EventHandler CanExecuteChanged;
}

MVVM 구조

D2 Diagram

Model → ViewModel : Notify

  • Model에서 데이터가 변경되면 ViewModel에 통보합니다.
  • 일반적으로 이벤트 또는 INotifyPropertyChanged를 통해 변경 사항을 알립니다.
public class Model : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    private int _data;
    public int Data
    {
        get => _data;
        set
        {
            _data = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Data)));
        }
    }
}

ViewModel → Model : Update

  • ViewModel은 사용자의 작업이나 View의 요청에 따라 Model의 데이터를 갱신합니다.
public class ViewModel
{
    private Model _model;
    public int Data
    {
        get => _model.Data;
        set => _model.Data = value;
    }
}

ViewModel → View : Binding

  • ViewModel은 데이터를 바인딩을 통해 View에 제공합니다.
  • INotifyPropertyChanged를 통해 View가 데이터 변경을 감지하고 UI를 갱신합니다.
<TextBox Text="{Binding Data}" />

View → ViewModel : Event

  • View에서 발생한 사용자 입력 이벤트를 ViewModel에 전달합니다.
  • 일반적으로 Command또는 EventHandler를 사용하여 이벤트를 처리합니다.
public ICommand SaveCommand { get; }

MVVM 패턴의 장점

  • 코드의 분리: MVVM 패턴은 View와 비즈니스 로직이 분리되어 있어, UI가 변경되어도 비즈니스 로직에 영향을 미치지 않으며, 반대로도 마찬가지입니다.
  • 유연한 테스트: ViewModel은 UI와 분리되어 있으므로 단위 테스트가 용이합니다.
  • 데이터 바인딩: View와 ViewModel 간의 데이터 바인딩을 통해 코드의 양을 줄이고 UI 요소와 데이터 간의 동기화를 자동으로 처리할 수 있습니다.
  • 유지보수성: 각 계층의 역할이 분리되어 있어, 코드를 이해하고 수정하기 쉽습니다.

MVVM 패턴의 단점

  • 복잡성 증가: 간단한 애플리케이션에서는 오히려 ViewModel을 구현하는 것이 불필요하게 복잡해질 수 있습니다.
  • 과도한 코드 분리: 모든 애플리케이션에서 ViewModel을 사용하는 것이 적합하지 않을 수 있으며, 작은 애플리케이션에서 오히려 불필요한 구조화로 인해 복잡성이 증가할 수 있습니다.

MVVM과 MVC의 비교

  • MVCModel-View-Controller 패턴은 View와 Controller 사이에 강한 결합이 존재하지만, MVVM에서는 View와 ViewModel이 바인딩을 통해 연결됩니다. 이는 UI의 복잡성을 줄이고, 더 나은 테스트 가능성과 유지보수성을 제공합니다.
  • MVC는 웹 애플리케이션에서 주로 사용되며, MVVM은 데스크톱 애플리케이션이나 UI 중심의 애플리케이션에서 더 많이 사용됩니다.

결론

MVVM 패턴은 UI 애플리케이션에서 코드의 분리, 데이터 바인딩, 테스트 가능성을 개선하는 데 유용한 디자인 패턴입니다. 특히, WPF, Xamarin 등 데이터 바인딩을 지원하는 프레임워크에서 매우 효과적으로 사용됩니다.

심화 학습

데이터 바인딩

MVVM 패턴의 핵심 기능 중 하나는 데이터 바인딩Data Binding입니다. ViewModelView 사이의 양방향 바인딩을 통해 데이터를 효율적으로 동기화할 수 있습니다. 즉, 사용자가 UI에서 값을 변경하면 그 값이 ViewModel로 즉시 반영되고, 반대로 ViewModel의 데이터가 변경되면 UI에도 자동으로 반영됩니다.

데이터 바인딩 예시

// ViewModel
public class BookViewModel : INotifyPropertyChanged
{
    private string _bookTitle;
    public string BookTitle
    {
        get { return _bookTitle; }
        set
        {
            if (_bookTitle != value)
            {
                _bookTitle = value;
                OnPropertyChanged(nameof(BookTitle));
            }
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
<!-- XAML View -->
<TextBox Text="{Binding BookTitle, Mode=TwoWay}" />
  • 이 예시에서 BookTitle 프로퍼티는 ViewModel에서 정의되어 있으며, INotifyPropertyChanged 인터페이스를 사용하여 프로퍼티 변경 시 UI에 자동으로 반영됩니다.
  • TextBoxView에서 BookTitle에 양방향으로 바인딩되어 있어 사용자가 값을 입력할 때 ViewModelBookTitle 프로퍼티도 자동으로 변경됩니다.

명령 바인딩

MVVM 패턴에서 View는 비즈니스 로직을 직접 호출하지 않으며, 대신 명령Command을 통해 사용자 인터랙션을 처리합니다. 명령 패턴은 특히 버튼 클릭 등의 이벤트 처리를 ViewModel로 위임하는 데 유용합니다.

명령 바인딩 예시

// ViewModel
public class BookViewModel : INotifyPropertyChanged
{
    public ICommand SaveCommand { get; }
    public BookViewModel()
    {
        SaveCommand = new RelayCommand(SaveBook);
    }
    private void SaveBook(object parameter) => Console.WriteLine("Book Saved!");
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
<!-- XAML View -->
<Button Content="Save" Command="{Binding SaveCommand}" />
  • RelayCommand 클래스는 ICommand 인터페이스를 구현하여 명령을 처리합니다.
  • SaveCommandView에서 버튼 클릭 시 호출되며, 도서 정보를 저장하는 비즈니스 로직을 ViewModel에서 처리합니다.

ViewModel의 책임 분리

MVVM에서 ViewModelViewModel을 중개하는 역할을 합니다. 이때, ViewModel은 비즈니스 로직을 직접 수행하지 않고, 단순히 데이터를 가공하여 View에 제공하는 역할만 수행해야 합니다. 즉, 비즈니스 로직은 별도의 서비스나 리포지토리로 분리하고, ViewModel은 그 서비스를 호출하는 구조로 설계하는 것이 좋습니다.

서비스 분리 예시

// Model
public class BookService
{
    public void SaveBook(Book book)
    {
        // 도서 저장 로직
    }
}
// ViewModel
public class BookViewModel : INotifyPropertyChanged
{
    private readonly BookService _bookService;
    public ICommand SaveCommand { get; }
    public BookViewModel()
    {
        _bookService = new BookService();
        SaveCommand = new RelayCommand(SaveBook);
    }
    private void SaveBook(object parameter)
    {
        // 비즈니스 로직을 서비스로 위임
        _bookService.SaveBook(new Book { Title = BookTitle });
    }
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
  • ViewModel은 비즈니스 로직을 처리하지 않고, 단순히 BookService 클래스를 통해 데이터를 처리합니다.
  • 이렇게 책임을 분리하면, 테스트와 유지보수가 더 쉬워지며 ViewModel의 역할이 명확해집니다.

MVVM의 테스트 가능성

MVVM 패턴은 테스트 가능한 코드 작성을 돕습니다. ViewModel은 UI에 의존하지 않으므로, 단위 테스트를 쉽게 작성할 수 있습니다. 특히, ViewModel은 순수 로직을 처리하기 때문에 UI 컴포넌트와의 상호작용 없이도 로직을 검증할 수 있습니다.

단위 테스트 예시

[TestClass]
public class BookViewModelTests
{
    [TestMethod]
    public void SaveCommand_ShouldCallSaveBookInService()
    {
        // Arrange
        var mockService = new Mock<BookService>();
        var viewModel = new BookViewModel(mockService.Object);
        // Act
        viewModel.SaveCommand.Execute(null);
        // Assert
        mockService.Verify(service => service.SaveBook(It.IsAny<Book>()), Times.Once);
    }
}
  • 이 예시에서는 Mock을 사용하여 BookServiceSaveBook 메서드가 호출되는지 확인할 수 있습니다. ViewModel은 UI 없이도 비즈니스 로직을 테스트할 수 있으므로, 높은 테스트 가능성을 가집니다.

MVVM과 의존성 주입 통합

ViewModelService 간의 의존성을 관리하기 위해 의존성 주입DI을 적용할 수 있습니다. DI는 ViewModel에서 구체적인 서비스 클래스에 의존하지 않고, 런타임에 적절한 서비스를 주입하여 유연성을 높입니다.

DI 적용 예시

// ViewModel
public class BookViewModel : INotifyPropertyChanged
{
    private readonly IBookService _bookService;
    public BookViewModel(IBookService bookService)
    {
        _bookService = bookService;
    }
    public ICommand SaveCommand { get; }
    private void SaveBook(object parameter)
    {
        _bookService.SaveBook(new Book { Title = BookTitle });
    }
}
  • 이 구조에서는 BookViewModel이 구체적인 서비스 구현체에 의존하지 않으므로, 다양한 서비스 구현체를 주입할 수 있습니다.
  • 의존성 주입을 통해 ViewModel의 유연성과 테스트 가능성이 더욱 향상됩니다.