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 구조
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입니다. ViewModel
과 View
사이의 양방향 바인딩을 통해 데이터를 효율적으로 동기화할 수 있습니다. 즉, 사용자가 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에 자동으로 반영됩니다. TextBox
는View
에서BookTitle
에 양방향으로 바인딩되어 있어 사용자가 값을 입력할 때ViewModel
의BookTitle
프로퍼티도 자동으로 변경됩니다.
명령 바인딩
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
인터페이스를 구현하여 명령을 처리합니다.SaveCommand
는View
에서 버튼 클릭 시 호출되며, 도서 정보를 저장하는 비즈니스 로직을ViewModel
에서 처리합니다.
ViewModel의 책임 분리
MVVM에서 ViewModel
은 View
와 Model
을 중개하는 역할을 합니다. 이때, 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
을 사용하여BookService
의SaveBook
메서드가 호출되는지 확인할 수 있습니다.ViewModel
은 UI 없이도 비즈니스 로직을 테스트할 수 있으므로, 높은 테스트 가능성을 가집니다.
MVVM과 의존성 주입 통합
ViewModel
과 Service
간의 의존성을 관리하기 위해 의존성 주입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
의 유연성과 테스트 가능성이 더욱 향상됩니다.