🗂️ WPF 폴더 구조 & 네임스페이스 전략 — MVVM 실무편

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.Views
    • MyApp.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 스레드에서 변경

    📚 함께 읽으면 좋은 글

    댓글 달기

    이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

    위로 스크롤