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));
+ }
+}