WPF MVVM 프로젝트에서 유지보수성과 확장성을 동시에 잡는 폴더 구조, 네임스페이스/어셈블리 분리, 의존성 역전 원칙, DI 설정 패턴을 실무 예제와 다이어그램으로 정리했습니다.
🧭 목표
- 명확한 레이어 분리로 테스트와 확장 용이성 확보
- 네임스페이스 = 물리 구조를 일치시켜 탐색/리팩터링 비용 최소화
- 기능 확장 시 의존 방향을 지키는 안전한 가드레일 마련
🏗️ 솔루션(멀티 프로젝트) 권장 구조
MyApp
├─ MyApp.Presentation # WPF(UI): Views, ViewModels, XAML 등
├─ MyApp.Application # UseCase/서비스 오케스트레이션, 포트(인터페이스)
├─ MyApp.Domain # 순수 도메인 모델/규칙(의존성 0)
├─ MyApp.Infrastructure # 외부 어댑터: Communication, Persistence, Logging
├─ MyApp.Plugins # 선택: 장치/시퀀스 플러그인 모듈
└─ MyApp.Tests # Unit/Integration 테스트
네임스페이스 매핑 규칙
- 프로젝트 폴더 = 루트 네임스페이스. 예)
MyApp.Domain.Models.TestStep - 폴더 깊이는 최대 3레벨로 제한:
Communication/Serial/RsBox - 공개 API만
public, 내부 구현은internal+InternalsVisibleTo(Tests)로 노출 관리
🔌 의존성 방향(다이어그램)

원칙: Presentation과 Infrastructure는 Application의 인터페이스에 의존합니다. Domain은 아무것에도 의존하지 않습니다.
📁 폴더 상세(실무형)
MyApp.Presentation (WPF)
/Views # *.xaml
/ViewModels # *.cs (INPC, Commands)
/Converters # IValueConverter
/Behaviors # Interaction behaviors
/Controls # 재사용 UI
/Resources # 이미지, 문자열
/Styles # Theme/Dictionary
/Templates # Data/ControlTemplate
네임스페이스 예
MyApp.Presentation.ViewsMyApp.Presentation.ViewModels
MyApp.Application
/Services # 고수준 유스케이스(시퀀스, 리포트 오케스트레이션)
/Interfaces # 포트: ITestSequenceService, IReportService, IDeviceClient
/Messaging # Mediator/EventAggregator 계약
/Configuration # 옵션/설정 스키마
네임스페이스 예: MyApp.Application.Services, .Interfaces
MyApp.Domain
/Models # TestStep, RunResult, InstrumentConfig
/ValueObjects # Strong-typed 값
/Enums # 상태/에러코드 등
/Policies # 비즈니스 규칙(순수 C#)
네임스페이스 예: MyApp.Domain.Models
MyApp.Infrastructure
/Communication # 장치/프로토콜 클라이언트
/Serial
/Tcp
/Usb
/Can
/Persistence # 파일/DB/Repo
/Csv
/Sqlite
/Logging # Serilog/NLog 래퍼
네임스페이스 예: MyApp.Infrastructure.Communication.Serial
MyApp.Plugins (선택)
- 서드파티/사내용 장치 모듈을 별도 패키지로 분리해 배포/버전 관리 용이
🧪 테스트 프로젝트 구성
MyApp.Tests
├─ Unit
│ ├─ Presentation (ViewModel 테스트)
│ ├─ Application (서비스/유스케이스)
│ └─ Domain (정책/VO)
└─ Integration
└─ Infrastructure (장치 시뮬레이터 연동)
InternalsVisibleTo("MyApp.Tests")로 내부 구현 접근
🪄 DI(의존성 주입) 배선 패턴
Application의 인터페이스를 Infrastructure가 구현하고, Presentation의 App.xaml.cs에서 묶습니다.
// MyApp.Presentation/App.xaml.cs
using Microsoft.Extensions.DependencyInjection;
using MyApp.Application.Interfaces;
using MyApp.Application.Services;
using MyApp.Infrastructure.Communication.Serial;
var services = new ServiceCollection();
// Application 레이어 서비스
services.AddSingleton<ITestSequenceService, TestSequenceService>();
// Infrastructure 구현체 바인딩
services.AddSingleton<IDeviceClient, RsBoxDeviceClient>();
// ViewModel 등록
services.AddTransient<RunnerViewModel>();
var provider = services.BuildServiceProvider();
var main = new MainWindow { DataContext = provider.GetRequiredService<RunnerViewModel>() };
main.Show();
팁: 규모가 커지면 Prism/Generic Host를 도입해 모듈/Region/설정 라이프사이클을 관리하세요.
🧩 네임스페이스 & 파일 네이밍 컨벤션
- 클래스/인터페이스:
PascalCase, 인터페이스는I* - 비동기 메서드:
*Async - 커맨드:
*Command(ex.RunCommand), Toolkit이면RelayCommand/AsyncRelayCommand - 파일명 = 타입명 1:1 (partial 사용 시
.Part.cs접미) - View ↔ ViewModel 1:1:
RunnerView.xaml↔RunnerViewModel.cs
🧱 레이어 가드(컴파일 타임 제어)
Presentation→Application만 참조,Infrastructure직접 참조 금지- Directory.Build.props로 불필요 참조 방지, Analyzer로 순환 참조 감시
<!-- Solution 루트/Directory.Build.props -->
<Project>
<ItemGroup>
<ProjectReference Update="MyApp.Presentation\MyApp.Presentation.csproj">
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
</ItemGroup>
</Project>
🧭 기능 폴더 vs 레이어 폴더
- 레이어 기반(위 구조): 공통 규칙·테스트 용이. 대규모/멀티 장치에 적합.
- 기능(Feature) 기반: 기능 단위로 View/ViewModel/Service를 한 폴더에. 소규모 팀에서 생산성↑
혼합 전략: 상위는 레이어, 하위
Application/Services/Sequences/<Feature>로 기능별 하위 폴더.
🔧 예시: RS-Box 채널 On/Off 최소 단위 구조
MyApp
├─ MyApp.Application
│ ├─ Interfaces/IDeviceClient.cs
│ └─ Services/TestSequenceService.cs
├─ MyApp.Domain
│ └─ Models/ChannelState.cs
├─ MyApp.Infrastructure
│ └─ Communication/Serial/RsBoxDeviceClient.cs
└─ MyApp.Presentation
├─ ViewModels/RunnerViewModel.cs
└─ Views/RunnerView.xaml
RunnerViewModel→ITestSequenceService.RunAsync()호출TestSequenceService→IDeviceClient로 RS-Box에 프레임 전송
🧯 실수 방지 체크리스트
- ViewModel에 비즈니스 규칙을 넣지 않는다(도메인/서비스로 이동)
-
Infrastructure타입이 XAML에 직접 등장하지 않는다 -
async void금지(이벤트 핸들러 제외) - 바인딩 경로 오타 방지:
nameof(Property)활용 -
ObservableCollection<T>는 UI 스레드에서 변경
