From 8419bf30ff70f9bbf599e4d49fae466d41e735ff Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Mon, 1 Jun 2026 23:30:06 +0900 Subject: [PATCH] =?UTF-8?q?refactor(P2):=20DI=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=EC=85=98=20=EB=A3=A8=ED=8A=B8=20+=20Scrutor=20?= =?UTF-8?q?=EC=96=B4=EC=85=88=EB=B8=94=EB=A6=AC=20=EC=8A=A4=EC=BA=94=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 컴파일타임 하드코딩(Bootstrap 14개 new + 수동 생성자 와이어링)을 제거 — 새 Provider는 클래스만 추가하면 자동 등록되고 그래프가 변환 엣지를 자동 흡수(확장성 토대). Strangler Fig 파사드로 호출부 무변경. - Core/ServiceCollectionExtensions.AddEverything2Everything: Scrutor FromAssembliesOf → AssignableTo → AsSelfWithInterfaces(단일 싱글턴 공유) → Singleton. Heic(Magick)/Docx·Ocr·Hwpx(Pdf)/Llm(Settings) 생성자 의존 자동 해소. ProviderRegistry/Engine 등록. - Bootstrap.CreateDefault: 내부를 DI(BuildServiceProvider→GetRequiredService)로 교체하되 시그니처·동작 불변 — App/CLI/테스트 호출부 전부 무파손. - 패키지: Microsoft.Extensions.DependencyInjection 9.0.0, Scrutor 4.2.2 (CPM 경유). - DependencyInjectionTests: 14개 전수 해소 + 생성자 의존 자동 와이어링 + AsSelf 동일 인스턴스 + 공유 Settings + 파사드 하위호환 검증(적대적 리뷰의 'AsSelf 누락 silent 실패' 방어). App.xaml.cs Generic Host 전환·서비스로케이터 제거는 GUI 스모크가 필요해 P5(MVVM 생성자 주입)로 통합. 69개 테스트 전부 그린(65+4), 빌드 0경고/0오류, 골든마스터 녹색 유지(동작 불변). --- src/Directory.Packages.props | 2 + .../Everything2Everything.Core.csproj | 2 + .../Everything2EverythingBootstrap.cs | 32 +++------ .../ServiceCollectionExtensions.cs | 41 +++++++++++ .../DependencyInjectionTests.cs | 71 +++++++++++++++++++ 5 files changed, 126 insertions(+), 22 deletions(-) create mode 100644 src/Everything2Everything.Core/ServiceCollectionExtensions.cs create mode 100644 src/Everything2Everything.Tests/DependencyInjectionTests.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 2ca02e4..3f2b098 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -26,6 +26,8 @@ + + diff --git a/src/Everything2Everything.Core/Everything2Everything.Core.csproj b/src/Everything2Everything.Core/Everything2Everything.Core.csproj index 8d815f3..6a28fb8 100644 --- a/src/Everything2Everything.Core/Everything2Everything.Core.csproj +++ b/src/Everything2Everything.Core/Everything2Everything.Core.csproj @@ -26,6 +26,8 @@ + + diff --git a/src/Everything2Everything.Core/Everything2EverythingBootstrap.cs b/src/Everything2Everything.Core/Everything2EverythingBootstrap.cs index b13cff2..65c8ef6 100644 --- a/src/Everything2Everything.Core/Everything2EverythingBootstrap.cs +++ b/src/Everything2Everything.Core/Everything2EverythingBootstrap.cs @@ -1,31 +1,19 @@ -using Everything2Everything.Core.Providers; +using Microsoft.Extensions.DependencyInjection; namespace Everything2Everything.Core; public static class Everything2EverythingBootstrap { + /// + /// 기본 변환 엔진을 구성한다. 내부적으로 DI 컨테이너()로 + /// 모든 Provider를 어셈블리 스캔 자동 등록하고 ConversionEngine을 해소한다. + /// 기존 호출부(App/CLI/테스트) 하위호환을 위한 얇은 파사드 — 시그니처·동작 불변. + /// public static ConversionEngine CreateDefault(ISettingsStore? settings = null) { - settings ??= new DpapiSettingsStore(); - var magick = new Converters.MagickProvider(); - var pdf = new Converters.PdfProvider(); - var providers = new IConverterProvider[] - { - magick, - new Converters.HeicProvider(magick), - pdf, - new Converters.PdfToolProvider(), - new Converters.DocxProvider(pdf), - new Converters.HtmlProvider(), - new Converters.HwpxProvider(), - new Converters.OcrProvider(pdf), - new Converters.DocumentProvider(), - new Converters.DataProvider(), - new Converters.VectorProvider(), - new Converters.ImageOptimProvider(), - new Converters.LlmProvider(settings), - new Converters.FfmpegProvider(), - }; - return new ConversionEngine(new ProviderRegistry(providers)); + var services = new ServiceCollection(); + services.AddEverything2Everything(settings); + var provider = services.BuildServiceProvider(); + return provider.GetRequiredService(); } } diff --git a/src/Everything2Everything.Core/ServiceCollectionExtensions.cs b/src/Everything2Everything.Core/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..f5b60f8 --- /dev/null +++ b/src/Everything2Everything.Core/ServiceCollectionExtensions.cs @@ -0,0 +1,41 @@ +using Everything2Everything.Core.Providers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Everything2Everything.Core; + +/// +/// 변환 엔진의 DI 컴포지션 루트. 모든 IConverterProvider 구현을 어셈블리 스캔(Scrutor)으로 자동 등록해 +/// 컴파일타임 하드코딩(Bootstrap의 14개 new + 수동 생성자 와이어링)을 제거한다. +/// 새 Provider는 클래스만 추가하면 자동 등록되고, 그래프가 그 변환 엣지를 자동 흡수한다(확장성 토대). +/// +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddEverything2Everything( + this IServiceCollection services, + ISettingsStore? settings = null) + { + // 설정 저장소: 호출자가 공유 인스턴스(App의 DPAPI 저장소)를 주면 그걸 등록 — 키 변경이 LlmProvider에 즉시 반영. + // 없으면 기본 DPAPI 저장소를 단 1회 등록. + if (settings is not null) + services.AddSingleton(settings); + else + services.TryAddSingleton(); + + // IConverterProvider 구현 전수 자동 등록. AsSelfWithInterfaces = + // 구체 타입을 단일 싱글턴으로 등록 + IConverterProvider는 그 인스턴스로 포워드한다. + // → Heic(MagickProvider)/Docx·Ocr·Hwpx(PdfProvider)/Llm(ISettingsStore) 생성자 의존이 + // 동일 싱글턴 인스턴스로 자동 해소된다(다중 생성자는 MS.DI가 해소 가능한 최다 매개변수 생성자 선택). + services.Scan(scan => scan + .FromAssembliesOf(typeof(IConverterProvider)) + .AddClasses(c => c.AssignableTo(), publicOnly: false) + .AsSelfWithInterfaces() + .WithSingletonLifetime()); + + // 그래프 빌더: 등록된 모든 IConverterProvider로부터 변환 그래프를 합성. + services.AddSingleton(sp => new ProviderRegistry(sp.GetServices())); + services.AddSingleton(); + + return services; + } +} diff --git a/src/Everything2Everything.Tests/DependencyInjectionTests.cs b/src/Everything2Everything.Tests/DependencyInjectionTests.cs new file mode 100644 index 0000000..a0969f5 --- /dev/null +++ b/src/Everything2Everything.Tests/DependencyInjectionTests.cs @@ -0,0 +1,71 @@ +using System.Linq; +using Everything2Everything.Core; +using Everything2Everything.Core.Converters; +using Everything2Everything.Core.Providers; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Everything2Everything.Tests; + +/// +/// DI 컴포지션 루트(AddEverything2Everything) 등록 규약 검증. Scrutor 자동 등록이 하드코딩 14개와 +/// 동일하게 모든 Provider를 해소하고, 생성자 의존(Magick/Pdf/Settings)이 자동 와이어링되는지 보증한다. +/// 적대적 리뷰 지적: AsSelf 누락 시 Heic/Docx/Ocr 의존 해소가 silent 실패 → 일부 Provider 런타임 누락. +/// +public class DependencyInjectionTests +{ + [Fact] + public void AddEverything2Everything_RegistersAll14Providers() + { + var services = new ServiceCollection(); + services.AddEverything2Everything(); + using var sp = services.BuildServiceProvider(); + + var providers = sp.GetServices().ToList(); + Assert.Equal(14, providers.Count); // 하드코딩 14개와 동일 — Scrutor 누락/초과 방어 + + Assert.NotNull(sp.GetRequiredService()); + Assert.NotNull(sp.GetRequiredService()); + } + + [Fact] + public void ConstructorDependencies_AreAutoWired() + { + var services = new ServiceCollection(); + services.AddEverything2Everything(); + using var sp = services.BuildServiceProvider(); + + // 이들이 해소되면 생성자 의존이 DI로 연결된 것(AsSelf 미등록 시 여기서 실패). + Assert.NotNull(sp.GetRequiredService()); // ← MagickProvider 주입 + Assert.NotNull(sp.GetRequiredService()); // ← PdfProvider 주입 + Assert.NotNull(sp.GetRequiredService()); // ← PdfProvider 주입 + Assert.NotNull(sp.GetRequiredService()); // ← PdfProvider 주입 + Assert.NotNull(sp.GetRequiredService()); // ← ISettingsStore 주입 + + // AsSelfWithInterfaces: 구체 타입과 인터페이스가 동일 싱글턴 인스턴스를 공유. + var magickAsSelf = sp.GetRequiredService(); + var magickAsInterface = sp.GetServices().OfType().Single(); + Assert.Same(magickAsSelf, magickAsInterface); + } + + [Fact] + public void SharedSettings_AreRegisteredInstance() + { + // App이 공유 ISettingsStore를 주입하면 그 인스턴스가 등록되어야 한다(LlmProvider 키 공유). + var settings = new DpapiSettingsStore(); + var services = new ServiceCollection(); + services.AddEverything2Everything(settings); + using var sp = services.BuildServiceProvider(); + Assert.Same(settings, sp.GetRequiredService()); + } + + [Fact] + public void CreateDefault_Facade_StillWorks() + { + // 파사드 하위호환: DI 내부전환 후에도 CreateDefault가 동일하게 엔진/그래프를 구성. + var engine = Everything2EverythingBootstrap.CreateDefault(); + Assert.Equal(14, engine.Providers.All.Count); + Assert.NotNull(engine.Providers.Graph.FindBestPath(".png", ".jpg")); + Assert.NotNull(engine.Providers.Graph.FindBestPath(".svg", ".jpg", maxHops: 3)); + } +}