MVVM 아키텍처의 핵심 원리를 체계적으로 정리합니다. MVVM을 배우는 초보자들은 일반적으로 "결합을 해제하려면 어떻게 해야 할까?"라는 질문에 빠지기 쉬운 함정이 있습니다. 데이터 중심 접근법을 이해하면 MVVM의 본질을 파악할 수 있습니다.
MVVM의 핵심 원리: UI와 로직의 분리
WPF, Avalonia, MAUI 개발에서는 MVVM 패턴이 표준적인 구조로 자리 잡고 있습니다. 많은 개발자들이 궁금해하는 대표적 질문은 다음과 같습니다: "ViewModel에서 비즈니스 로직을 처리하는 것이 왜 Window.Show()나 버튼 상태 조절이 불가능한가요?"
이러한 제약은 소프트웨어 공학에서 최상의 목표인 분리성을 달성하기 위한 설계입니다.
- UI 참조 금지의 이유
ViewModel에 System.Windows.Controls 또는 Button과 같은 UI 컨트롤 타입을 직접 참조하는 경우, 다음 3가지 문제점이 발생합니다:
-
테스트 용이성 저하: ViewModel이 UI 요소를 직접 조작하게 되면 테스트 시 전체 UI 엔진을 실행해야 하며, 단위 테스트 수행이 어려워집니다. 이는 자동화 테스트의 효율성을 극도로 낮춥니다.
-
비즈니스 로직과 UI의 강한 결합: 예를 들어, ListBox를 DataGrid로 변경해야 할 때 ViewModel에 ListBox 관련 코드가 존재한다면, 비즈니스 로직 수정이 필요해집니다. 이는 개방-폐쇄 원칙(OCP)을 위반합니다.
-
다중 플랫폼 호환성 문제: Avalonia와 같은 현대 프레임워크는 크로스 플랫폼 지원을 제공합니다. Windows 특화 UI 로직이 포함된 ViewModel은 Linux나 Android로 전환 시 재구현이 필수적이 됩니다.
-
UI 제어 방식의 혁신적 접근
MVVM에서는 명령형 UI 조작 대신 상태 변화를 통해 UI가 자동으로 반응하도록 설계합니다.
1. 데이터 동기화: 바인딩 기술
사례: 버튼 비활성화 및 로딩 애니메이션 제어
- 전통적 방법: btnSave.IsEnabled = false;
- MVVM 방식: ViewModel에서 bool IsBusy 속성을 정의하고, XAML에서 버튼의 IsEnabled 속성을 IsBusy 값에 바인딩하여 반전 처리
- 원리: ViewModel이 IsBusy = true 값을 갱신하면, UI가 해당 값의 변화를 감지해 자동으로 비활성화 처리됨
2. 복잡한 UI 상호작용: 행동 추가(Behaviors)
사례: 스크롤 위치 조절 및 TextBox 포커스 시 전체 선택
- 해결책: Microsoft.Xaml.Behaviors 라이브러리 사용
- 원리: 특정 컨트롤에 대한 일반적인 '행동' 클래스를 작성하고, ViewModel의 속성 변화를 감지해 UI 레이어에서 API 호출 수행
3. 다이얼로그 및 네비게이션: 서비스 추상화
사례: 작업 완료 후 확인 다이얼로그 표시
- 해결책: 의존성 주입(DI)을 통한 인터페이스화
- 실현 로직:
- IDialogService 인터페이스 정의 (Show 메서드 포함)
- ViewModel 생성자에서 해당 인터페이스 주입
- ViewModel은 _dialogService.Show("저장 완료")만 호출
- Service 구현체는 Win32 창 또는 Material Design 다이얼로그 등 구체적인 UI 구현체 관리
4. 모듈 간 통신: 메시지 중개자(Messenger)
사례: A 창 버튼 클릭 시 B 창 새로고침
- 해결책: CommunityToolkit.Mvvm의 Messenger 사용
- 원리: ViewModel A에서 RefreshMessage 발행, View B 또는 ViewModel B가 해당 메시지 구독하여 반응 처리
- UI 코드 작성의 적절한 경계
많은 개발자가 MainWindow.xaml.cs 파일을 비워두어야 한다고 생각하지만, 이는 오해입니다.
원칙: 시각적 표현에 필요한 로직은 반드시 View 영역에 남겨야 합니다.
- View 후단에서 작성 가능한 사례: 순수한 애니메이션 처리, 복잡한 마우스 트래킹, 다중 모니터 좌표 계산, 제3자 컨트롤 라이브러리 요구사항 충족
- 판단 기준: "이 코드를 삭제해도 비즈니스 로직이 콘솔 앱에서 작동할 수 있는가?" 만약 가능하다면 이는 UI 로직으로 판단되어 View에 남겨야 합니다.
- 마무리: 사고방식의 전환
MVVM의 본질은 상태 기계 개념입니다.
- View는 상태의 관찰자
- ViewModel은 상태의 관리자
- Model은 상태의 데이터 원천
ViewModel에서 txtUser.Text = "" 처럼 UI 직접 조작을 시도할 때는 멈추고, 어떤 속성을 정의해야 UI가 자동으로 갱신될 수 있을지 고민해야 합니다.
DI를 활용한 ViewModel에서 다이얼로그 트리거링 구현 예시
MVVM을 CommunityToolkit.Mvvm으로 구현할 때, ObservableObject와 DI 기법을 활용하여 우아한 다이얼로그 처리 방식을 구현합니다.
핵심 아이디어는: ViewModel이 요청을 제기하고, Service가 실행을 담당합니다.
1. 서비스 인터페이스 정의 (추상화 계층)
ViewModel 소속 프로젝트에서 인터페이스 정의
// 인터페이스는 ViewModel 소속 프로젝트에 정의
public interface IDialogServiceInterface
{
// 기본 알림 창 표시
void ShowNotification(string title, string message);
// 확인 대화상자 표시 및 결과 반환
bool RequestConfirmation(string title, string message);
}
2. UI 계층에서 서비스 구현 (구체적 구현)
UI 프로젝트 내부에서 실제 UI 연동 코드 작성
using System.Windows;
// 구현체는 UI 프로젝트 내부에 위치
public class WpfDialogServiceImpl : IDialogServiceInterface
{
public void ShowNotification(string title, string message)
{
// 실제 UI 연동 코드
MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information);
}
public bool RequestConfirmation(string title, string message)
{
var result = MessageBox.Show(message, title, MessageBoxButton.YesNo, MessageBoxImage.Question);
return result == MessageBoxResult.Yes;
}
}
3. ViewModel에서 서비스 사용 (분리 계층)
CommunityToolkit.Mvvm의 특성 활용하여 생성자 주입
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class MainViewModelClass : ObservableObject
{
// 서비스 인터페이스 선언
private readonly IDialogServiceInterface _dialogService;
[ObservableProperty]
private string _userName = "게스트";
// 생성자에서 서비스 주입 (DI 컨테이너와 연동)
public MainViewModelClass(IDialogServiceInterface dialogService)
{
_dialogService = dialogService;
}
// RelayCommand 사용으로 비즈니스 로직 처리
[RelayCommand]
private void ExecuteSave()
{
// 1. 비즈니스 로직 처리 (데이터베이스 저장 등)
// ... 가상 저장 성공 ...
// 2. 다이얼로그 표시 시 인터페이스 메서드 호출
_dialogService.ShowNotification("시스템 알림", $"사용자 {_userName} 데이터 저장 완료!");
}
[RelayCommand]
private void Logout()
{
// 사용자 확인 요청
bool isConfirm = _dialogService.RequestConfirmation("로그아웃 확인", "로그아웃하시겠습니까?");
if (isConfirm)
{
UserName = "로그아웃";
}
}
}
4. 의존성 주입 구성 (시작 계층)
App.xaml.cs에서 서비스 등록 처리
public partial class ApplicationStartup : Application
{
public ApplicationStartup()
{
Services = ConfigureServices();
}
public IServiceProvider Services { get; }
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
// 서비스 등록: IDialogServiceInterface 요청 시 WpfDialogServiceImpl 제공
services.AddSingleton<IDialogServiceInterface, WpfDialogServiceImpl>();
// ViewModel 등록
services.AddTransient<MainViewModelClass>();
return services.BuildServiceProvider();
}
}
왜 이런 설계가 우수한가?
- UI 독립성 확보: MainViewModelClass에는 System.Windows 관련 코드 전혀 없음
- 테스트 용이성: MockDialogServiceImpl을 작성해 실제 UI 표시 없이 호출 여부 검증 가능
- 플랫폼 교체 유연성: WpfDialogServiceImpl 코드만 수정하면 ViewModel 변경 필요 없음
고급: 새 창 열기 시나리오
논리 구조는 동일합니다. IDialogServiceInterface를 확장해 ShowDetailWindow(object dataContext) 메서드 추가. Service 구현체에서는 새로운 창 생성 및 DataContext 설정 처리