# MVVM 아키텍처 MVVM(Model-View-ViewModel)은 사용자 인터페이스(UI) 구현에서 관심사 분리를 통해 유지보수성과 테스트 용이성을 높이기 위한 소프트웨어 아키텍처 패턴이다. Model은 비즈니스 로직과 데이터 모델을, View는 사용자 인터페이스를, ViewModel은 View와 Model 사이의 중재 역할을 수행하며 데이터 바인딩과 명령 패턴을 통해 상호작용을 돕는다. 이 패턴은 특히 데이터 바인딩이 강력하게 지원되는 프레임워크에서 널리 사용된다. --- ## 역사 및 배경 - 1990년대 말에서 2000년대 초에 걸쳐 WPF, Silverlight, Xamarin 등 .NET 계열 프레임워크의 등장과 함께 대중화되었다. - 초기 GUI 패턴인 MVC, MVP의 한계를 극복하고 View의 상태를 ViewModel을 통해 관리하는 방향으로 발전하였다. - 모바일 및 웹 프런트엔드에서도 MVVM 기반의 데이터 바인딩을 제공하는 프레임워크가 등장하며 폭넓게 채택되었다. --- ## 핵심 구성 요소 - Model - 비즈니스 도메인 객체, 데이터 접근 로직, 비즈니스 규칙 등을 포함한다. - 보통 순수한 데이터 구조 또는 데이터와 관련된 서비스 계층의 계약을 표현한다. - View - 사용자에게 보이는 UI를 담당한다. - 일반적으로 XAML, HTML 템플릿, XML 레이아웃 등의 뷰 기술로 구현된다. - ViewModel - View와 Model 사이의 중재자 역할을 한다. - View의 상태를 표현하는 데이터 바인딩 프로퍼티와 사용자 명령을 나타내는 ICommand를 제공한다. - View에 대한 직접적인 참조 없이 데이터와 동작 로직을 제공하므로 테스트 용이성이 높아진다. - Data Binding - View의 UI 요소를 ViewModel의 프로퍼티에 연결하는 기술이다. - 양방향 바인딩 또는 단방향 바인딩이 가능하며, ViewModel의 변경 알림(INotifyPropertyChanged 등)을 통해 View가 자동으로 갱신된다. - Commands - UI의 이벤트를 ViewModel 수준으로 끌어올려 비즈니스 로직과 뷰를 분리한다. - 예: SaveCommand, LoadCommand - Services / Repositories - 데이터 소스 접근, 네트워크 호출, 로컬 저장소 등 Model 계층의 의존성을 주입 받아 분리한다. - Messaging / Event Aggregator (선택적) - ViewModel 간의 느슨한 결합을 유지하면서 이벤트를 전달하는 메커니즘이다. - Navigation (선택적) - 화면 간 이동을 ViewModel에서 제어하는 패턴이다. 뷰의 네비게이션 로직을 View보다 ViewModel 쪽으로 옮겨 재사용성과 테스트성을 높인다. --- ## 데이터 흐름과 바인딩 원리 - 기본 흐름 - ViewModel의 프로퍼티가 View와 바인딩되고, ViewModel의 명령이 View의 이벤트를 대체한다. - 사용자가 UI를 통해 값을 변경하면 바인딩 메커니즘에 의해 ViewModel의 프로퍼티가 갱신되고, 필요 시 Model의 데이터로 반영된다. - 변경 알림 - ViewModel은 INotifyPropertyChanged 등의 메커니즘으로 프로퍼티 변경을 View에 알린다. - 변경 알림은 바인딩 엔진에 의해 자동으로 View의 UI 요소에 반영된다. - 단방향 vs 양방향 바인딩 - 단방향 바인딩: ViewModel→View의 데이터 흐름만 가능 (뷰가 읽기 전용일 때 유용) - 양방향 바인딩: ViewModel↔View의 양방향 데이터 흐름이 가능 (입력 컨트롤에 자주 사용) - 명령 바인딩 - View의 버튼 클릭 등 이벤트를 ViewModel의 ICommand로 연결하여 UI 이벤트를 처리한다. --- ## 구현 원칙 및 모범 사례 - SRP(단일 책임 원칙) 및 Separation of Concerns - View, ViewModel, Model 간 경계를 명확히 하여 변경 영향 범위를 최소화한다. - ViewModel의 경계 설정 - ViewModel은 뷰에 특화된 의존성을 가지지 않도록 설계한다. - 테스트 용이성 - 비즈니스 로직은 ViewModel에서 테스트 가능해야 하며, UI 바인딩은 테스트 대상을 ViewModel 테스트에서 모의하는 방향으로 구성한다. - 의존성 주입 - Services, Repositories, DataSource 등의 의존성은 DI를 통해 주입하여 ViewModel의 재사용성과 교체성을 높인다. - 무상태 vs 상태 관리 - ViewModel의 상태를 최소화하고 필요 시 상태를 외부 저장소에 맡겨 일관성을 유지한다. - 성능 관리 - 무분별한 바인딩 갱신으로 인한 UI 렌더링 비용을 줄이기 위해 UpdateSourceTrigger와 같은 바인딩 제어를 적절히 조정한다. --- ## 플랫폼별 차이점과 실무 적용 예 - WPF / .NET (C#, XAML) - DataBinding, INotifyPropertyChanged, ICommand를 중심으로 구현한다. - 예: XAML에서 ViewModel의 프로퍼티에 바인딩하고, Button의 Command에 SaveCommand를 연결. - Android (Jetpack ViewModel, LiveData, Data Binding) - ViewModel + LiveData를 사용해 Observable 데이터를 관리하고, Data Binding으로 레이아웃에 연결한다. - iOS / SwiftUI - MVVM 스타일에 비슷한 바인딩 메커니즘이 존재하며, ObservableObject와 @Published를 활용한 바인딩이 일반적이다. - Web (Vue.js, Knockout.js 등) - Vue.js의 MVVM적 특성에서 ViewModel에 해당하는 Vue 인스턴스나 컴포넌트가 View와 데이터 바인딩을 수행한다. - 공통 모범 사례 - ViewModel에서 도메인 로직과 데이터 변환을 수행하고, View는 프레이밍과 이벤트 전달에 집중한다. - 모델은 가능한 순수 데이터 구조로 유지하고, 네트워크/저장소 로직은 별도 서비스 계층으로 분리한다. --- ## 구현 예시 (간단한 ToDo 애플리케이션) - XAML 예시 (WPF): ```xml <!-- 간단한 입력과 목록 표시 바인딩 예시 --> <Window x:Class="TodoApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:TodoApp" Title="ToDo" Height="350" Width="525"> <Window.DataContext> <local:MainViewModel /> </Window.DataContext> <Grid Margin="10"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,8"> <TextBox Width="300" Text="{Binding NewItemTitle, UpdateSourceTrigger=PropertyChanged}" /> <Button Content="추가" Command="{Binding AddCommand}" Margin="8,0,0,0" /> </StackPanel> <ListBox Grid.Row="1" ItemsSource="{Binding Items}"> <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Title}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window> ``` - ViewModel C# 예시: ```csharp using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; public class MainViewModel : INotifyPropertyChanged { private string _newItemTitle; public string NewItemTitle { get => _newItemTitle; set => SetProperty(ref _newItemTitle, value); } public ObservableCollection<TodoItem> Items { get; } = new ObservableCollection<TodoItem>(); public ICommand AddCommand { get; } public MainViewModel() { AddCommand = new RelayCommand(_ => AddItem(), _ => !string.IsNullOrWhiteSpace(NewItemTitle)); } private void AddItem() { Items.Add(new TodoItem { Title = NewItemTitle }); NewItemTitle = string.Empty; } protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null) { if (Equals(storage, value)) return false; storage = value; OnPropertyChanged(propertyName); return true; } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public class TodoItem { public string Title { get; set; } } ``` - RelayCommand 예시: ```csharp 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?.Invoke(parameter) ?? true; public void Execute(object? parameter) => _execute(parameter); public event EventHandler? CanExecuteChanged; public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); } ``` - 간단한 도메인 모델: ```csharp public class TodoItem { public string Title { get; set; } } ``` 이 예시는 MVVM의 기본 원칙을 따라 ViewModel에서 상태를 관리하고, View와의 바인딩을 통해 UI를 갱신하는 흐름을 보여준다. 실제 프로젝트에서는 데이터 저장소 연동, 입력 검증, 비동기 작업 처리 등을 추가적으로 구현한다. --- ## 도입 시 고려사항 - 초기 복잡성 증가 - MVVM은 단순한 UI에 비해 초기 설계가 다소 무거울 수 있다. 프로젝트 규모와 팀의 역량에 맞춰 적용 범위를 결정한다. - 바인딩 과다로 인한 디버깅 난이도 - 바인딩 경로를 추적하기 어렵다면 로깅과 테스트를 통해 문제를 진단하는 전략이 필요하다. - 테스트 전략 - ViewModel 중심의 단위 테스트가 가능하도록 구조를 설계하고, 뷰의 바인딩은 UI 테스트로 보완한다. - 성능 관리 - 큰 데이터 집합이나 잦은 바인딩 갱신이 성능에 영향을 미칠 수 있으므로 적절한 업데이트 트리거를 사용한다. - 플랫폼 제약 - 각 플랫폼의 바인딩 엔진과 라이프사이클 관리 차이를 이해하고, 해당 플랫폼에 맞춘 구현 패턴을 채택한다. --- ## 관련 도서 및 자료 - MVVM 패턴 비교: MVVM vs MVC vs MVP - Data Binding의 원리 및 구현 패턴 --- 관련 문서: [[MVVM 패턴 비교: MVVM vs MVC vs MVP]], [[Data Binding의 원리 및 구현 패턴]] [[소프트웨어 설계 패턴]]