Architecture

.NET WPF 모던 아키텍처 2026: 레거시를 벗어나는 실전 구성

Generic Host + DI + CommunityToolkit.Mvvm + .NET 9 기반으로 WPF 앱을 현대적으로 구성하는 방법. MVVM 보일러플레이트 제거, 테스트 가능한 구조, 마이그레이션 전략까지 실무 기준으로 정리한다.

이런 분이 읽으면 좋습니다

요약: WPF는 2006년에 출시됐지만 2026년에도 엔터프라이즈 데스크톱 앱의 핵심이다. 문제는 “WPF 자체”가 아니라 “2010년대 스타일로 작성된 WPF 코드”다. .NET 9 + Generic Host + CommunityToolkit.Mvvm 조합으로 WPF 앱을 현대적으로 구성하면, ASP.NET Core 수준의 DI·설정·로깅을 데스크톱에서 쓸 수 있고, MVVM 보일러플레이트를 90% 줄일 수 있다. 이 글은 새 WPF 프로젝트의 아키텍처 구성과 기존 프로젝트의 점진적 현대화 전략을 다룬다.

이 글은 WPF 앱을 유지보수하거나 신규 개발하는 C# 개발자를 위해 썼다. .NET Framework 4.x에서 .NET 9로의 마이그레이션을 고려하는 팀에도 참고가 된다.


2026년 WPF의 위치: 죽지 않았고, 대체되지도 않았다

“WPF는 레거시 아닌가요?” — 아니다. Microsoft는 .NET 9에서도 WPF를 적극 지원하고 있고, Windows 데스크톱 앱 시장에서 WPF의 점유율은 여전히 압도적이다. 금융 트레이딩 터미널, 의료 영상 뷰어, 산업 제어 시스템, 엔터프라이즈 관리 도구 등에서 WPF를 대체할 현실적 대안이 없는 경우가 많다.

WPF (.NET 9)WinUI 3MAUIAvalonia
성숙도 20년, 매우 안정성장 중, 일부 미완성성장 중, 모바일 중심성숙, 크로스플랫폼
Windows 지원 Win7~11 모두Win10 1809+만Win10+Win/Mac/Linux
생태계 최대 (Telerik, DevExpress 등)성장 중제한적성장 중
기존 자산 호환 기존 XAML 100% 호환XAML 유사하나 비호환XAML 유사하나 비호환유사 XAML, 비호환
DirectX 통합 DirectX 9/11DirectX 12 (WinAppSDK)플랫폼 의존Skia 기반
적합 시나리오 엔터프라이즈 LOB, 레거시 현대화Windows 전용 신규 앱크로스플랫폼 모바일+데스크톱크로스플랫폼 데스크톱
2026년 기준 .NET 데스크톱 UI 프레임워크 비교. WPF는 기존 자산 호환과 생태계에서 여전히 우위.

모던 WPF 아키텍처 구성 — 핵심 4계층

전체 구조

MyApp.sln
├── MyApp/                          # WPF 프로젝트 (진입점)
│   ├── App.xaml / App.xaml.cs      # Generic Host 통합
│   ├── Views/                      # XAML View 파일
│   ├── appsettings.json            # 설정 파일
│   └── Hosting/                    # Host 연결 코드
├── MyApp.Core/                     # 순수 C# 라이브러리 (UI 무관)
│   ├── ViewModels/                 # CommunityToolkit.Mvvm ViewModel
│   ├── Models/                     # 도메인 모델
│   ├── Services/                   # 비즈니스 로직
│   └── Interfaces/                 # 서비스 인터페이스
└── MyApp.Tests/                    # 단위 테스트
    └── ViewModelTests/

핵심 원칙: ViewModel과 비즈니스 로직은 WPF 참조 없는 순수 .NET 프로젝트에 분리한다. 이렇게 하면 ViewModel을 콘솔 앱이나 테스트 프로젝트에서 WPF 없이 실행할 수 있다.


1계층: Generic Host 통합 — App.xaml.cs

WPF의 Application 라이프사이클과 .NET Generic Host의 라이프사이클을 연결하는 것이 첫 번째 단계다.

App.xaml.cs csharp
public partial class App : Application { private readonly IHost _host; public App() { _host = Host.CreateDefaultBuilder() .ConfigureAppConfiguration((context, config) => { config.AddJsonFile("appsettings.json", optional: true); }) .ConfigureServices((context, services) => { // ViewModels services.AddTransient<MainViewModel>(); services.AddTransient<SettingsViewModel>(); // Services services.AddSingleton<IDataService, DataService>(); services.AddSingleton<INavigationService, NavigationService>(); // Views (Window는 Transient) services.AddTransient<MainWindow>(); // HttpClient services.AddHttpClient<IApiClient, ApiClient>(); // Logging services.AddLogging(builder => { builder.AddDebug(); builder.AddSerilog(); }); }) .Build(); } protected override async void OnStartup(StartupEventArgs e) { await _host.StartAsync(); var mainWindow = _host.Services.GetRequiredService<MainWindow>(); mainWindow.Show(); base.OnStartup(e); } protected override async void OnExit(ExitEventArgs e) { await _host.StopAsync(); _host.Dispose(); base.OnExit(e); } }

이 패턴의 이점:

  • DI 컨테이너 — 생성자 주입으로 의존성 관리, new 키워드로 직접 생성하지 않음
  • 설정 관리appsettings.json + 환경별 오버라이드 (appsettings.Development.json)
  • 로깅ILogger<T> 표준 인터페이스, Serilog/NLog 플러그인
  • HttpClientIHttpClientFactory 패턴으로 소켓 고갈 방지

2계층: CommunityToolkit.Mvvm — 보일러플레이트 제거

CommunityToolkit.Mvvm(이전 Microsoft.Toolkit.Mvvm)은 소스 생성기 기반의 MVVM 라이브러리다. 어트리뷰트만 붙이면 컴파일 타임에 코드가 자동 생성된다.

기존 방식 (수동 INotifyPropertyChanged):

// 30줄 — 필드 1개, 커맨드 1개에 이 양의 보일러플레이트
public class MainViewModel : INotifyPropertyChanged
{
    private string _name;
    public string Name
    {
        get => _name;
        set
        {
            if (_name != value)
            {
                _name = value;
                OnPropertyChanged(nameof(Name));
            }
        }
    }

    private ICommand _saveCommand;
    public ICommand SaveCommand => _saveCommand ??=
        new RelayCommand(Save, () => !string.IsNullOrEmpty(Name));

    private void Save() { /* ... */ }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string name) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

CommunityToolkit.Mvvm 방식:

MainViewModel.cs csharp
// 10줄 — 동일한 기능, 보일러플레이트 제로 public partial class MainViewModel : ObservableObject { [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand))] private string _name; [RelayCommand(CanExecute = nameof(CanSave))] private void Save() { /* ... */ } private bool CanSave() => !string.IsNullOrEmpty(Name); }

소스 생성기가 자동으로 만드는 것:

  • Name 프로퍼티 (get/set + PropertyChanged 이벤트 발화)
  • SaveCommand (IRelayCommand 구현)
  • CanExecute 연결 (NotifyCanExecuteChangedFor)

3계층: 서비스 계층 — 인터페이스 기반 설계

IDataService.cs csharp
// 인터페이스: MyApp.Core 프로젝트 public interface IDataService { Task<IReadOnlyList<Customer>> GetCustomersAsync(CancellationToken ct = default); Task<Customer> GetCustomerByIdAsync(int id, CancellationToken ct = default); Task SaveCustomerAsync(Customer customer, CancellationToken ct = default); }
DataService.cs csharp
// 구현: MyApp.Core 프로젝트 (또는 MyApp.Infrastructure) public class DataService : IDataService { private readonly IDbContextFactory<AppDbContext> _dbFactory; private readonly ILogger<DataService> _logger; public DataService( IDbContextFactory<AppDbContext> dbFactory, ILogger<DataService> logger) { _dbFactory = dbFactory; _logger = logger; } public async Task<IReadOnlyList<Customer>> GetCustomersAsync( CancellationToken ct = default) { await using var db = await _dbFactory.CreateDbContextAsync(ct); return await db.Customers .AsNoTracking() .OrderBy(c => c.Name) .ToListAsync(ct); } }

핵심 패턴:

  • 인터페이스 분리 — ViewModel은 IDataService에만 의존, 구현은 DI가 주입
  • 비동기 우선 — 모든 I/O 작업은 async/await, UI 스레드 블로킹 방지
  • CancellationToken — 사용자가 화면을 전환하면 진행 중인 쿼리를 취소 가능
  • DbContextFactoryDbContext를 매번 새로 생성하여 동시성 문제 방지

4계층: View — XAML은 얇게

MainWindow.xaml.cs csharp
public partial class MainWindow : Window { // View의 코드비하인드는 이것뿐 public MainWindow(MainViewModel viewModel) { InitializeComponent(); DataContext = viewModel; } }

View 원칙: 코드비하인드에 비즈니스 로직을 넣지 않는다. DataContext 설정과 순수 UI 조작(드래그&드롭, 포커스 이동 등 XAML로 할 수 없는 것)만 허용.


네비게이션: 화면 전환 패턴

WPF에서 화면 전환은 크게 두 가지 패턴이 있다.

ContentControl 교체Window 기반 네비게이션
구조 단일 Window + ContentControl.Content 교체화면별 별도 Window
적합 시나리오 탭/사이드바 기반 SPA 스타일다이얼로그, 독립 창
DI 연동 INavigationService가 ViewModel 교체Window를 DI로 생성
상태 관리 ViewModel 생존 주기 제어 용이Window 닫히면 상태 소멸
추천 대부분의 LOB 앱에 적합설정 창, 모달 다이얼로그에 적합
대부분의 엔터프라이즈 WPF 앱은 ContentControl 교체 패턴이 적합.

테스트: ViewModel은 WPF 없이 테스트

CommunityToolkit.Mvvm + 인터페이스 기반 설계의 가장 큰 이점은 ViewModel 단위 테스트가 WPF 참조 없이 가능하다는 것이다.

MainViewModelTests.cs csharp
public class MainViewModelTests { [Fact] public async Task LoadCustomers_SetsCustomerList() { // Arrange — Mock 서비스 주입 var mockService = Substitute.For<IDataService>(); mockService.GetCustomersAsync(default) .Returns(new[] { new Customer { Name = "Test" } }); var vm = new MainViewModel(mockService); // Act await vm.LoadCustomersCommand.ExecuteAsync(null); // Assert Assert.Single(vm.Customers); Assert.Equal("Test", vm.Customers[0].Name); } }

기존 WPF 프로젝트 현대화 전략

.NET Framework → .NET 9 마이그레이션 순서

Phase 1: 빌드 시스템 전환 (위험 낮음)

  • .csproj를 SDK 스타일로 변환 (dotnet try-convert 도구)
  • NuGet packages.configPackageReference
  • 이 시점에서는 여전히 .NET Framework 타겟

Phase 2: .NET 9 타겟 전환 (위험 중간)

  • <TargetFramework> 변경: net48net9.0-windows
  • 호환되지 않는 NuGet 패키지 대체
  • WCF → gRPC/REST, Remoting → 직접 통신으로 전환
  • Microsoft.Windows.Compatibility 패키지로 Win32 API 호환성 확보

Phase 3: 아키텍처 현대화 (위험 낮음, 점진적)

  • Generic Host + DI 도입 (App.xaml.cs 수정)
  • 한 ViewModel씩 CommunityToolkit.Mvvm으로 전환
  • 서비스 계층 인터페이스 추출 + DI 등록
  • 기존 싱글턴/정적 클래스 → DI 관리 서비스로 이관

Phase 4: 테스트 추가 (점진적)

  • 새로 전환한 ViewModel부터 단위 테스트 추가
  • 레거시 코드는 변경할 때만 테스트 추가 (보이스카우트 규칙)

WPF vs 전면 교체: 판단 기준

  • Win10 이상만 지원해도 되고, WinUI 3의 최신 컨트롤이 필요한 경우
  • 크로스플랫폼이 필수인 경우 (Mac/Linux → Avalonia)
  • 모바일까지 확장해야 하는 경우 (→ MAUI)
  • 기존 WPF 코드가 거의 없고 신규 개발에 가까운 경우

그 외에는 기존 WPF + .NET 9 + Generic Host + CommunityToolkit.Mvvm이 2026년 가장 현실적인 선택이다.


피해야 할 안티패턴


결론: WPF는 레거시가 아니라 “레거시 패턴으로 작성된 WPF”가 레거시다

2026년 WPF 모던 아키텍처의 핵심 스택:

  1. .NET 9 — 최신 런타임, 성능 향상, C# 13 문법
  2. Generic Host — DI + Configuration + Logging 통합
  3. CommunityToolkit.Mvvm — 소스 생성기로 MVVM 보일러플레이트 제거
  4. EF Core + DbContextFactory — 비동기 데이터 액세스
  5. xUnit + NSubstitute — ViewModel 단위 테스트

이 구성이면 ASP.NET Core 프로젝트와 동일한 수준의 구조, 테스트 가능성, 유지보수성을 데스크톱 앱에서 달성할 수 있다. WPF 자체를 바꿀 필요가 아니라, WPF 위에서 작성하는 코드의 구조를 바꾸면 된다.

다음에 읽을 글