MASTER PLAN · Single Source of Truth

모든 변환을 엣지로, 엔진이 경로를 합성하는 만능 변환기

손으로 짠 변환 switch를 자동 경로 탐색 그래프로 교체하고, 그 위에 PDF 압축·영상·HWP·AI를 엣지로 얹어, 코드 한 줄당 N×M 매트릭스가 발현하는 '변환하면서 더 좋아지는' 만능 변환기.

생성 2026-06-01
방법 멀티에이전트 Workflow
에이전트 17
서브에이전트 토큰 1,426,291
도구 호출 367
종합 idx2(AI·미디어 우선, 89점) 실행 골격 + idx0(그래프 코어, 84점) 아키텍처 영혼
00

북극성 비전

Everything2Everything의 북극성은 "세상의 모든 변환을 원자(atomic) 엣지로 등록하면, 엔진이 그 조합으로 임의의 A→Z를 스스로 합성하고, 변환하면서 AI가 결과를 더 좋게 만드는 변환 그래프 OS"다. 핵심 통찰은 현재 DocumentProvider.RouteAsync(92-205)가 사실상 '사람이 손으로 그린 Dijkstra'(md→html→docx, docx→html→md, hwp→html→md를 switch에 박아넣음)라는 점이며, 이 손그림을 삭제하고 엔진이 같은 경로를 '계산'하게 만드는 것이 모든 확장의 열쇠다. 그래프가 코어가 되면 FFmpeg(미디어), PDF 압축, HWP 양방향, AI 요약/번역/캡션이 전부 '엣지 추가'로 환원되고, OutputsForInput은 1-hop 직접 출력에서 도달 가능한 모든 포맷(transitive closure)으로 폭발한다. 동시에 사용자가 명시한 신규 가치(PDF 압축·HWP·영상·AI)를 인프라 완성을 기다리지 않고 빠르게 출시해 체감 차별화를 먼저 만든다. AI는 핵심 엔진이 아니라 '키 없으면 조용히 비활성되는 부가가치 엣지'로, 변환의 로컬 예측가능성이라는 신뢰를 절대 깨지 않는다.

01

설계 원칙

아키텍처를 관통하는 불변식. 모든 코드 변경은 이 원칙을 위배하지 않아야 한다.

P01

변환은 엣지, 엔진은 라우터

모든 Provider는 단일 홉 원자 변환(md→html, png→pdf)만 선언한다. 멀티홉(md→docx)은 절대 Provider 내부에 손으로 짜지 않고 엔진의 그래프 탐색이 자동 합성한다. DocumentProvider.RouteAsync의 switch 지옥이 재발하지 않도록 이를 불변식으로 강제한다.

P02

기능이 그래프를 견인하되, 그래프가 기능을 받친다

사용자 체감 가치(PDF 압축·HWP·영상·AI)를 빠르게 출시하되, 신규 기능은 반드시 '그래프 엣지'로만 추가한다. Phase 0에 심은 그래프 코어가 하드코딩 유혹을 구조적으로 차단한다.

P03

손실은 가중치다

품질 손실을 ConversionPair.LossClass(Lossless/Container/Recode/Rasterize)로 SSOT화하고 -log(보존율)+홉페널티로 환산한다. 멀티홉 경로 선택과 UI '손실 변환' 경고 배지가 모두 이 단일 출처를 소비한다.

P04

AI는 끄면 사라지는 부가 엣지

AI는 절대 기본 경로를 점유하지 않는다. 키가 없으면 모든 기존 변환은 100% 동작하고 AI 페어만 자동 비활성(NotReady)되며 ✨AI 배지로만 opt-in 노출된다. 변환의 로컬 예측가능성 신뢰를 깨지 않는다.

P05

무거운 외부 도구는 분리 호출로만

FFmpeg(GPL 정적링크 금지·LGPL 분리호출만), Ghostscript/MuPDF(AGPL·사용자 설치본 감지만), H2Orestart/Calibre(GPL·외부 프로세스 분리)를 본체에 절대 정적 링크하지 않는다. 라이선스 경계를 코드 리뷰 게이트로 강제해 상업 배포 오염을 원천 차단한다.

P06

순수 .NET 우선, 외부 바이너리 차선

단일 포터블 EXE 부담을 줄이기 위해 SharpCompress·Parquet.Net·PDFsharp·Svg.Skia 같은 순수 관리 코드를 EXE에 직접 포함하고, FFmpeg/Pandoc/Calibre 같은 무거운 바이너리는 '외부 설치 감지 + 미설치 시 안내/자동조달' 모델로만 통합한다.

P07

점진 마이그레이션, 무중단

IConverterProvider/ConvertResult/ConvertOptions 일반화는 기존 8개 Provider를 어댑터로 감싸 한 번에 깨지지 않게 한다. 모든 코어 변경은 회귀 테스트(현재 0개에서 출발)로 '동일 동작'을 객관 증명한다.

02

타깃 아키텍처

4계층 변환 그래프 아키텍처. 핵심은 ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격하는 것.

4계층 변환 그래프 아키텍처. (1) Abstractions 계층이 Provider 계약을 담고, (2) 그래프 코어가 모든 원자 변환을 방향 그래프로 합성해 Dijkstra로 멀티홉 경로를 푼다. (3) Provider 계층은 in-box 코드 Provider(이미지/문서/미디어/AI)와 manifest 기반 외부 도구 어댑터로 나뉘며, (4) 실행 계층(ExternalProcessRunner·ISettingsStore)이 외부 프로세스·설정·키를 횡단 관리한다. 핵심은 ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격하는 것이다.

Abstractions 계층 (Everything2Everything.Abstractions)
Provider 계약을 별도 어셈블리로 분리해 타입 동일성을 보장하고 향후 플러그인의 안정적 참조점을 제공
IConverterProviderConvertRequest/ConvertContextConvertResult(비파일 산출물 포함)ProviderCapabilityConversionPair+LossClassExternalDependency
그래프 코어 계층 (Core.Graph)
모든 Provider Capability를 순회해 방향 그래프(노드=확장자, 엣지=Provider+LossClass 가중치)를 빌드하고, 자체 Dijkstra로 최저손실 멀티홉 경로를 탐색·실행
ConversionGraphPathFinder(자체 Dijkstra)ChainExecutor(ExecuteChainAsync)ConversionEngine(라우터로 축소)ProviderRegistry(증분 등록 Register/Rebuild)
Provider 계층
단일 홉 원자 변환 능력을 선언·실행. in-box 코드 Provider와 manifest 어댑터 Provider 공존
MagickProvider/PdfProvider/HtmlProvider(기존)LlmProvider(AI)FfmpegProvider(미디어)PdfToolProvider(압축)ImageCombineProvider(N→1)ExternalToolProvider(manifest 어댑터 베이스)
실행/인프라 계층
외부 프로세스 실행·설정 영속화·키 보안·미리보기를 횡단 제공
ExternalProcessRunner(CliWrap, 타임아웃+stderr+Kill)ISettingsStore(DPAPI 암호화)ExternalToolDetector(번들 경로 폴백)IPreviewRenderer(프리뷰 캐시)ManifestLoader
데이터 흐름

파일 입력 → ConversionEngine.ConvertOneAsync가 입력/출력 확장자 정규화 → ConversionGraph.FindBestPath(in,out,options)로 경로 탐색(직접 엣지 있으면 1홉, 없으면 손실가중치 기반 멀티홉) → ChainExecutor가 경로의 각 홉을 순차 실행하며 중간 산출물을 공용 workDir(Temp/e2e_{Guid})에 체이닝 → 각 홉은 Provider.ConvertAsync(ConvertRequest) 호출, 진행률은 홉 수로 분할 매핑 → 마지막 홉 산출물을 OutputPathHelper로 충돌 해결 후 최종 출력 → ConvertResult(출력 경로 + 비파일 산출물) 반환, 중간 산출물 정리. AI/외부도구 엣지는 CheckAvailabilityAsync 게이트를 먼저 통과해야 그래프에 활성 노드로 참여.

03

핵심 아키텍처 결정 (ADR)

현재 코드의 구체적 한계를 직접 겨냥한 결정. 클릭하면 근거·대안·트레이드오프가 펼쳐진다.

ADR-1ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격
결정

_byPair 단일 룩업(ProviderRegistry.cs:6,43)을 유지하되 그 위에 인접 리스트 그래프(Dictionary<string,List<Edge>>)를 빌드하고, 외부 의존성 없는 자체 Dijkstra(80~120줄, .NET 9 PriorityQueue 사용)로 멀티홉 경로를 탐색한다. DocumentProvider.RouteAsync의 손그림 멀티홉을 엔진 합성으로 대체.

근거

현재 멀티홉이 Provider 내부 switch에 하드코딩되어 형식 N개에 O(N²)로 수동 증식한다. NCSA Polyglot 모델(노드=포맷, 엣지=Provider, 가중치=손실)은 학계 검증된 best practice이며, 그래프가 수십 노드·수백 엣지 규모라 성능 이슈가 없다.

대안

QuikGraph(MS-PL, 2022 이후 정체)·Pandoc식 단일 AST 허브(이질적 도메인에 부적합). 자체 구현이 단일 EXE/AOT/라이선스 검토 모두 무부담이라 1순위.

트레이드오프

멀티홉은 중간 임시파일 I/O가 늘고 손실이 누적될 수 있다. 완화: 직접 엣지 우선, MaxHops=3 제한, 손실 블랙리스트, 손실 경로 UI 경고 배지.

ADR-2손실을 ConversionPair.LossClass 가중치로 SSOT화
결정

ConversionPair에 LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8) 필드를 추가하고, 엣지 가중치를 -log(품질보존율)+홉페널티+실행비용 합산으로 계산한다. UI '손실 변환' 배지도 이 가중치를 소비.

근거

손실은 본래 곱셈적(0.9×0.8)이므로 -log 변환으로 덧셈 최단경로(Dijkstra)가 곧 최대 품질보존 경로가 된다. 래스터화(텍스트/벡터→PNG)는 단방향 손실 절벽이므로 큰 페널티로 자연 회피.

대안

동적 손실 측정(Versus식 실측). 초기엔 정적 가중치 테이블로 시작하고 동적 측정은 처음부터 넣지 않는다(과도한 복잡도).

트레이드오프

정적 가중치는 추정값이라 일부 쌍에서 비최적 경로 가능. 완화: 보수적으로 직접 엣지 우선, 멀티홉은 fallback으로만 운영.

ADR-3레지스트리 충돌을 조용한 first-wins에서 Priority 기반 명시 선택으로 교체
결정

_byPair.TryAdd(ProviderRegistry.cs:22)의 '조용한 첫 등록자 우선'을 ProviderCapability.Priority 필드 + 다중 Provider 공존 모델 + 충돌 시 진단 경고로 교체한다. 같은 (input,output)에 빠른변환/고품질/AI 등 복수 전략 등록 허용.

근거

PDF압축 vs PDF렌더, AI변환 vs 일반변환처럼 한 쌍에 복수 전략이 필연적으로 생긴다. 현재는 부트스트랩 순서에 따라 비결정적으로 한쪽이 조용히 사라져 데이터 손실이다.

대안

현 first-wins 유지(확장 불가). 비용 기반 자동 선택만(사용자 전략 선택 불가). Priority+공존이 그래프 가중치와도 자연 연결.

트레이드오프

같은 쌍에 복수 Provider가 등록되면 UI에서 전략 선택지를 노출해야 하는 추가 복잡도. 완화: 기본은 최저비용 자동 선택, 고급 모드에서만 명시 선택.

ADR-4IConverterProvider 시그니처를 ConvertRequest/ConvertContext로 일반화
결정

단일 sourcePath/단일 outputExtension/IProgress<double> 고정 시그니처(IConverterProvider.cs:9-15)를 ConvertRequest(다중 입력·옵션 백·미디어 메타) + ConvertContext로 일반화하고, ConvertResult(ConvertResult.cs)에 ExtractedText/AiResponse/Metadata/IntermediateArtifacts 필드를 추가한다. 기존 8개 Provider는 어댑터로 감싸 무중단 마이그레이션.

근거

현 시그니처는 N→1 결합, AI 비파일 응답, 영상 메타데이터 프로빙, 멀티홉 중간 컨텍스트를 표현할 수 없다. 미디어/AI 엣지가 들어올 '그릇'을 코어에 먼저 판다.

대안

시그니처 유지하고 옵션에 모든 것 욱여넣기(갓 오브젝트 가속). 점진 어댑터 전략이 8개 Provider 동시 파괴를 방지.

트레이드오프

어댑터 계층이 일시적 중복을 만든다. 완화: 회귀 테스트로 동일 동작 보장 후 어댑터를 점진 제거.

ADR-5AI는 IAiProvider 특수 인터페이스가 아니라 그래프의 부가 엣지로 편입
결정

LlmProvider를 일반 IConverterProvider로 구현하고 Microsoft.Extensions.AI(IChatClient) 추상화 위에 OpenAI/Anthropic 공식 SDK를 연결한다. AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출되고, 키 부재 시 CheckAvailabilityAsync가 NotReady를 반환해 그래프에서 자동 비활성된다.

근거

AI를 특수 카테고리로 두면 그래프·레지스트리 밖에 별도 배관이 생긴다. 엣지로 환원하면 OcrProvider가 Windows OCR을 흡수한 선례처럼 매트릭스에 자연 편입되고, AI 후처리 파이프(OCR→LLM 교정)도 멀티홉으로 자동 합성된다.

대안

별도 IAiConverterProvider 확장(추상화 분기 증가). 통합 IConverterProvider가 단순하고 그래프와 정합.

트레이드오프

AI는 비결정적·유료·네트워크 의존이라 '재현 가능한 변환'과 충돌. 완화: ✨AI 배지·기본 경로 불점유·키 없으면 비활성 불변식.

ADR-6무거운 외부 도구는 분리 프로세스 호출 + 라이선스 게이트로만 통합
결정

FFmpeg는 BtbN lgpl-shared 빌드를 별도 프로세스로 호출(LGPL 준수), Ghostscript/MuPDF는 AGPL이라 사용자 설치본 감지만, H2Orestart/Calibre/Pandoc은 GPL이라 외부 프로세스 분리. 공통 ExternalProcessRunner(CliWrap, 타임아웃+stderr+Kill)로 통일하고, 라이선스 경계를 코드 리뷰 게이트로 강제한다.

근거

단일 포터블 EXE 상업 배포에서 GPL/AGPL 바이너리 정적 링크는 즉시 라이선스 오염이다. 이미 LibreOffice를 외부 도구로 다루는 검증된 패턴을 그대로 확장.

대안

GPL 빌드 번들(라이선스 위반)·상업 라이선스 구매(비용). 분리 호출 + 사용자 설치 감지/LGPL 자동조달이 안전.

트레이드오프

진정한 자족 EXE가 아니라 외부 의존 체인이 길어진다. 완화: 순수 .NET 라이브러리 우선, 외부 도구는 NotReady로 친절히 안내.

ADR-7CombineAsync를 ImageCombineProvider(N→1 엣지)로 분리
결정

ConversionEngine.CombineAsync의 ImageMagick 직접 의존(ConversionEngine.cs:2,164-257)과 정적 HashSet(CombinableInputs/Outputs:14-23)을 IMultiInputProvider 추상화로 분리한다. 엔진은 라이브러리 중립이 되고 결합 가능 형식은 Provider 능력 선언으로 통합.

근거

현재 '결합'이 Provider 추상화 밖에 있어 엔진이 ImageMagick에 결합되고, PDF 병합·동영상 concat·오디오 믹스 같은 비이미지 결합으로 확장 불가하다. 정적 HashSet과 능력 선언의 이중 관리도 해소.

대안

현 구조 유지(이미지 결합만 영구 고착). N→1 추상화가 모든 결합을 동일 패턴으로 흡수.

트레이드오프

결합 진행률 보고가 단일 출력 가정과 달라 재설계 필요. 완화: ConvertProgress를 N→1 케이스로 확장.

ADR-8ConvertOptions 갓 오브젝트를 그래프 옵션 + 형식별 옵션 백으로 분해
결정

11개 sub-record 갓 오브젝트(ConvertOptions.cs:35-55)를 그래프 옵션(AllowMultiHop/MaxHops/AvoidLossy) + 형식별 옵션 백(IReadOnlyDictionary 또는 Provider 선언형 스키마)으로 분해한다. Video/Audio/Ai/PdfCompress를 sub-record 증식 없이 수용하고 ISettingsStore(DPAPI 암호화)로 영속화.

근거

형식 추가마다 sub-record가 비대해지고 모든 Provider가 무관한 옵션을 끌고 다닌다. 영상 코덱·AI 프롬프트·PDF 압축 레벨을 담을 자리가 코어 record 증식 없이 필요하다.

대안

sub-record 계속 추가(god object 가속). 옵션 백이 형식별 옵션만 주입해 확장성 확보.

트레이드오프

강타입 안전성이 약화된다. 완화: Provider가 옵션 스키마(이름/타입/범위/기본값)를 선언하고 UI가 동적 생성·검증.

ADR-9manifest는 풀 DSL이 아니라 단순 CLI용 선언적 인자 템플릿으로 제한 채택
결정

manifest를 ExternalProcessRunner 위의 '선언적 인자 템플릿({input}/{output}/{outdir}/{format})'으로만 좁게 채택해 qpdf/Ghostscript 같은 단순 CLI 압축 도구를 코드 없이 추가한다. 복잡 로직(FFmpeg HW가속 폴백·AI)은 in-box 코드 Provider 원칙을 P1부터 못박는다.

근거

Provider 8개·테스트 0개 단일 개발자 프로젝트에 풀 manifest DSL·동적 ALC 로더는 ROI가 낮다. FFmpeg의 nvenc→AV1 조건부 폴백은 manifest로 표현 불가하므로 하이브리드 경계가 필수.

대안

풀 플러그인 생태계(과잉 엔지니어링)·전부 코드(확장 비용). 좁은 manifest가 단순 도구 추가 비용만 제거.

트레이드오프

manifest가 또 다른 갓 오브젝트가 될 위험. 완화: '90% 단순 CLI만 manifest, 복잡 로직은 in-box' 경계를 P1 불변식으로 명문화.

04

변환 매트릭스 · 그래프 라우팅

단일 홉 → 멀티홉 자동 합성. transitive closure로 "이 파일로 만들 수 있는 모든 포맷"이 폭발한다.

현재 상태

8개 Provider가 PairsFromMatrix로 N×M 쌍을 선언하지만 ProviderRegistry는 (input,output) 단일 홉 딕셔너리(_byPair)만 매핑한다. 멀티홉(md→docx)은 DocumentProvider.RouteAsync(92-205)에 손코딩되어 형식 N개에 O(N²)로 수동 증식한다. 매트릭스는 '거의 모든 것→이미지/PDF/텍스트' 단방향으로만 풍부하고, 역방향(이미지/PDF→편집문서, HWP 출력, 미디어/아카이브)이 구조적으로 비어 있다. 동일포맷(pdf→pdf 압축)은 ConversionEngine.cs:88에서 무조건 Skip된다.

목표 상태

ConversionGraph가 모든 Provider Capability를 순회해 방향 그래프를 빌드하고, Dijkstra가 임의의 A→Z를 원자 엣지 조합으로 자동 합성한다. OutputsForInput은 transitive closure로 확장되어 '이 파일로 만들 수 있는 모든 포맷'을 노출한다. PDF/HWP 양방향, 영상/오디오, 아카이브/데이터/벡터, AI 후처리가 모두 엣지로 편입되고, 동일포맷 압축(pdf→pdf)도 옵션으로 허용되는 엣지가 된다.

구조적 공백

  • PDF 압축(pdf→pdf): 어떤 Provider도 수행 못 함 — PdfToolProvider 신설 필요
  • PDF→DOCX/HTML 역변환: 편집가능 역변환 경로 전무 — LibreOffice 경유 추가
  • HWP/HWPX 출력: H2Orestart import 전용이라 →HWP 불가, →DOCX/HTML/TXT도 미노출
  • 영상/오디오 전 카테고리: mp4/mp3/flac 등 미디어 Provider 0개
  • 아카이브/폰트/벡터/데이터/전자책: 빈 카테고리(ComingSoon enum 미사용)
  • AI 변환(요약/번역/캡션): 추상화·옵션·Provider 어디에도 자리 없음
  • 동일포맷 최적화(이미지 리인코딩, PDF 압축): ConversionEngine.cs:88에서 Skip되어 표현 불가
그래프 라우팅 설계 — ProviderRegistry → ConversionGraph

1) 그래프 빌드(앱 시작 1회): ProviderRegistry 생성자 루프(16-30)에서 각 Provider의 Capability.SupportedConversions를 순회해 인접 리스트 Dictionary<string,List<Edge>>를 구축한다. 노드=정규화된 확장자(.png/.pdf/.docx), 엣지=Edge{Provider, ConversionPair, Weight}. 노드 수십·엣지 수백 규모라 그래프는 매우 작다. 2) 가중치: 각 ConversionPair.LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8)를 -log(품질보존율)로 환산하고 홉페널티(작은 상수)와 실행비용(외부 프로세스>in-process)을 가중합한다. 손실은 곱셈적이므로 -log 변환으로 덧셈 최단경로가 곧 최대 품질보존 경로가 된다. 3) 탐색: .NET 9 System.Collections.Generic.PriorityQueue로 Dijkstra(O(E log V), 80~120줄)를 자체 구현한다. ConversionEngine.ConvertOneAsync(91)에서 직접 엣지가 있으면 1홉(기존 동작 호환), 없으면 FindBestPath(inExt,outExt,options)로 멀티홉 경로를 구한다. AllowMultiHop(기본 true)/MaxHops(기본 3)/AvoidLossy 옵션으로 게이트. 4) 실행: ChainExecutor가 경로의 각 홉을 순차 실행하며 중간 산출물을 공용 workDir(Temp/e2e_{Guid})에 체이닝하고, 진행률을 홉 수로 분할해 IProgress에 매핑한다. 각 홉은 기존 provider.ConvertAsync를 그대로 호출(인터페이스 변경 불필요). 5) 안전장치: 멀티홉은 직접 엣지가 없을 때만 발동, 손실 블랙리스트(텍스트→래스터 같은 도메인 경계 전이 통제), 한 홉 실패 시 어느 홉에서 실패했는지 사용자에게 전달. 6) UI: OutputsForInput을 reachability(transitive closure)로 확장하고, 손실 경로로만 도달하는 출력에 '손실 변환' 경고 배지를 붙인다.

05

AI 통합 — Codex OAuth + API

키가 없어도 모든 기존 변환은 100% 동작. AI는 ✨ 배지로만 opt-in 노출되는 부가가치 엣지.

Codex non-interactive OAuth

Codex CLI를 PATH에서 감지될 때만 활성화되는 구독자용 opt-in 보조 백엔드로 둔다. 핵심 제약: ChatGPT 구독 OAuth 토큰(auth.json의 access/refresh)은 Codex 백엔드 전용이라 api.openai.com에 직접 Bearer로 붙일 수 없다 — 구독 재사용은 오직 codex CLI 프로세스 호출로만 가능. 실행은 ExternalProcessRunner로 `codex exec --skip-git-repo-check --json --output-schema schema.json -o out.json --cd <tempdir> "<프롬프트 + 파일경로>"` 형태. --skip-git-repo-check는 변환 앱에 필수(git 저장소 아닌 폴더 허용), --output-schema로 응답을 JSON Schema로 강제해 메타데이터 추출, --json으로 JSONL 이벤트 스트림 파싱. CheckAvailabilityAsync에서 `codex --version` 프로브 + auth.json 존재 확인. auth.json refresh 토큰 race를 막기 위해 SemaphoreSlim(1) 직렬화 또는 --ephemeral 사용.

API 키 모드 (기본 경로)

기본 경로는 API 키 + 공식 SDK다. Microsoft.Extensions.AI(IChatClient, MIT) 단일 추상화로 OpenAI(공식 OpenAI 패키지, MIT)와 Anthropic(공식 Anthropic 패키지, MIT)을 동일 인터페이스로 다룬다. 사용자는 설정에서 'OpenAI / Claude / Codex CLI / auto'를 고르고 API 키만 입력한다. 키는 ISettingsStore에서 System.Security.Cryptography.ProtectedData(DPAPI, CurrentUser)로 암호화해 %LOCALAPPDATA%에 저장하고, OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수도 폴백으로 읽어 CI/파워유저 친화. CheckAvailabilityAsync가 키 부재 시 NotReady(키 발급 URL을 ExternalDependency로 안내)를 반환해 그래프에서 자동 비활성.

활용 사례 (AI 전용 신규 엣지)

  • 요약: pdf/docx/txt/md → txt/md (긴 문서를 LLM이 요약)
  • 번역: txt/docx/md → txt/docx (대상 언어는 옵션, 비파일 입력 LLM 왕복)
  • OCR 교정: OcrProvider 출력(.txt)을 받아 LLM이 오탈자/줄바꿈 정리 (그래프가 OCR→LLM 2단계 멀티홉으로 자동 합성)
  • 이미지 캡션/대체텍스트: png/jpg → txt (비전 모델)
  • 문서 언어 번역 + 포맷 정규화: csv→md(표), txt→md
  • 메타데이터 생성: 임의 입력 → json (제목/태그/요약, Structured Outputs로 구조화)

아키텍처 — AI는 끄면 사라지는 부가 엣지

LlmProvider를 별도 IAiProvider가 아닌 일반 IConverterProvider로 구현해 그래프의 부가 엣지로 편입한다(ADR-5). AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출되며, 등록 순서로 '로컬 변환이 이미 있는 페어는 로컬 Provider가 우선, AI는 신규 페어만'을 보장한다(Priority 충돌 모델). 불변식: 키가 없어도 모든 기존 변환은 100% 동작하고 AI 페어만 비활성, AI는 절대 기본 경로를 점유하지 않으며 UI에 ✨AI 배지(종량과금·네트워크 명시)로만 opt-in 노출된다. 텍스트 추출이 필요하면 DocumentProvider/PdfProvider/OcrProvider를 주입받아 '추출→LLM' 2단계로 구성(OcrProvider가 PdfProvider를 주입받는 선례). 프라이버시: 로컬 문서가 외부 서버로 전송되므로 명시적 동의 토글 필수(기본 OFF), 미래에 Ollama 로컬 모델 경로를 IChatClient로 열어둔다.

06

미디어 레이어 — 영상·오디오·PDF 압축

FFmpeg(LGPL 분리 호출)·Ghostscript(AGPL 감지만)로 카테고리를 미디어 변환기로 점프.

영상 (Video)

FfmpegProvider(FFMpegCore 5.4.0, MIT)로 mp4/mkv/webm/mov/avi/gif N×M 트랜스코딩. H.264/H.265는 HW 인코더(h264_nvenc/qsv/amf) 우선, LGPL 빌드엔 libx264/x265(GPL)가 없으므로 HW 미지원 시 AV1(libaom)/VP9(libvpx, 둘 다 BSD-like royalty-free)로 폴백. FFprobe로 duration 확보 후 NotifyOnProgress(Action<double>,TimeSpan)을 IProgress에 직결, CancellableThrough(ct)로 취소.

오디오 (Audio)

오디오는 mp3/aac/m4a/opus/ogg/flac/wav N×M. AAC는 FFmpeg 네이티브 aac 인코더(LGPL, libfdk-aac=nonfree 회피), Opus/FLAC/MP3는 LGPL 빌드로 직접 처리. 오디오 전용 출력(flac/mp3)은 영상 입력에서 오디오 트랙만 추출.

PDF 압축

PdfToolProvider 3단계: Light=PDFsharp(MIT, in-process) 또는 qpdf(Apache 2.0) 구조 최적화(object stream 압축·linearize), Strong=PDFium 렌더+ImageMagick 재인코딩(텍스트 선택성 잃지만 라이선스 안전), Max=Ghostscript(-dPDFSETTINGS /screen)는 AGPL이라 번들 금지·사용자 설치본 감지만. 병합/분할/암호화는 PDFsharp 또는 qpdf.

이미지 최적화

기존 MagickProvider의 ApplyEncoding(jpg/png/webp/avif/tiff 품질·알파평탄화·MaxLongEdge)을 공용 ImageEncoder 헬퍼로 추출해 PdfProvider/HtmlProvider/CombineAsync의 4중 복제를 제거. 동일포맷 이미지 리인코딩(품질 조절)도 엣지로 허용.

외부 바이너리 · 라이선스 게이트 전략

단일 포터블 EXE 부담을 줄이기 위해 무거운 바이너리(FFmpeg ~100MB)는 절대 번들하지 않고 'RequiresExternal + 최초 사용 시 자동 다운로드' 모델. 라이선스 게이트(코드 리뷰 강제): FFmpeg는 BtbN lgpl-shared 빌드(--enable-gpl/nonfree 없음)를 별도 프로세스로 호출(동적 분리)해 LGPL 준수 — gyan.dev/BtbN gpl 빌드(GPLv3) 번들 절대 금지. ExternalToolDetector.TryFindFfmpeg가 (a)%LOCALAPPDATA%\Everything2Everything\ffmpeg, (b)시스템 PATH 순 탐지, 없으면 lgpl-shared zip을 SHA256 검증 후 다운로드. GlobalFFOptions.Configure로 경로 고정. NVENC는 LGPL 빌드에서 --enable-nonfree 없이 합법 사용 가능(NVIDIA 공식 확인). About 화면에 'uses FFmpeg under LGPLv2.1' 고지 + 소스 다운로드 링크(LGPL 의무). MSIX 변형에서는 샌드박스 정책상 lgpl-shared DLL을 패키지 동봉(여전히 LGPL 준수). Ghostscript/MuPDF(AGPL)는 사용자 설치본 감지만, codec 특허(H.264/AAC) 위험을 줄이려 AV1/VP9/Opus/FLAC(royalty-free)를 기본 권장 출력으로.

07

실행 로드맵 · P1 → P8

각 단계가 독립적으로 가치를 전달하고 이전 단계에 의존한다. P1은 그래프 코어 + 즉시 체감(PDF 압축)을 함께 심는다.

P1
그래프 엔진 도입 + 즉시 체감 가치(PDF 압축)
ProviderRegistry를 ConversionGraph로 승격하고 멀티홉 경로 탐색을 엔진에 내장한다. 동시에 PDF 압축이라는 즉시 체감 신기능을 출시해 '보이지 않는 리팩터링의 함정'을 회피한다.
예정LRISK medium

산출물

  • ConversionGraph + 자체 Dijkstra PathFinder(외부 의존성 0, .NET 9 PriorityQueue)
  • ConversionPair.LossClass 필드 + 정적 가중치 테이블
  • ConversionEngine.ConvertOneAsync 그래프 위임 + ChainExecutor(공용 workDir 헬퍼)
  • PdfToolProvider 신설: PDF 압축(Light=PDFsharp 구조최적화, Strong=PDFium 렌더+Magick 재인코딩, Max=Ghostscript 외부폴백) + 병합/분할
  • xUnit 테스트 프로젝트 신설(현재 0개) + 그래프 경로탐색 회귀 테스트

핵심 코드 변경

ProviderRegistry.cs
_byPair 위에 인접 리스트 그래프 빌드, 증분 등록 Register/Rebuild 추가
ConversionEngine.cs:91
TryGet 직접 매핑에서 그래프 FindBestPath→ExecuteChainAsync 위임으로 전환
ConversionPair
LossClass 필드 추가, 엣지 가중치 SSOT
신규 PdfToolProvider
동일포맷 pdf→pdf Skip(ConversionEngine.cs:88) 우회, 3단계 압축
Exit Criteria
기존 모든 변환이 그래프 경로로 동일 동작(회귀 테스트 통과)하고, PDF 파일을 3단계 레벨로 압축해 출력 용량 감소를 GUI에서 확인 가능.
P2
손그림 멀티홉 제거 + HWP 한글 양방향
DocumentProvider.RouteAsync의 손코딩 switch를 삭제하고 원자 엣지만 선언하게 해 그래프를 도그푸딩한다. HWP→DOCX/HTML/TXT 출력 매트릭스를 확장해 한글 사용자 핵심 요구를 충족.
예정MRISK mediumdepends · P1

산출물

  • DocumentProvider.RouteAsync(92-205) 삭제 → md→html, html→docx 등 원자 엣지만 선언, md→docx는 엔진 자동 합성
  • HWP/HWPX 출력 확장: HwpxProvider Outputs에 .docx/.html/.txt/.odt 추가(soffice --convert-to 파라미터화)
  • .hwp 입력 시 --infilter='Hwp2002_File' 조건부 지정 + 함초롬/맑은고딕 폰트 누락 감지 경고
  • DocumentProvider 입력에 .pdf 추가 → pdf→docx/html/txt 역변환(soffice) + pdf→txt 무외부 폴백(PdfPig)
  • RouteAsync 삭제가 손그림과 동일 동작함을 회귀 테스트로 증명

핵심 코드 변경

DocumentProvider.cs:92-205
멀티홉 switch 삭제, 단일 홉 원자 변환만 선언
HwpxProvider
Outputs 배열에 .docx/.html/.txt/.odt 추가, soffice 타깃 파라미터화
DocumentProvider Inputs
.pdf 추가로 PDF 역변환 엣지 개통
Exit Criteria
HWP/HWPX 파일을 DOCX/HTML/TXT/PDF로 변환 가능하고, md→docx 같은 멀티홉이 RouteAsync 없이 그래프 합성으로 동일하게 동작.
P3
외부 프로세스 통합 + 인터페이스 일반화
3중 복제된 LibreOffice 호출을 단일 ExternalProcessRunner로 통합하고(타임아웃·stderr·Kill), IConverterProvider/ConvertResult를 일반화해 미디어·AI 엣지가 들어올 그릇을 판다.
예정LRISK mediumdepends · P2

산출물

  • ExternalProcessRunner(CliWrap): 타임아웃+stderr수집+Kill 통합, LibreOffice 3중 복제 흡수
  • Abstractions 어셈블리 분리(IConverterProvider/ConvertResult 이전, 타입 동일성)
  • IConverterProvider→ConvertRequest/ConvertContext 일반화, 기존 8개 Provider 어댑터로 무중단 마이그레이션
  • ConvertResult에 ExtractedText/AiResponse/Metadata/IntermediateArtifacts 필드
  • ISettingsStore(DPAPI 암호화) 신설 — API 키·도구 경로 영속화 토대
  • Priority 기반 충돌 모델로 _byPair.TryAdd first-wins 교체

핵심 코드 변경

3개 Provider
ConvertWithLibreOfficeAsync 복붙을 ExternalProcessRunner로 통합
IConverterProvider.cs:9-15
ConvertRequest/ConvertContext로 일반화
ConvertResult.cs
비파일 산출물 필드 추가
신규 ISettingsStore
DPAPI ProtectedData 암호화 JSON 영속화
Exit Criteria
LibreOffice가 멈춰도 타임아웃으로 복구되고 stderr가 에러 메시지에 포함되며, 기존 변환이 일반화된 시그니처로 무중단 동작(회귀 테스트 통과).
P4
미디어 레이어 — 영상/오디오 코덱·압축
FFmpeg로 카테고리를 '미디어 변환기'로 점프시킨다. 영상/오디오 N×M 코덱·압축을 라이선스 안전하게 통합하고 배치 병렬화로 트랜스코딩 병목을 해소.
예정XLRISK highdepends · P3

산출물

  • FfmpegProvider: FFMpegCore(MIT) + 영상(mp4/mkv/webm/mov/avi/gif)·오디오(mp3/aac/m4a/opus/flac/wav) N×M
  • 바이너리 조달: ExternalToolDetector.TryFindFfmpeg + BtbN lgpl-shared 자동 다운로드(SHA256 검증), GlobalFFOptions 경로 고정
  • HW 인코더(nvenc/qsv/amf) 우선 + SW 폴백, NotifyOnProgress→IProgress 직결, CancellableThrough(ct)
  • 배치 병렬화: ConvertManyAsync 순차 for-loop(57-70)를 Parallel.ForEachAsync로 교체(MaxDegreeOfParallelism)
  • PreviewService→IPreviewRenderer 추상화 + FFmpeg 프레임 추출 + 프리뷰 캐시
  • ImageMagick ResourceLimits 전역 설정(decompression bomb 방어) + NU190x 취약점 경고 재활성화

핵심 코드 변경

신규 FfmpegProvider
FFMpegCore 래퍼, HW 가속 폴백, RequiresExternal
ConversionEngine.cs:57-70
순차 for-loop를 Parallel.ForEachAsync로 교체
PreviewService.cs:23
닫힌 switch를 IPreviewRenderer 레지스트리로, 영상 프레임 추출 추가

신규 Provider

+ FfmpegProvider
Exit Criteria
mp4→webm, wav→mp3 등 영상/오디오 변환이 HW 가속으로 동작하고 진행률·취소가 정확하며, 100개 배치가 멀티코어를 활용.
P5
AI 부가가치 레이어 — Codex OAuth + API
변환에 'AI가 더 좋게 만든다'는 해자를 얹는다. 기본은 API 키 + 공식 SDK, Codex CLI는 구독자용 opt-in. 키 없으면 AI 페어만 비활성, 기존 변환 무영향.
예정LRISK highdepends · P3

산출물

  • LlmProvider: Microsoft.Extensions.AI(IChatClient)로 OpenAI/Anthropic 공식 SDK 연결 + Codex CLI opt-in(codex exec --json --output-schema)
  • AI 매트릭스: 요약(pdf/docx/txt→txt/md), 번역(→대상언어), OCR교정(OcrProvider 출력 2단계 파이프), 이미지 캡션(png/jpg→txt 비전), 메타데이터(→json Structured Output)
  • 키 관리: ISettingsStore DPAPI 암호화 + OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수 폴백, CheckAvailabilityAsync 게이트
  • UI: AI 출력 페어에 ✨AI 배지(종량과금·네트워크 명시) + 설정에서 백엔드/모델/키 입력
  • Codex 경로 SemaphoreSlim(1) 직렬화(auth.json refresh 토큰 race 방지) 또는 --ephemeral

핵심 코드 변경

신규 LlmProvider
IConverterProvider로 구현, AI는 로컬 변환 없는 신규 엣지로만
CheckAvailabilityAsync
키/codex --version 게이트로 키 부재 시 NotReady→그래프 자동 비활성
UI
AI 페어 ✨ 배지, 등록 순서로 기본 경로 불점유 보장

신규 Provider

+ LlmProvider
Exit Criteria
API 키 입력 시 PDF 요약·이미지 캡션·번역이 동작하고, 키가 없으면 AI 페어만 사라지고 모든 기존 변환은 100% 동작.
P6
매트릭스 자동 극대화 + 헤드리스 CLI
앞 단계에서 쌓인 모든 엣지를 그래프가 자동 합성해 진짜 N×M·다방향을 완성하고(video→mp3→txt AI전사 등), 헤드리스 CLI로 자동화·스크립팅을 개방한다.
예정LRISK mediumdepends · P5

산출물

  • OutputsForInput을 transitive closure로 확장 — '이 파일로 만들 수 있는 모든 포맷' UI 노출 + 손실 경로 경고 배지
  • 멀티홉 도그푸딩 검증: hwp→pdf→png, video→mp3→txt(AI) 같은 신규 합성 경로 동작 확인
  • 헤드리스 CLI 분리: --json/--output-dir/--quality/--prompt/--codec/--recursive 플래그 + stdout JSON 결과 + exit code
  • 워치폴더 모드(FileSystemWatcher + 디바운스 + 파일잠금 재시도, 출력 디렉터리 분리로 무한루프 방지)
  • QuickProgressWindow 취소 토큰 전파 + 케이퍼빌리티 사전 점검

핵심 코드 변경

ProviderRegistry.cs:52
OutputsForInput을 그래프 reachability로 확장
CliRouter.cs:21
옵션 플래그 파싱 + stdout JSON + exit code
App.xaml.cs:96
Quick 경로에 취소 토큰 전파
Exit Criteria
HWP 파일에서 PNG까지(멀티홉) 변환 가능하고, CLI가 WPF 창 없이 JSON 결과를 stdout으로 반환해 스크립트가 파싱 가능.
P7
순수 .NET 카테고리 보강 + manifest 어댑터
EXE 번들 가능한 순수 관리 라이브러리로 빈 카테고리를 채우고, 단순 CLI 도구를 코드 없이 추가하는 좁은 manifest 어댑터를 도입한다.
예정LRISK lowdepends · P6

산출물

  • ArchiveProvider(SharpCompress, 순수관리) — zip/7z/tar/gz/bz2
  • DataProvider(Parquet.Net/ClosedXML/CsvHelper) — csv↔json↔xlsx↔parquet
  • VectorProvider(Svg.Skia) — svg→png/jpg/webp/pdf, EPS는 Magick+Ghostscript
  • PandocProvider(외부 CLI) — md/rst/latex/ipynb/epub 마크업 매트릭스, LibreOffice 겹침은 Priority 라우팅
  • EbookProvider(Calibre ebook-convert, 외부) — epub↔mobi↔azw3↔pdf
  • manifest 어댑터(ExternalProcessRunner 위 인자 템플릿): qpdf/gs 같은 단순 CLI 코드 없이 추가

핵심 코드 변경

신규 4-5개 Provider
순수 .NET은 EXE 직접 포함, 외부 CLI는 분리 호출
ManifestLoader
tools/*.manifest.json으로 단순 CLI 엣지 추가
Bootstrap
하드코딩 배열에 신규 Provider 등록 + manifest 동적 등록

신규 Provider

+ ArchiveProvider+ DataProvider+ VectorProvider+ PandocProvider+ EbookProvider
Exit Criteria
zip 압축/해제, csv→xlsx, svg→png가 외부 도구 없이 동작하고, manifest 파일 하나로 새 CLI 변환 도구를 코어 재컴파일 없이 추가 가능.
P8
확장성·신뢰성·배포 굳히기
기능이 다 들어온 뒤 회귀 방지·UI 분해·배포를 다진다. 차별화는 끝났으니 여기서부터는 깨지지 않게 유지.
예정LRISK lowdepends · P7

산출물

  • UI MVVM 분해(MainWindow.xaml.cs 1171줄) + Provider 선언형 옵션 스키마 기반 동적 옵션 UI 생성
  • 히스토리 도메인 로직을 Core로 분리 + 데모 시드 제거 + 스트리밍 로드/회전 정책
  • CI 강화: NuGet 캐시 + self-contained portable EXE 산출 + 외부 바이너리 번들링 파이프라인(FFmpeg LGPL 고지) + dotnet test 게이트
  • 출력 형식 매트릭스 3중 중복(AllFormats/PopularOutputs/파일다이얼로그)을 단일 FormatCatalog로 통합
  • 테스트 확대: OutputPathHelper 충돌·결합 로직·JSONL round-trip 순수 함수 커버

핵심 코드 변경

MainWindow.xaml.cs
MVVM 분해, 동적 옵션 UI
BuildMsix.ps1
외부 바이너리 번들 + 라이선스 고지 단계
build.yml/release.yml
캐시+테스트 게이트+self-contained 산출물
Exit Criteria
PR마다 테스트가 게이트로 동작하고, self-contained portable EXE가 자동 산출되며, 새 형식 추가가 단일 FormatCatalog 한 곳 수정으로 끝남.
08

성공 지표

현재 → 목표. 각 지표가 로드맵 완료를 객관적으로 측정한다.

멀티홉 경로 자동 합성
DocumentProvider.RouteAsync에 손코딩된 3-4개 체인만 동작
엔진이 임의 A→Z를 그래프 탐색으로 자동 합성, RouteAsync 0줄
입력당 도달 가능 출력 포맷 수
1-hop 직접 출력만(OutputsForInput 직접 매핑)
transitive closure로 확장된 도달 가능 전체 포맷 + 손실 배지
지원 카테고리 수
이미지/PDF/문서/HEIC/OCR (약 5)
+영상/오디오/아카이브/데이터/벡터/전자책/AI (약 12)
PDF 압축 기능
어떤 Provider도 수행 불가
3단계 레벨(Light/Strong/Max) 압축 + 병합/분할
HWP 출력 매트릭스
→PDF/이미지만, →DOCX/HTML/TXT 미노출
HWP→DOCX/HTML/TXT/PDF 완성
코어 테스트 커버리지
테스트 프로젝트 0개
그래프 탐색·OutputPathHelper·결합·JSONL round-trip 커버 + CI 게이트
배치 처리 동시성
순차 for-loop(코어 1개만 사용)
Parallel.ForEachAsync(MaxDegreeOfParallelism)로 멀티코어 활용
CLI 자동화 가능성
WPF 창만 띄우고 stdout 무반환
--json/--codec/--prompt 플래그 + stdout JSON + exit code
09

리스크 레지스터

가장 큰 위협은 "보이지 않는 리팩터링의 함정"과 GPL/AGPL 라이선스 오염.

리스크발생가능영향완화책
'보이지 않는 리팩터링의 함정' — 그래프 코어 재설계가 사용자 체감 변화 0인 상태로 길어짐 RISK mediumRISK high P1에서 그래프 도입과 PDF 압축(즉시 체감 신기능)을 묶고, transitive closure로 늘어나는 '만들 수 있는 포맷 목록'을 가시 성과로 노출. DocumentProvider.RouteAsync 삭제를 회귀 테스트로 동일 동작 증명.
'최단 경로' 압박으로 LlmProvider/FfmpegProvider를 또 하드코딩 switch로 끼워넣어 RouteAsync 지옥 재생산 RISK mediumRISK high P1에 그래프 코어를 먼저 심어 하드코딩을 구조적으로 차단. '신규 기능은 그래프 엣지로만 추가'를 불변식으로 명문화하고 코드 리뷰 게이트로 강제.
GPL/AGPL 바이너리(FFmpeg gpl빌드·Ghostscript·H2Orestart) 정적 링크로 상업 배포 라이선스 오염 RISK mediumRISK high 모든 무거운 외부 도구를 별도 프로세스 분리 호출 + 사용자 설치 감지/LGPL 빌드 자동조달로만 통합. 라이선스 경계를 코드 리뷰 게이트로 강제(ADR-6).
멀티홉 손실 누적·은폐 — HWP→PDF(래스터화)→DOCX가 '편집가능'을 약속하나 이미지 덩어리 반환 RISK mediumRISK medium LossClass 가중치로 래스터화에 큰 페널티, 멀티홉은 직접 엣지 없을 때만, MaxHops=3, 손실 블랙리스트, 손실 경로 UI 경고 배지 3겹 가드레일.
인터페이스 일반화(ConvertRequest)가 8개 기존 Provider를 한 번에 깸 RISK mediumRISK high 기존 시그니처를 어댑터로 감싸 점진 마이그레이션, 무중단을 회귀 테스트로 보장. P3에 배치해 미디어/AI 동기가 코드에 들어온 뒤 일반화.
AI 비결정성·종량과금·네트워크 의존이 '로컬 예측가능 변환' 신뢰를 깸 RISK highRISK medium AI는 기본 경로 불점유, ✨AI 배지 opt-in, 키 없으면 조용히 비활성을 설계 불변식으로 박음. 토큰/비용 표시, 사용자 확인 게이트, 재시도·백오프.
테스트 0개 상태에서 대규모 코어 변경이 회귀를 탐지 못 함 RISK highRISK high P1에서 xUnit 테스트 프로젝트를 최우선 신설하고 그래프 경로탐색·DocumentProvider 회귀를 첫 안전망으로. CI에 dotnet test 게이트 추가.
manifest가 또 다른 갓 오브젝트화 — FFmpeg HW가속 폴백 같은 복잡 로직을 manifest로 표현 시도 RISK lowRISK medium manifest는 '90% 단순 CLI(qpdf/gs)만, 복잡 로직은 in-box 코드 Provider' 경계를 P1부터 불변식으로 명문화.
10

다음 세션 인계 노트

이 문서가 SSOT다. 다음 세션은 아래 순서대로 시작한다.

▶ Handoff — 어디서부터 시작하고 무엇을 먼저 검증할지

다음 세션은 P1(그래프 엔진 도입 + PDF 압축)부터 시작한다. 시작 순서와 검증 포인트:

1) 가장 먼저 xUnit 테스트 프로젝트를 신설하라(현재 0개). 이게 모든 코어 변경의 안전망이며, 특히 DocumentProvider.RouteAsync 삭제가 '손그림과 동일 동작'임을 증명할 회귀 테스트의 전제다. 먼저 현재 RouteAsync의 모든 경로(md→docx, docx→md, hwp→html 등)에 대한 골든 테스트를 작성해 baseline을 고정하라.

2) ConversionGraph + 자체 Dijkstra를 ProviderRegistry 옆에 얇게 얹어라. ProviderRegistry.cs:16-30 생성자 루프에 그래프 빌드 한 단계만 추가. _byPair는 유지(직접 엣지 1홉 호환). ConversionPair에 LossClass 필드 추가가 선결.

3) 첫 검증: ConversionEngine.ConvertOneAsync(91)를 그래프 위임으로 바꾼 뒤, 기존 모든 변환이 동일 동작하는지 회귀 테스트로 확인. 그 다음에야 RouteAsync를 삭제하고 원자 엣지만 선언하게 바꿔 md→docx가 그래프 합성으로 동일하게 나오는지 검증.

4) PDF 압축(PdfToolProvider)은 ConversionEngine.cs:88의 동일포맷 Skip을 우회해야 한다 — pdf→pdf를 엣지로 허용하는 메커니즘이 그래프 도입과 함께 필요. PDFsharp(MIT) in-process 압축부터 시작하면 외부 의존성 0으로 즉시 체감 가치.

먼저 검증할 불변식: (a) 기존 8개 Provider 변환이 그래프 경로로 100% 동일 동작, (b) 멀티홉은 직접 엣지 없을 때만 발동, (c) 손실 경로에 가중치가 정확히 반영되는지. 라이선스 게이트(GPL/AGPL 분리 호출)는 P4(미디어)부터 본격 적용되지만, P1의 Ghostscript 폴백에서도 '사용자 설치본 감지만, 번들 금지' 원칙을 처음부터 지켜라.

참고: 빌드 후에는 메모리의 project_build_pipeline(publish + 카스케이드 재등록 PowerShell 시퀀스)를 따르고, 사용자가 직접 push & GUI 검증하는 워크플로이므로 큰 결정은 빠른 승인 후 단일 commit으로 진행.

11

설계안 비교 · 심사

3개 독립 아키텍트가 서로 다른 각도에서 제안했고, 심사가 점수화·종합했다.

변환 그래프 코어 우선 — "모든 변환은 엣지(edge)이고, 엔진은 라우터(router)다." Provider를 손으로 체이닝하는 대신, 모든 변환을 그래프의 단방향 엣지로 등록하고 멀티홉 경로 탐색(Dijkstra)이 N×M·다방향을 자동으로 합성하게 만든다. UI/AI/미디어/PDF/HWP는 전부 이 그래프 위에 '엣지를 더하는 것'으로 환원된다.

84

Everything2Everything의 북극성은 "원자적 변환(atomic conversion)의 조합으로 임의의 A→Z를 자동 합성하는 변환 그래프 OS"다. 현재 코드의 가장 강력한 증거는 DocumentProvider.RouteAsync(DocumentProvider.cs:92-205)다. 이건 사람이 손으로 그린 Dijkstra다 — `md→docx`를 `md→html→docx`로, `docx→md`를 `docx→html→md`로 중첩 switch에 박아넣었다. 형식이 N개로 늘면 이 손그림 그래프는 O(N²)로 폭발하고, 새 형식 하나가 모든 분기를 건드린다. 비전의 핵심…

핵심 변경

  • ProviderRegistry를 그래프로 승격: _byPair 단일 룩업(ProviderRegistry.cs:6,43) 옆에 ConversionGraph(인접 리스트 Dictionary<string,List<Edge>>)를 빌드하고, 자체 Dijkstra FindBestPath(in,out,options)를 추가. 외부 라이브러리 없이 80~120줄, 단일 EXE/AOT 친화.
  • ConversionPair에 LossClass(Lossless/Container/Recode/Rasterize) 필드 추가 → 엣지 가중치의 단일 출처(SSOT). 충돌 시 _byPair.TryAdd의 '조용한 첫 등록자 우선'(ProviderRegistry.cs:22)을 비용/우선순위 기반 명시 선택 + 진단 경고로 교체.
  • ConversionEngine.ConvertOneAsync(ConversionEngine.cs:91)를 그래프 위임으로 전환: 직접 엣지가 없거나 더 싼 멀티홉이 있으면 ExecuteChainAsync가 경로의 각 홉을 순차 실행(중간 산출물은 DocumentProvider의 workDir 패턴을 공용 헬퍼로 승격해 재사용). 진행률은 홉 수로 분할 매핑.
  • DocumentProvider.RouteAsync의 손그림 멀티홉(:92-205)을 삭제 → DocumentProvider는 md→html, html→docx 같은 원자 엣지만 선언. md→docx는 엔진이 자동 합성. 이것이 그래프 코어의 첫 검증(dogfooding).
  • IConverterProvider 시그니처(IConverterProvider.cs:9-15)를 ConvertRequest/ConvertContext 객체로 일반화 — 다중 입력(N→1 결합), 비파일 결과(추출 텍스트·AI 응답·미디어 메타데이터), 멀티홉 중간 컨텍스트, 풍부한 진행률을 표현. ConvertResult(ConvertResult.cs:10)에 비파일 산출물 필드 추가.
  • CombineAsync의 ImageMagick 직접 의존(ConversionEngine.cs:2,164-257)을 IMultiInputProvider(N→1 엣지)로 분리 → 엔진의 라이브러리 결합 제거. 결합 가능 형식의 정적 HashSet(ConversionEngine.cs:14-23)을 Provider 능력 선언으로 통합.

최대 리스크 · 그래프 코어 재설계가 '엔진은 깔끔해졌는데 사용자에게 보이는 변화가 0'인 상태로 Phase 0~1을 길게 끄는 '보이지 않는 리팩터링의 함정'이 가장 큰 위험이다. 그래프 엔진은 그 자체로는 데모할 게 없다 — DocumentProvider.RouteAsync를 삭제

플러그인 생태계 우선 (Plugin-Ecosystem-First): 코어를 변환 호스트(host)로 축소하고, 모든 변환 능력을 선언적 manifest + 어댑터 Provider로 외부화한다. "코드를 늘려 포맷을 늘리는" 모델에서 "manifest를 늘려 포맷을 늘리는" 모델로 전환.

71

Everything2Everything을 단일 모놀리식 변환기가 아니라 "변환 능력의 OS"로 재정의한다. 코어는 더 이상 변환을 '아는' 주체가 아니라, 변환 능력을 선언받아 조합·라우팅·실행하는 얇은 호스트가 된다. 세상의 모든 변환 도구(FFmpeg/Ghostscript/Pandoc/LibreOffice/Calibre/qpdf/LLM)는 코드가 아닌 선언적 manifest(JSON)로 등록되며, 코어는 이 능력들을 방향 그래프로 합성해 manifest 작성자가 한 번도 명시하지 않은 멀티홉 경로(HWP→PDF→DOCX, PNG→PDF 압축→DOCX)까지 자동으로…

핵심 변경

  • 코어 분리 + 어댑터 베이스 추출: IConverterProvider를 Everything2Everything.Abstractions 별도 어셈블리로 분리(타입 동일성 보장)하고, 7개 in-box Provider의 외부 프로세스 호출 로직(현재 DocumentProvider.SofficeConvertAsync:238-281이 3개 Provider에 복붙됨)을 단일 ExternalToolProvider 추상 베이스 + ProcessRunner(CliWrap 기반, 타임아웃·stderr 수집·Kill 통합)로 통합. 이 베이스가 manifest 어댑터의 실행 엔진이 된다.
  • 선언적 Manifest + 동적 등록 파이프라인: tools/*.manifest.json 스키마를 기존 ProviderCapability/ConversionPair/ExternalDependency 모양 그대로 직렬화한 형태로 정의. manifest는 (tool id, 실행파일 탐지 규칙, 입력×출력 매트릭스, argument 템플릿 {input}/{output}/{outdir}/{format}, 성공 판정, LossClass)를 선언. ManifestLoader가 런타임에 읽어 ExternalToolProvider 인스턴스로 합성. ProviderRegistry를 '닫힌 생성자'에서 Register/RegisterRange/Rebuild가 가능한 '증분 등록' 구조로 개조(현재 생성자 17-30행 인덱싱 로직을 private Index(provider)로 추출).
  • ProviderRegistry → ConversionGraph 승격 + 멀티홉 경로 탐색: 모든 Provider의 Capability.SupportedConversions를 순회해 방향 그래프(노드=확장자, 엣지=Provider+LossClass 가중치) 구축. 외부 의존성 없는 자체 Dijkstra(~100줄)로 최저손실 경로 탐색. ConversionEngine.ConvertOneAsync(91행)에서 직접 엣지가 없으면 그래프 탐색으로 폴백, 경로의 각 홉을 ExecuteChainAsync로 순차 실행(중간 산출물은 DocumentProvider의 workDir 패턴 재사용). DocumentProvider의 손으로 짠 RouteAsync switch는 '단일 홉 원자 변환'만 선언하도록 분해 → md→docx 같은 경로는 엔진이 자동 합성.
  • 레지스트리 충돌 모델 교체: _byPair.TryAdd(22행)의 '조용한 first-wins'를 ProviderCapability.Priority 필드 + 명시적 다중 Provider 공존 모델로 교체. 같은 (input,output)에 빠른변환/고품질/AI 등 복수 전략을 등록하고 우선순위·진단으로 선택. 이로써 manifest 어댑터가 in-box Provider를 덮어쓰는 사고를 방지하고 변환 전략 다중화 가능.
  • ConvertRequest/ConvertResult 일반화 + 옵션 백 분해: IConverterProvider 시그니처를 단일 sourcePath/outputExtension(IConverterProvider.cs:9-15)에서 ConvertRequest(다중 입력·미디어 메타·옵션 백)/ConvertContext로 일반화. ConvertResult(현재 OutputPaths만, ConvertResult.cs:10)에 추출 텍스트·AI 응답·메타데이터 필드 추가. ConvertOptions 갓 오브젝트(11개 sub-record)를 manifest별 IReadOnlyDictionary 옵션 백으로 분해해 Video/Audio/Ai 옵션을 sub-record 증식 없이 수용.
  • AI는 또 하나의 manifest 어댑터: LlmProvider를 Microsoft.Extensions.AI(IChatClient) 추상화 위에 구축. 기본 경로는 API 키(OpenAI/Anthropic 공식 SDK), opt-in 보조 경로는 Codex CLI(codex exec --json --output-schema, PATH 감지 시만 활성). 키는 DPAPI(ProtectedData)로 암호화 저장하는 ISettingsStore 신설. CheckAvailabilityAsync가 키/CLI 부재 시 NotReady→그래프에서 자동 비활성 노드 처리. AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출, UI에 'AI' 배지.

최대 리스크 · manifest 추상화의 '표현력 천장'과 멀티홉 신뢰성이 동시에 무너지는 것. (1) 선언적 argument 템플릿({input}/{output}/{format})은 단순 CLI 도구에는 완벽하지만, FFmpeg의 코덱별 HW가속 폴백(nvenc 실패→AV1)이나 조건부 인자처럼 &#

AI·미디어 기능 우선 — 사용자가 명시한 신규 가치(Codex/API AI 통합, 영상/오디오/PDF 압축, HWP 한글 변환)를 최단 경로로 출시하고, 그래프/레지스트리 리팩터링은 '그 기능을 켜기 위한 최소 인프라'로만 취급한다. 인프라 완성도가 아니라 사용자 체감 차별화가 북극성이다.

SELECTED BASE
89

Everything2Everything을 '무엇이든 → 무엇이든, 그리고 변환하면서 더 좋아지는' 도구로 만든다. 핵심 차별화는 두 가지다. (1) 변환이 단순 포맷 치환이 아니라 'AI 부가가치 레이어'를 거친다 — 영상을 mp4로 바꾸면서 자동으로 자막을 뽑고, PDF를 압축하면서 OCR 오탈자를 LLM이 교정하고, HWP를 DOCX로 풀면서 요약·번역을 곁들인다. AI는 변환의 핵심 엔진이 아니라 '후처리 부가가치 단계'로 배치해, API 키가 없어도 모든 기존 변환은 100% 동작하고 AI 페어에만 '✨ AI' 배지가 붙는다…

핵심 변경

  • ConvertContext/ConvertRequest 도입으로 IConverterProvider 시그니처 일반화 — 단일 sourcePath/단일 outputExtension/IProgress<double> 고정(IConverterProvider.cs:9-15)을 다중 입력·비파일 결과(추출 텍스트·AI 응답·미디어 메타데이터)·다단계 진행률을 담는 컨텍스트 객체로 교체. AI·미디어·압축 Provider가 요구하는 모든 표현을 인터페이스 레벨에서 한 번에 연다.
  • ConvertOptions 갓-오브젝트(ConvertOptions.cs)를 형식별 옵션 백으로 분해하고 Ai/Media/PdfCompress 옵션 그룹 신설 — 영상 코덱·CRF·fps, 오디오 비트레이트, AI 모델·프롬프트·온도·백엔드(openai|anthropic|codex-cli|auto), PDF 압축 레벨(Light/Strong/Max)을 담을 자리를 만들고 DPAPI 암호화 설정 영속화 계층(ISettingsStore)을 신설해 API 키·도구 경로를 안전 저장.
  • ProviderRegistry를 얇은 ConversionGraph로 승격 — _byPair 단일홉(ProviderRegistry.cs:6,43) 위에 인접 리스트 그래프를 얹고 외부 의존성 없는 자체 Dijkstra(80~120줄, 비용=손실가중+홉페널티)로 멀티홉 경로를 자동 합성. DocumentProvider.RouteAsync(DocumentProvider.cs:92-205)의 손으로 짠 md→html→docx switch를 '원자 변환 선언 + 엔진 자동 합성'으로 대체해 형식 추가 시 O(N²) 수동 증식을 제거.
  • 공통 ExternalProcessRunner + 외부 도구 어댑터 추상화 신설 — 3곳에 복붙된 LibreOffice 호출(DocumentProvider/DocxProvider/HwpxProvider)을 타임아웃·stderr 수집·Kill 통합 단일 러너로 합치고, FFmpeg·Ghostscript·qpdf·codex CLI를 manifest 기반으로 동일 패턴 재사용. 신규 외부 도구 추가가 보일러플레이트 복붙 없이 끝나게.
  • 신규 1급 Provider 4종 추가 — LlmProvider(요약·번역·캡션·OCR교정·메타데이터, MEAI IChatClient 추상화 + Codex CLI opt-in), FfmpegProvider(영상/오디오 N×M 코덱·압축, RequiresExternal + LGPL 빌드 자동조달), PdfToolProvider(PDF→PDF 압축/병합/분할, PDFsharp in-process + gs 고급압축 폴백), 그리고 DocumentProvider 출력 매트릭스 확장으로 HWP→DOCX/HTML/TXT/PDF 완성.
  • 배치 병렬화 + 헤드리스 CLI — 순차 for-loop(ConversionEngine.cs:57-70)를 Parallel.ForEachAsync로 교체(AI 왕복·영상 트랜스코딩의 치명적 병목 해소)하고, stdout JSON + exit code + 옵션 플래그(--quality/--prompt/--codec/--json)를 받는 headless 모드를 분리해 AI 스크립팅·배치 자동화를 가능케.

최대 리스크 · AI·미디어 기능을 최단 경로로 밀다 보면 'Phase 0 인프라 일반화'를 건너뛰고 LlmProvider/FfmpegProvider를 또 하드코딩으로 끼워넣으려는 유혹이 가장 크다 — 그러면 DocumentProvider.RouteAsync처럼 새 switch 지옥이 카테고리마다 생겨

심사 종합 권고

승자는 idx 2(AI·미디어 기능 우선, 89점)를 '실행 골격'으로, idx 0(그래프 코어, 84점)을 '아키텍처 영혼'으로 삼아 종합한다. 단독 채택이 아니라 두 안의 합성이 정답이다.

핵심 통찰: 세 안의 기술적 부품(자체 Dijkstra 멀티홉, ExternalProcessRunner 통합, ConvertRequest/ConvertResult 일반화, LossClass 가중치, Priority 충돌 모델, AI=엣지, DPAPI 키저장)은 사실상 동일하다. 진짜 차이는 '무엇을 북극성으로 삼아 순서를 짜느냐' 하나뿐이다. 이 프로젝트는 단일 개발자가 직접 push하고 GUI로 검증하며 큰 결정을 빠르게 승인하는 워크플로(프로젝트 메모리)이고, 사용자가 명시적으로 요구한 것은 AI/미디어/HWP/PDF압축이라는 '기능'이다. 따라서 '보이지 않는 리팩터링의 함정'(idx 0 본인이 인정한 최대 리스크)에 빠지는 그래프-우선 순서는 이 맥락에서 부적합하다.

그러나 idx 2의 최대 리스크('최단 경로 압박으로 LlmProvider/FfmpegProvider를 또 하드코딩 switch로 끼워넣어 RouteAsync 지옥 재생산')는 실재하고, idx 0의 그래프 코어가 바로 이 리스크의 백신이다. 그래서 둘을 봉합하는 마스터플랜은 다음 순서다:

Phase 0 (기반, 그러나 즉시 가치와 묶기 — idx 0의 자기 완화책 채택): ExternalProcessRunner 통합(3중 복제 제거) + Priority 충돌 모델 + ConvertRequest/ConvertResult 일반화(어댑터로 무중단). 동시에 DocumentProvider.RouteAsync를 원자 엣지로 분해하고 자체 Dijkstra를 넣어 '손그림=그래프 동일 동작'을 회귀 테스트로 증명(테스트 0개 탈출의 첫걸음). 이 단계의 가시 성과는 transitive closure로 늘어나는 '만들 수 있는 포맷 목록'.

Phase 1 (즉시 체감 — idx 2 순서): HWP 출력 매트릭스 확장(이미 깔린 LibreOffice+H2Orestart 배관 재사용 → DOCX/HTML/TXT) + PDF 압축. 이때 신규 기능은 반드시 '그래프 엣지'로만 추가한다는 것을 불변식으로 박아 idx 2의 하드코딩 유혹을 Phase 0 그래프가 구조적으로 차단.

Phase 2~3 (미디어 + AI 부가가치 레이어 — idx 2): FFmpeg/Ghostscript/qpdf를 idx 2의 라이선스 게이트(분리 프로세스·LGPL/AGPL 경계) 하에 엣지로 추가. AI는 idx 0의 '엣지' + idx 2의 '✨AI 배지·키 없으면 비활성·기본 경로 불점유' 이중 불변식으로 통합. 배치 병렬화는 이 시점에 필수.

idx 1(플러그인 생태계, 71점)은 베이스로는 과잉 엔지니어링이라 탈락하지만, 두 아이디어는 흡수한다: (1) Priority 기반 충돌 모델(이미 Phase 0에 편입), (2) manifest를 '풀 DSL'이 아니라 'ExternalProcessRunner 위 선언적 인자 템플릿'으로 축소해 qpdf/gs 같은 단순 CLI를 코드 없이 추가하는 좁은 용도로만 채택. 복잡 로직(FFmpeg HW가속·AI)은 in-box 코드 원칙을 P1부터 못박아 manifest 갓오브젝트화를 방지한다(idx 1 본인의 하이브리드 경계 그대로).

한 줄 요약: idx 2의 '기능이 견인하는 로드맵'에 idx 0의 '그래프가 받치는 코어'를 Phase 0에 심어, 사용자 체감 가치를 빠르게 내면서도 RouteAsync 지옥의 재발을 그래프로 원천 차단한다.

마스터플랜에 흡수한 최고의 아이디어

  • [idx 0의 핵심] DocumentProvider.RouteAsync(92-205) 손그림 멀티홉을 '삭제'하고 엔진이 Dijkstra로 동일 경로를 계산하게 만드는 도그푸딩 — 이것을 회귀 테스트로 '그래프=손그림 동일 동작' 객관 증명. 테스트 0개인 현 상태에서 이 변환의 첫 안전망이 된다. 어떤 마스터플랜이 채택되든 이 검증 루프는 필수.
  • [idx 0의 핵심] 손실을 -log(보존율)+홉페널티 단일 가중치로 ConversionPair.LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8) 필드에 SSOT화. 이것이 멀티홉 경로 선택과 UI '⚠손실' 배지의 단일 출처. idx 2의 '손실 변환 경고 배지'도 이 가중치를 그대로 소비.
  • [idx 0의 핵심] AI를 IAiProvider 특수 인터페이스가 아니라 '로컬 변환이 없는 신규 엣지(요약/번역/캡션)'로 그래프에 환원 — idx 2의 '후처리 부가가치 레이어'와 결합하면, AI는 그래프상 엣지이면서 동시에 키 없으면 자동 비활성 노드 + ✨AI 배지로 노출되는 이중 안전장치를 얻는다.
  • [idx 2의 핵심] AI 불변식: 키가 없어도 모든 기존 변환 100% 동작, AI는 절대 기본 경로를 점유하지 않고 ✨AI 배지 페어로만 opt-in, 키 부재 시 등록 순서·게이트로 조용히 비활성. '변환은 로컬에서 예측가능' 신뢰를 깨지 않는 설계 불변식.
  • [idx 2의 핵심] 라이선스 경계를 코드 리뷰 게이트로 강제: FFmpeg는 GPL 정적링크 금지·LGPL 분리호출만, Ghostscript/MuPDF는 AGPL이라 사용자 설치본 감지만, H2Orestart/Calibre는 GPL이라 외부 프로세스 분리. 모든 무거운 외부 도구 = '별도 프로세스 분리 호출 + 사용자 설치 감지 또는 LGPL 빌드 자동조달'. .NET 9 단일 EXE 상업 배포 오염 방지의 핵심.
  • [idx 2의 핵심] 단계 순서: PDF 압축 + HWP 출력 매트릭스 확장을 최우선 출시(이미 HwpxProvider의 LibreOffice+H2Orestart 배관 존재 → DOCX/HTML/TXT 출력만 추가하면 즉시 신규 가치). 초기 체감 가치를 그래프 리팩터링보다 먼저.
  • [idx 2의 핵심] 배치 병렬화: ConversionEngine 순차 for-loop(57-70)를 Parallel.ForEachAsync(동시성 제한 포함)로 교체. AI 네트워크 왕복·영상 트랜스코딩의 치명적 병목 해소. 더불어 ImageMagick ResourceLimits 전역 설정 + decompression bomb 방어로 미디어 공격면 차단.
  • [idx 1의 핵심] 레지스트리 충돌 모델 교체: _byPair.TryAdd(22)의 조용한 first-wins를 ProviderCapability.Priority + 다중 Provider 공존으로 교체 + 충돌 시 진단 경고. 같은 (input,output)에 빠른변환/고품질/AI 등 복수 전략 등록 가능. idx 0/2 모두 이 교체가 전제 조건.
  • [idx 1의 부분 채택] manifest는 '풀 생태계 비전'이 아니라 'ExternalProcessRunner 위의 선언적 인자 템플릿({input}/{output}/{outdir}/{format})'으로만 제한 채택 — qpdf/Ghostscript 같은 단순 CLI 압축 도구를 코드 없이 추가하는 용도. 단, 복잡 로직(FFmpeg HW가속 폴백/AI)은 in-box 코드 Provider 원칙을 P1부터 못박아 manifest 갓오브젝트화 방지.
  • [3안 공통] ExternalProcessRunner 단일 추상화로 LibreOffice 3중 복제(DocumentProvider:238-281 / DocxProvider:113-157 / HwpxProvider:107-151) 통합 — 타임아웃·stderr 수집·Kill 일원화. 이후 모든 외부 엣지(FFmpeg/Ghostscript/qpdf/Codex CLI)가 이 러너 하나 공유. 세 안이 만장일치로 지목한 가장 안전하고 즉시 실행가능한 첫 리팩터링.
  • [3안 공통] IConverterProvider 시그니처(9-15)를 ConvertRequest/ConvertContext로 일반화 + ConvertResult(10)에 비파일 산출물 필드(추출 텍스트·AI 응답·미디어 메타데이터) 추가. 단, 기존 8개 Provider는 어댑터로 감싸 점진 마이그레이션 + 회귀 테스트로 무중단 보장(idx 0의 마이그레이션 전략 채택).
  • [3안 공통] ConvertOptions 갓 오브젝트(14+ sub-record)를 그래프 옵션(AllowMultiHop/MaxHops/AvoidLossy) + 형식별 옵션 백(IReadOnlyDictionary 또는 Provider 선언형 스키마)으로 분해 → Video/Audio/Ai/PdfCompress를 sub-record 증식 없이 수용. DPAPI(ProtectedData) 기반 ISettingsStore 신설로 API 키·도구 경로 안전 저장.
12

인터넷 리서치 (7개 토픽)

2026년 6월 기준 WebSearch로 조사한 라이브러리·방법론·라이선스. 클릭하면 출처까지 펼쳐진다.

그래프 기반 멀티홉 변환 경로 탐색 아키텍처 (Everything2Everything 적용)

핵심 발견

  • **Pandoc = 단일 AST 허브-앤-스포크**: 모든 포맷을 하나의 중립 AST(Pandoc AST)로 파싱(reader)하고 거기서 각 포맷으로 직렬화(writer)한다. M개 reader + N개 writer만 구현하면 M×N 변환을 자동 커버하고, 새 포맷은 reader/writer 1개 추가로 끝. 단 이 모델은 '한 도메인 안에서 의미가 보존되는 공통 표현'이 존재할 때(텍스트/마크업)만 성립한다. 이미지/오디오/문서를 하나의 AST로 묶는 건 불가능 — Everything2Everything처럼 도메인이 이질적이면 단일 허브가 아니라 '여러 허브를 가진 변환 그래프'가 정답이다.
  • **NCSA Polyglot / Conversion Software Registry(CSR)가 이 프로젝트의 정확한 청사진**: 노드=파일 포맷(확장자), 엣지=특정 소프트웨어를 통한 (입력→출력) 변환, 가중치=변환 시 '정보 보존량(information retained)'. 입력→출력 최단 경로를 탐색해 멀티홉 체인을 자동 생성한다. 손실 정량화는 별도 프레임워크 Versus(file-to-file 비교)로 측정해 엣지 가중치로 환산 → '정보 손실이 가장 적은 경로'를 고른다. 즉 노드=포맷 / 엣지=Provider / 가중치=손실 모델은 학계에서 이미 검증된 best practice다.
  • **가중치 모델링 best practice = 비용을 곱셈이 아니라 덧셈으로 만들기**: Dijkstra/A*는 경로비용이 엣지비용의 '합'일 때 동작한다. 손실은 본래 곱셈적(0.9 × 0.8...)이므로 `weight = -log(품질보존율)` 형태로 변환하면 합산 최단경로가 곧 '최대 품질보존 경로'가 된다. 여기에 lossy 엣지에 큰 페널티, lossless(컨테이너 재포장·무손실 코덱)에 0에 가까운 비용, 외부 도구 실행/렌더링 속도 비용을 가중합으로 섞는다. 홉 수 자체에도 작은 상수 페널티를 줘 '불필요하게 긴 체인'을 억제한다.
  • **손실 경로 회피 핵심 규칙들**: (1) 같은 lossy 인코딩을 두 번 거치지 않게 한다(JPEG→PNG→JPEG 같은 generation loss는 누적·비가역). (2) lossy→lossless 변환은 데이터를 복원하지 못하므로(이미 버려진 정보) 가중치에 반영. (3) 래스터화는 '단방향 손실 절벽' — 벡터/텍스트(PDF·SVG·DOCX)를 PNG로 한 번 떨구면 텍스트·벡터 정보가 영구 소실되므로, 래스터를 중간 허브로 쓰는 경로는 '꼭 필요할 때만(예: OCR, 썸네일)' 허용하고 가중치를 매우 높게 준다.
  • **중간 포맷(허브) 선택 전략**: 문서 도메인은 HTML/Markdown 또는 OOXML(DOCX)을 허브로(현재 DocumentProvider가 이미 HTML을 사실상 허브로 사용 중). 인쇄·레이아웃 보존이 중요하면 PDF가 허브(이미 PDFium 보유). 이미지 도메인은 무손실 중간 포맷(PNG/TIFF, 또는 ImageMagick 내부의 MIFF)을 허브로 써 generation loss를 막는다. 즉 '하나의 글로벌 허브'가 아니라 '도메인별 허브 + 도메인 간 경계는 의도적 손실 게이트(PDF, PNG)'로 설계하는 게 핵심.
  • **ImageMagick delegate = 이미 멀티홉 엔진**: decode/encode만 지정된 delegate를 자동으로 이어붙여 중간 포맷 체인을 만든다(예: BPG→PNG(중간)→내부표현→출력). %i(입력)/%o(출력)/%u(고유 임시파일) 토큰으로 중간 임시파일을 관리. 같은 변환에 delegate 여러 개면 선언 순서대로 시도하다 성공하는 것을 채택(우선순위=순서 + 가용성 fallback). Everything2Everything의 MagickProvider는 이 체인을 라이브러리 내부에서 이미 활용 중이므로, 이미지 노드 사이는 사실상 단일 '슈퍼노드'로 묶어도 된다.
  • **FFmpeg filtergraph = DAG 기반, format negotiation**: 노드=필터, pad=타입 있는 입출력 포트, 엣지=프레임 흐름. source/sink 개념, 사이클·다중 링크 허용. 핵심 시사점은 '인접 노드 간 포맷 협상(format negotiation)' — 변환 그래프에서도 각 Provider가 받을 수 있는/내보낼 수 있는 포맷 집합을 선언하고 엔진이 그 교집합으로 연결을 결정하는 구조가 견고하다.
  • **현재 코드 상태**: ProviderRegistry는 `(Input,Output)→Provider` 단일 홉 딕셔너리(`_byPair`)만 갖고, 멀티홉 경로 탐색이 전혀 없다. 멀티홉은 DocumentProvider.RouteAsync 안에 `md→html→docx`, `docx→html→md` 식으로 하드코딩되어 Provider 내부에 묻혀 있다. 이 하드코딩 분기들이 바로 '그래프로 끌어올려야 할' 멀티홉 로직이다.

권장 접근 (.NET 9 / 단일 EXE)

단일 글로벌 AST 허브(Pandoc식)는 도메인이 이질적인 이 프로젝트에 부적합하다. 대신 **NCSA Polyglot 모델(노드=포맷, 엣지=Provider, 가중치=손실)을 ProviderRegistry 위에 얇은 그래프 레이어로 얹는 것**을 권장한다. **1) 그래프 빌드 (앱 시작 시 1회)**: 모든 Provider의 Capability.SupportedConversions를 순회해 방향 그래프를 만든다. 노드=정규화된 확장자, 엣지=해당 Provider+ConversionPair. 노드 수가 수십 개, 엣지 수가 수백 개 수준이므로 그래프는 매우 작다. **2) 경로 탐색**: Dijkstra(또는 A*) 1회로 충분. 비용은 엣지 가중합 = `α·(-log 품질보존율) + β·홉상수 + γ·실행비용`. lossy 게이트(래스터화, lossy 재인코딩)에 큰 가중치를 줘 손실 경로를 자연스럽게 회피한다. 직접 엣지(단일 홉)는 항상 비용이 낮아 기존 동작과 호환된다. **3) 라이브러리 선택**: 그래프가 작고 알고리즘이 표준적이므로 **외부 의존성 없이 자체 Dijkstra 약 80~120줄로 구현하는 것을 1순위로 권장**한다. 단일 포터블 EXE/MSIX 배포에 유리하고(트리밍·AOT 충돌 없음), MS-PL 같은 라이선스 검토도 불필요하다. 직접 구현이 부담되면 QuikGraph(MS-PL, net5~net10 호환)를 쓰되 2022년 이후 릴리스가 없는 점을 감안한다. **4) 손실 정량화(선택적 고도화)**: 초기에는 포맷 쌍별 정적 가중치 테이블(lossless=0.0, 컨테이너변환=0.05, lossy재인코딩=0.4, 래스터화=0.8 등)로 시작하고, 추후 Versus처럼 실제 결과물을 비교해 가중치를 보정하는 단계로 확장한다. 처음부터 동적 측정을 넣을 필요는 없다. **5) 안전장치**: 멀티홉은 '직접 엣지가 없을 때만' 발동하게 하고, 최대 홉 수(예: 3)와 '명시적으로 금지된 손실 전이' 블랙리스트를 둔다. 중간 산출물은 임시 폴더에 만들고 마지막에 정리(DocumentProvider의 workDir 패턴 그대로 재사용).

라이브러리 · 도구

이름용도라이선스성숙도
자체 Dijkstra 구현 (직접 작성)ProviderRegistry 위에 포맷 그래프 + 가중치 최단경로 탐색을 직접 구현 (PriorityQueue<TElement,TPriority>는 .NET 9 BCL에 내장) N/A (프로젝트 코드)production (표준 알고리즘, 그래프 규모가 작아 검증 부담 낮음)
QuikGraph방향 그래프 자료구조 + Dijkstra/A*/k-shortest path/BFS 등 알고리즘 제공 MS-PL (Microsoft Public License, 상업적 사용 가능)active이나 정체 (최신 2.5.0이 2022-07 릴리스, 이후 신규 릴리스 없음 / 다운로드 1300만+)
Kemsekov.GraphSharpDijkstra·그래프 컬러링·컴포넌트 등 알고리즘, QuikGraph 어댑터 제공 확인 필요 (NuGet/리포 라이선스 확인 권장)active (3.1.x 최근 업데이트, QuikGraph보다 활발)
Dijkstra.NET우선순위 큐 기반 Dijkstra(O(E log V)) 단일 목적 라이브러리 MIT (리포 확인 권장)beta/소규모 (단순·경량, 업데이트 빈도 낮음)
Pandoc (외부 CLI, 참조 아키텍처)문서 도메인 단일 AST 허브 변환 엔진. 라이브러리가 아니라 '허브-앤-스포크' 설계 참조 + 선택적 외부 도구 GPL-2.0+ (CLI를 번들 없이 외부 호출하면 프로젝트 라이선스에 영향 없음)production (업계 표준, 활발)

통합 노트

현재 ProviderRegistry는 `_byPair`(단일 홉)와 `_outputsByInput`만 갖고 있고, 멀티홉은 DocumentProvider.RouteAsync에 하드코딩돼 있다. 다음 단계로 그래프 레이어를 얇게 얹는 것을 권장한다. **1) ConversionGraph (신규, ProviderRegistry 내부 또는 옆에)**: 생성자에서 모든 Provider의 Capability.SupportedConversions를 순회해 `Dictionary<string, List<Edge>>`(노드=확장자, Edge={Provider, ConversionPair, Weight})로 인접 리스트를 만든다. ProviderRegistry 생성자 루프(현재 17~30행)에 그래프 빌드 한 단계만 추가하면 된다. **2) 가중치 부여**: ProviderCapability에 정적 손실 등급을 노출하는 게 깔끔하다. 예) `ConversionPair`에 선택적 `LossClass`(Lossless/Container/Recode/Rasterize) 필드를 추가하거나, Provider가 `double EstimateCost(ConversionPair)`를 구현(IConverterProvider 확장). 가중치는 `-log(보존율)+홉페널티` 합산. 기존 Provider는 기본값(직접 변환=저비용)으로 두면 무중단 마이그레이션 가능. **3) ConvertOptions 확장**: `bool AllowMultiHop`(기본 true), `int MaxHops`(기본 3), `bool AvoidLossy`(true면 래스터화·lossy 재인코딩 엣지를 큰 페널티/제외) 옵션을 추가. 현재 ConvertOptions 패턴(섹션별 옵션 객체)에 자연스럽게 들어간다. **4) ConversionEngine.ConvertOneAsync 수정**: 현재 91행 `_registry.TryGet`이 직접 매핑만 본다. 여기서 직접 엣지가 없으면(또는 더 저비용 멀티홉이 있으면) `ConversionGraph.FindBestPath(inExt, outExt, options)`로 경로를 구해, 경로의 각 홉을 순차 실행하는 `ExecuteChainAsync`로 위임한다. 중간 산출물은 DocumentProvider가 이미 쓰는 `workDir = Temp/e2e_..._{Guid}` 패턴을 공용 헬퍼로 올려 재사용하고, 각 홉은 기존 `provider.ConvertAsync`를 그대로 호출(인터페이스 변경 불필요). 진행률은 홉 수로 분할해 IProgress<double>에 매핑. **5) DocumentProvider 단순화(점진적)**: 그래프 레이어가 안정화되면 RouteAsync의 `md→html→docx` 같은 하드코딩 멀티홉 분기를 제거하고, DocumentProvider는 '단일 홉 원자 변환'(md→html, html→docx 등)만 선언하게 만든다. 그러면 md→docx는 엔진의 그래프 탐색이 자동으로 md→html→docx로 합성한다. 이게 Pandoc식 '작은 변환의 조합' 철학을 레지스트리 수준에서 실현하는 것. **6) UI/탐색 표시**: OutputsForInput가 지금은 직접 출력만 반환한다. 멀티홉을 켜면 도달 가능한 모든 출력(그래프 reachability)으로 확장할 수 있어, '이 파일로 만들 수 있는 모든 포맷' 목록이 훨씬 풍부해진다. 단, 손실 경로로만 도달하는 출력은 UI에서 경고 배지(예: '손실 변환')로 구분해 사용자에게 알리는 걸 권장(Versus 철학의 경량판).

Codex non-interactive OAuth + API를 .NET 9/WPF 데스크톱 파일 변환기(Everything2Everything)에 통합

핵심 발견

  • [가장 중요] ChatGPT 구독 OAuth 토큰은 Codex 백엔드 전용이다. auth.json에 저장된 access/refresh 토큰은 codex CLI가 자체 백엔드를 호출할 때만 유효하며, 이 토큰을 꺼내 api.openai.com(공식 OpenAI API)에 직접 Bearer로 붙여도 동작하지 않는다. 따라서 '구독으로 API를 공짜로 쓰는' 경로는 오직 codex CLI 프로세스를 외부 실행하는 방법뿐이고, SDK를 직접 호출하려면 반드시 별도의 종량제 API 키(CODEX_API_KEY 또는 OPENAI_API_KEY)가 필요하다. 이 둘은 과금 모델이 완전히 분리된 별개 경로다.
  • Codex CLI 인증은 3가지: (1) 브라우저 ChatGPT OAuth(`codex login`) — 구독 사용, (2) 디바이스 코드 플로우(`codex login --device-auth`, 2026년 3월 추가, 헤드리스/원격용 베타) — URL+코드를 다른 기기에서 입력, (3) API 키(`CODEX_API_KEY`/`OPENAI_API_KEY` 환경변수) — CI/CD·프로그래매틱 권장. 토큰은 기본 `~/.codex/auth.json`(Windows는 `%USERPROFILE%\.codex\auth.json`)에 평문 JSON으로 저장되며, config의 `cli_auth_credentials_store`를 file/keyring/auto로 바꿀 수 있다.
  • 비대화형 실행은 `codex exec "<프롬프트>"`. 주요 플래그: `--json`(stdout이 JSONL 이벤트 스트림), `--output-schema <schema.json>`(응답을 JSON Schema로 강제 — 메타데이터/구조화 추출에 핵심), `-o/--output-last-message <path>`(최종 메시지를 파일로), `--model <name>`, `--cd <dir>`(작업 디렉터리), `--skip-git-repo-check`(git 저장소 아닌 폴더 허용 — 변환 앱에 필수), `--sandbox read-only|workspace-write|danger-full-access`, `--ephemeral`(세션 미저장). 프롬프트 안에 파일/이미지 경로를 직접 적으면 Codex가 읽어들인다.
  • 헤드리스 부트스트랩: 브라우저 있는 PC에서 `codex login` 후 생성된 auth.json을 헤드리스 머신으로 복사하거나, `printenv CODEX_ACCESS_TOKEN | codex login --with-access-token`로 토큰 주입 가능. CI에서는 auth.json을 매 실행 덮어쓰면 refresh된 토큰이 stale해지는 race가 있어 'if [ ! -f ] 가드'와 concurrency 직렬화가 권장된다 — 데스크톱 앱이 동시 변환 다건을 돌릴 때 동일 문제 발생 가능(아래 risks 참조).
  • 공식 OpenAI .NET SDK는 NuGet `OpenAI`(v2.10.0, 2026-04-04), MIT 라이선스, netstandard2.0 타깃이라 .NET 9에서 문제없이 동작. Chat Completions·비전(이미지 입력)·Structured Outputs(JSON Schema, gpt-4o 계열 이상) 모두 지원, 활발히 유지보수 중. Azure 전용 확장은 `Azure.AI.OpenAI`(공식 OpenAI 패키지 위에 얹힘). `OpenAI-DotNet`(8.x), `tryAGI.OpenAI`(4.x)는 커뮤니티 대안.
  • Anthropic Claude는 2026년부터 공식 .NET SDK가 NuGet `Anthropic` 패키지(v12.23.0, 2026-05-21, netstandard2.0)로 제공 — anthropics/anthropic-sdk-csharp. 이름이 비슷한 `Anthropic.SDK`(tghamm, 5.x)와 `tryAGI.Anthropic`은 비공식이다. 공식 패키지를 쓰는 것이 권장.
  • Microsoft.Extensions.AI(MEAI) 1.0이 2026-04-03 정식 출시(stable, MIT). `IChatClient` 단일 추상화로 OpenAI/Anthropic/Ollama/Bedrock/Gemini를 한 인터페이스로 다루며, 멀티모달(텍스트+이미지) 메시지를 지원. 프로바이더 교체가 한 줄 변경이라 이 프로젝트의 Provider/Registry 'OpenAI냐 Claude냐를 사용자가 선택' 요구에 정확히 들어맞는다.

권장 접근 (.NET 9 / 단일 EXE)

단일 포터블 EXE / MSIX 배포라는 제약이 결정적이다. codex CLI는 별도 설치가 필요한 외부 Node 기반 바이너리이고, 포터블 EXE에 번들하기 어렵고(수백 MB), OAuth 토큰을 API로 재사용할 수 없으므로 '비용 절감' 명분도 사라진다. 따라서 기본 경로는 SDK 직접 호출로 가는 것이 맞다. 권장 아키텍처(2층): 1) 추상화 층 — Microsoft.Extensions.AI의 `IChatClient`를 내부 LLM 게이트웨이로 채택. OpenAI는 공식 `OpenAI`(MIT) + MEAI OpenAI 커넥터, Claude는 공식 `Anthropic` 패키지(MEAI Anthropic 커넥터)로 연결. 사용자는 설정에서 'OpenAI / Claude / (옵션)Codex CLI'를 고르고 API 키만 입력하면 된다. 키는 Windows DPAPI(ProtectedData)로 암호화해 로컬 저장 — 포터블 EXE에서도 사용자별 암호화 가능. 2) 선택적 Codex CLI 백엔드 — ChatGPT Pro/Plus 구독을 이미 보유한 파워유저를 위해, codex가 PATH에 감지될 때만 활성화되는 보조 백엔드로 둔다. `CheckAvailabilityAsync`에서 `codex --version` 프로브 → 실패 시 RequiresExternal로 다운로드 안내. 실행은 `codex exec --skip-git-repo-check --json --output-schema schema.json -o out.json --cd <tempdir> "<프롬프트 + 파일경로>"` 형태로 Process 호출, JSONL 마지막 메시지 파싱. 즉 '기본은 API 키 + 공식 SDK(MEAI 추상화), Codex CLI는 구독자용 opt-in 보조 경로'의 하이브리드가 이 프로젝트에 최적이다. LLM 자체는 변환의 '핵심 엔진'이 아니라 '후처리/부가가치 단계'(요약·번역·포맷 정규화·OCR 교정·이미지 캡션·메타데이터)로 배치해, 키가 없어도 기존 변환은 100% 동작하고 LLM 기능만 비활성(ComingSoon/RequiresExternal 스타일)되게 한다.

라이브러리 · 도구

이름용도라이선스성숙도
OpenAI (공식 .NET SDK)OpenAI API 직접 호출 — Chat Completions, 비전(이미지 입력), Structured Outputs(JSON Schema). 문서 요약/번역/메타데이터 생성/이미지 캡션의 기본 엔진 MITproduction (v2.10.0, 2026-04-04, 활발히 유지보수, netstandard2.0이라 .NET 9 호환)
Anthropic (공식 Claude .NET SDK)Claude API 직접 호출 — OpenAI 대안. 긴 문서 요약/번역에 강점, 비전 지원 MIT (anthropics/anthropic-sdk-csharp)production (v12.23.0, 2026-05-21, 공식). 주의: 비공식 'Anthropic.SDK'(tghamm)·'tryAGI.Anthropic'과 혼동 금지
Microsoft.Extensions.AI / .AbstractionsIChatClient 단일 추상화로 OpenAI·Claude·Ollama를 동일 인터페이스로 — Provider/Registry의 LLM 백엔드 선택 계층 MITproduction (1.0 정식, 2026-04-03, 멀티모달 지원). 본 프로젝트 추상화와 가장 정합
OpenAI Codex CLI외부 프로세스(codex exec)로 ChatGPT 구독 OAuth를 재사용해 LLM 호출 — 구독 보유자 opt-in 보조 경로 Apache-2.0 (openai/codex 리포)active (2026년 활발, GPT-5.5 에이전틱). 단 별도 설치 필요·OAuth 토큰 API 재사용 불가·포터블 번들 부적합
Azure.AI.OpenAIAzure OpenAI Service를 쓸 경우의 확장(공식 OpenAI 패키지 위에 얹힘). 일반 OpenAI만 쓸 거면 불필요 MITproduction (v2.1.0). 본 프로젝트엔 선택적
tryAGI.OpenAI / OpenAI-DotNetOpenAI 비공식 커뮤니티 SDK 대안 MITactive (각각 v4.2.0 / v8.8.x). 공식 OpenAI 패키지가 있으므로 우선순위 낮음

통합 노트

현 추상화(IConverterProvider / ProviderCapability / ProviderRegistry / ConvertOptions)에 자연스럽게 끼워넣는 방법: 1) 신규 `LlmProvider : IConverterProvider` 추가 (OcrProvider와 동일한 패턴). SupportedConversions를 LLM 후처리 매트릭스로 정의: - 요약: .pdf/.docx/.txt/.md → .txt/.md (summary) - 번역: .txt/.docx/.md → .txt/.docx (대상 언어는 옵션) - 포맷 정규화: .txt → .md, .csv → .md(표) - OCR 교정: OcrProvider 출력(.txt)을 받아 LLM이 오탈자/줄바꿈 정리 (파이프라인 2단계) - 이미지 캡션/대체텍스트: .png/.jpg → .txt (비전) - 메타데이터 생성: 임의 입력 → .json (제목/태그/요약, Structured Outputs) OcrProvider가 PdfProvider를 생성자 주입으로 재사용하듯, LlmProvider도 텍스트 추출이 필요하면 DocumentProvider/PdfProvider/OcrProvider를 주입받아 '추출→LLM' 2단계로 구성한다. 2) `ConvertOptions`에 `LlmOptions Llm { get; set; } = new();` 추가. 필드 예: Backend(\"openai\"|\"anthropic\"|\"codex-cli\"|\"auto\"), Model, ApiKey(또는 키 저장소 참조), Task(Summarize/Translate/Normalize/Caption/Metadata), TargetLanguage, MaxTokens, Temperature. 기존 OcrOptions와 동일한 스타일이라 직렬화·UI 바인딩 일관성 유지. 3) `CheckAvailabilityAsync`가 게이트 역할: - SDK 경로: API 키(설정 또는 OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수) 존재 확인 → 없으면 ProviderAvailability.NotReady(\"API 키 미설정\", MissingDependencies). ExternalDependency로 키 발급 URL 안내. - Codex 경로: `codex --version` 프로세스 프로브 + auth.json 존재 확인 → 없으면 ProviderStatus.RequiresExternal로 다운로드/로그인 안내. 기존 OcrProvider가 OCR 언어팩 유무로 NotReady를 반환하는 패턴을 그대로 따른다. 4) `ConvertAsync` 구현: IProgress<double> / CancellationToken 시그니처 그대로 유지. SDK 경로는 IChatClient.GetResponseAsync(스트리밍 시 진행률 갱신), Codex 경로는 Process.Start로 `codex exec --json --output-schema ... -o out.json` 실행 후 표준출력 JSONL 파싱. 출력 파일은 기존 OutputPathHelper.ResolveOutputPath + OnCollision 규칙을 재사용. 실패 시 ConvertResult.Fail, 건너뛰기 ConvertResult.Skip로 통일. 5) ProviderRegistry는 수정 불필요 — 생성자에서 providers 목록에 LlmProvider 인스턴스만 추가하면 _byPair 매트릭스에 자동 편입된다. 단 LLM은 비결정적·유료·네트워크 의존이므로, 동일 (input,output) 페어를 로컬 변환 Provider가 이미 점유한 경우 TryAdd가 먼저 등록된 쪽을 유지하는 현 동작 덕분에 'LLM은 로컬 변환이 없는 신규 페어(요약/번역 등)에만 노출'되도록 등록 순서를 조정하면 된다. UI에서는 LLM 출력 페어에 '✨ AI' 배지를 붙여 종량 과금/네트워크 사용을 사용자에게 명시할 것. 6) 키 보안: 포터블 EXE에서도 System.Security.Cryptography.ProtectedData(DPAPI, CurrentUser)로 키를 암호화해 %APPDATA% 또는 앱 폴더에 저장. 환경변수 OPENAI_API_KEY/ANTHROPIC_API_KEY도 fallback으로 읽어 CI/파워유저 친화.

FFmpeg .NET 통합 (영상/오디오/코덱 변환) — Everything2Everything 적용 리서치

핵심 발견

  • 라이브러리 선택은 명확하다: FFMpegCore가 정답이다. v5.4.0 (2025-10-27 릴리스, 누적 600만 다운로드, 일 3K, MIT 라이선스, .NET Standard 2.0+ → .NET 9 호환). 라이브러리 자체가 MIT이므로 상업적 사용에 제약이 전혀 없다.
  • Xabe.FFmpeg는 라이브러리 코드 자체가 CC BY-NC-SA 3.0 (비상업) 라이선스다. 상업적 사용은 별도 유료 상업 라이선스 구매가 필요하다. 인용: 'You may use Software under Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0) license for non commercial projects.' → 상업 사용 선호 방침상 탈락.
  • 핵심 라이선스 함정은 라이브러리가 아니라 FFmpeg 바이너리 자체다. FFmpeg는 기본 LGPL 2.1+이지만 --enable-gpl(libx264/libx265 H.264/H.265 인코더 포함) 또는 --enable-nonfree(libfdk-aac) 빌드는 GPL/비배포 라이선스로 바뀐다. 폐쇄소스 상업 EXE에 가장 흔한 gyan.dev 빌드(GPLv3)나 BtbN gpl 빌드를 번들하면 안 된다.
  • 결정적 발견: 폐쇄소스 상업 배포에는 BtbN의 'lgpl-shared' 빌드를 써야 한다. LGPL 준수 조건은 (1) --enable-gpl/--enable-nonfree 없이 빌드, (2) 동적 링크(여기선 별도 ffmpeg.exe 프로세스 호출이 가장 안전한 동적 분리), (3) FFmpeg 소스 코드 제공 의무(또는 다운로드 링크), (4) 다운로드 페이지/앱 내 FFmpeg 사용 고지(attribution)다. 이미 LibreOffice를 외부 도구로 호출하는 본 프로젝트 구조와 정확히 일치한다.
  • NVENC(h264_nvenc/hevc_nvenc)는 LGPL 빌드에서 --enable-nonfree 없이 사용 가능하다. NVIDIA의 FFmpeg/NVENC 인터페이스 작성자(Philip Lachsinger)가 직접 'turning it on did not stop your ffmpeg build from being lgpl compliant (it does not require the non-free flag)'라고 확인. 헤더는 MIT, 런타임은 GPU 드라이버의 system library exception 적용. → LGPL-shared 빌드 + NVENC HW 가속이 상업 배포에 합법적으로 양립한다.
  • 코덱 라이선스 매트릭스(상업 배포 관점): H.264/H.265 인코딩은 libx264/libx265=GPL(폐쇄소스 불가) 대신 하드웨어 인코더(h264_nvenc/hevc_nvenc, h264_qsv/hevc_qsv, h264_amf)를 쓰면 LGPL 유지. AV1=libaom/libsvtav1(BSD-like, royalty-free), VP9=libvpx(BSD-like), Opus=libopus(BSD), FLAC(Xiph BSD), AAC=FFmpeg 네이티브 aac 인코더(LGPL, libfdk-aac는 nonfree라 회피). 즉 LGPL-shared 빌드만으로 AV1/VP9/Opus/FLAC/AAC/MP3는 모두 커버되고, H.264/H.265는 HW 인코더로 우회.
  • FFMpegCore는 본 프로젝트의 IConverterProvider 패턴에 매끄럽게 들어간다. 진행률은 NotifyOnProgress(Action<double> onPercentageProgress, TimeSpan totalTimeSpan) → IProgress<double>로 직결, 취소는 CancellableThrough(CancellationToken token, int timeout=0) → 기존 ct 직결, HW 가속은 WithHardwareAcceleration(HardwareAccelerationDevice) (enum: Auto/D3D11VA/DXVA2/QSV/CUVID/CUDA/VDPAU/VAAPI/LibMFX)로 지원.
  • 바이너리 크기 현실: FFmpeg essentials 정적 빌드 ffmpeg.exe는 압축 32MB / 압축해제 ~103MB. 단일 포터블 EXE에 내장하면 100MB가 더해진다. .NET single-file publish는 ffmpeg.exe를 임베드해 자가추출(IncludeNativeLibrariesForSelfExtract)할 수 있으나 시작 시 temp 추출 비용이 크다. 더 나은 전략은 런타임 다운로드(FFMpegDownloader.DownloadFFMpegSuite())다.

권장 접근 (.NET 9 / 단일 EXE)

FFMpegCore 5.4.0(MIT) + FFprobe를 채택하고, FFmpeg 바이너리는 EXE에 번들하지 말고 'RequiresExternal + 최초 사용 시 LGPL-shared 빌드 자동 다운로드' 방식을 권장한다. 근거: (1) 라이선스 — BtbN lgpl-shared 빌드(GPL/nonfree 없음)는 폐쇄소스 상업 EXE에 합법적이며, ffmpeg.exe를 별도 프로세스로 호출(=동적 분리)하면 본 앱 바이너리는 FFmpeg와 분리되어 LGPL 전염 없음. gyan.dev나 BtbN gpl 빌드(GPLv3, libx264/x265 포함)는 절대 번들 금지. (2) 배포 크기 — FFmpeg ~100MB를 단일 EXE에 박으면 다운로드/시작이 무거워지므로, 이미 LibreOffice를 외부 의존성으로 두는 본 프로젝트 철학대로 FFmpeg도 외부 도구로 취급. 구체적으로 FfmpegProvider를 ProviderStatus.RequiresExternal로 등록하고, CheckAvailabilityAsync에서 (a) 시스템 PATH의 ffmpeg.exe, (b) 앱 데이터 폴더(%LOCALAPPDATA%\\Everything2Everything\\ffmpeg)에 받아둔 바이너리, (c) FFMpegCore의 FFMpegDownloader로 최초 1회 자동 다운로드 순으로 탐지/조달. GlobalFFOptions.Configure(new FFOptions{ BinaryFolder = <앱데이터 ffmpeg 경로> })로 경로를 고정. (3) 코덱 전략 — H.264/H.265는 HW 인코더(h264_nvenc/qsv/amf, 미지원 시 mpeg4/우회) 우선, AV1/VP9/Opus/FLAC/AAC(네이티브)/MP3는 LGPL 빌드로 직접 처리. (4) 진행률은 FFprobe로 duration을 먼저 구해 NotifyOnProgress(Action<double>, TimeSpan)을 IProgress<double>에 연결, 취소는 CancellableThrough(CancellationToken)에 기존 ct 연결. MSIX 배포 시에는 자동 다운로드가 샌드박스/네트워크 정책에 걸릴 수 있으니, MSIX 변형에서는 lgpl-shared DLL/EXE를 앱 패키지에 동봉(여전히 LGPL 준수: 동적 호출 + 소스 제공 링크 + 고지)하는 분기 권장.

라이브러리 · 도구

이름용도라이선스성숙도
FFMpegCoreFFmpeg/FFprobe CLI를 감싸는 .NET fluent wrapper. 트랜스코딩, 압축, 포맷 변환, 미디어 분석, 진행률/취소/HW가속 지원. 영상·오디오 Provider의 핵심 엔진. MIT (라이브러리 코드 — 상업 사용 자유. FFmpeg 바이너리 라이선스는 별개)production — v5.4.0(2025-10-27), 누적 600만 다운로드/일 3K, 활발한 유지보수. NuGet rosenbjergsoftworks
Xabe.FFmpegFFmpeg .NET wrapper (대안 후보). 비슷한 트랜스코딩/변환 API. CC BY-NC-SA 3.0 (비상업 전용). 상업 사용은 유료 상업 라이선스 필수active — 유지되나 라이선스 모델이 상업 프로젝트에 부적합
직접 Process 호출 (System.Diagnostics.Process로 ffmpeg.exe 실행)의존성 0, ffmpeg CLI를 직접 ProcessStartInfo로 실행하고 stderr를 파싱해 진행률 추출. N/A (본인 코드, ffmpeg 바이너리만 라이선스 대상)production — 가장 단순/투명하지만 인자 빌드·진행률 파싱(time=/duration 정규식)·에러 처리를 직접 구현해야 함
FFmpeg 바이너리 (BtbN lgpl-shared 빌드)실제 트랜스코딩을 수행하는 네이티브 엔진. GPL/nonfree 미포함 LGPL 빌드. LGPL 2.1+ (--enable-gpl, --enable-nonfree 없음). 폐쇄소스 상업 배포 가능 — 단 동적 분리 호출 + 소스 제공 + 고지 필요production — BtbN/FFmpeg-Builds, 7.1.x 정기 릴리스(2025). winget: BtbN.FFmpeg.LGPL.Shared.7.1
FFmpeg 바이너리 (gyan.dev essentials/full)가장 널리 쓰이는 Windows 정적 빌드. libx264/x265 H.264/H.265 SW 인코딩 포함. GPLv3 (essentials/full 모두 --enable-gpl). 폐쇄소스 상업 EXE에 번들 금지production — 사실상 표준, 정기 갱신

통합 노트

새 FfmpegProvider : IConverterProvider를 src/Everything2Everything.Core/Converters/에 추가하고 ProviderRegistry에 등록한다(기존 7개 Provider와 동일). 구체 설계:\n\n1) Capability: Status=ProviderStatus.RequiresExternal, ExternalDependencies에 new ExternalDependency(Name:\"FFmpeg\", Description:\"영상/오디오 트랜스코딩 엔진(LGPL 빌드)\", DownloadUrl:\"https://github.com/BtbN/FFmpeg-Builds/releases\"). SupportedConversions는 ProviderCapability.PairsFromMatrix로 영상(mp4/mkv/webm/mov/avi/gif)·오디오(mp3/aac/m4a/opus/ogg/flac/wav) 입출력 N×M 구성. 단 코덱 호환 안 되는 쌍(예: → flac은 오디오 전용)은 매트릭스 후 필터링하거나 PairsFromMatrix 대신 명시적 ConversionPair 리스트로 정밀 제어 권장.\n\n2) 바이너리 탐지: ExternalToolDetector에 TryFindFfmpeg(out string ffmpegPath) 추가 — (a) %LOCALAPPDATA%\\Everything2Everything\\ffmpeg\\ffmpeg.exe, (b) 시스템 PATH(where ffmpeg), 순으로 탐지. CheckAvailabilityAsync에서 못 찾으면 NotReady 반환하되, 선택적으로 FFMpegDownloader.DownloadFFMpegSuite(new FFOptions{ BinaryFolder=<앱데이터경로> })로 자동 조달 후 GlobalFFOptions.Configure로 경로 고정. (FFMpegDownloader 기본 소스는 ffbinaries=gyan GPL 빌드일 수 있으니, 상업 배포에선 BtbN lgpl-shared zip을 직접 받는 커스텀 다운로더를 쓰거나 동봉 권장 — 라이선스 검증 필수.)\n\n3) ConvertAsync 본문(FFMpegCore 사용):\n var media = await FFProbe.AnalyseAsync(sourcePath, cancellationToken: ct); // duration 확보\n await FFMpegArguments\n .FromFileInput(sourcePath)\n .OutputToFile(outputPath, overwrite:true, opt => opt\n .WithHardwareAcceleration(HardwareAccelerationDevice.Auto) // NVENC/QSV 자동\n .WithVideoCodec(\"h264_nvenc\") // 또는 av1/libaom, vp9, opus 등 outExt별 분기\n .WithAudioCodec(\"aac\"))\n .NotifyOnProgress(p => progress?.Report(p/100.0), media.Duration) // IProgress<double> 직결\n .CancellableThrough(ct) // 기존 ct 직결\n .ProcessAsynchronously();\n 진행률 NotifyOnProgress(Action<double>, TimeSpan)의 0~100을 /100.0해 기존 IProgress<double> 계약(0~1)에 맞춘다. 예외는 기존 Provider처럼 try/catch로 ConvertResult.Fail, OperationCanceledException은 rethrow.\n\n4) HW 가속 폴백: CheckAvailabilityAsync 또는 첫 변환 시 ffmpeg -encoders로 h264_nvenc/h264_qsv/h264_amf 가용성을 탐지해 ConvertOptions에 저장하고, 없으면 SW 인코더로 폴백하되 H.264/H.265 SW는 GPL이라 LGPL 빌드엔 없음 → 폴백을 AV1/VP9(libaom/libvpx, LGPL) 또는 mpeg4로 잡거나 사용자에게 HW 미지원 안내. ConvertOptions에 코덱/품질(CRF) 옵션 필드 추가 고려.\n\n5) 라이선스 고지: 본 프로젝트 어딘가(About/설정)에 'This software uses libraries from the FFmpeg project under the LGPLv2.1' 문구 + FFmpeg 소스 다운로드 링크 추가(LGPL 의무). ExternalDependency.DownloadUrl을 통해 UI에서 안내 가능.\n\n6) csproj: Everything2Everything.Core.csproj ItemGroup에 <PackageReference Include=\"FFMpegCore\" Version=\"5.4.0\" /> 추가. 자동 다운로더를 쓸 경우 FFMpegCore에 내장된 FFMpegDownloader 사용(별도 패키지 불필요). 단일 EXE에 번들 시 ffmpeg.exe를 None/Content로 추가하고 single-file에서 SelfExtract 메타데이터로 제외/비압축 처리(시작 성능).

PDF 압축 및 PDF↔문서 양방향 변환 (Everything2Everything .NET 9 / WPF 통합 관점)

핵심 발견

  • 압축 엔진 라이선스가 핵심 갈림길이다. Ghostscript(-dPDFSETTINGS /screen·/ebook·/printer·/prepress)는 압축 품질이 가장 우수하지만 AGPL v3 듀얼 라이선스다. 포터블 EXE에 gs 바이너리를 동봉/배포하면 AGPL 전염 의무(소스 공개)가 발생하므로, 상업적 사용을 원하면 Artifex 상업 라이선스 구매가 필요하다. MuPDF/mutool clean도 동일하게 AGPL이라 같은 문제가 있다.
  • qpdf(Apache 2.0, 최신 12.4.0 / 2026-04, 매우 활발)가 라이선스 안전성 측면에서 1순위 무료 옵션이다. object stream 압축(--object-streams=generate), 스트림 재압축, linearize(웹 최적화), 암호화/복호화/비밀번호 변경, 페이지 분할·병합을 모두 CLI로 제공한다. 단, qpdf는 이미지 다운샘플링(리샘플링)은 하지 않는다 — 구조 최적화/무손실 위주라 압축률이 Ghostscript보다 낮다.
  • 최대 압축률(이미지 다운샘플링)은 라이선스 안전한 무료 도구만으로는 약하다. qpdf로 구조 최적화 + 프로젝트가 이미 보유한 ImageMagick/PDFium으로 이미지 페이지를 재인코딩하는 하이브리드가 AGPL을 피하면서 실용적 압축을 내는 최선의 무료 경로다.
  • PDF→DOCX 레이아웃 보존은 순수 .NET 라이브러리로는 사실상 불가능하다. PdfPig(Apache 2.0, v0.1.14 / 2026-03, 활발)는 텍스트·글자 위치(page.Letters)·단어(GetWords)·이미지 추출까지 가능하지만 DOCX 재구성(레이아웃 엔진)은 설계 범위 밖이다. Docnet.Core(MIT, PDFium 래퍼)도 렌더/텍스트 추출 전용이다.
  • PDF→DOCX/편집가능 변환의 현실적 최선은 이미 통합된 LibreOffice headless(soffice --convert-to docx)다. 무료(MPL/LGPL)이고 단락·표를 어느 정도 복원하지만, 스캔/복잡 레이아웃 PDF는 충실도가 낮다. 고충실도 상업 변환이 필요하면 Nutrient(PSPDFKit)/IronPDF 등 유료 SDK가 있으나 라이선스 비용이 든다.
  • PDF→텍스트/HTML은 순수 .NET로 충분하다. PdfPig(텍스트, 무료)와 PDFium(Docnet) 또는 mutool로 텍스트/구조 추출이 가능하다. PDF→HTML 레이아웃 보존은 LibreOffice 또는 pdf2htmlEX(외부)이 현실적이다.
  • PDF/A 변환은 Ghostscript(-dPDFA, AGPL) 또는 LibreOffice PDF export(SelectPdfVersion=1 → PDF/A-1)로 가능하며, 검증은 veraPDF(반사실적 표준 검증기, 무료)로 한다. 라이선스 안전을 원하면 LibreOffice 경로 + veraPDF 검증 조합이 적합하다.
  • 암호화/복호화/병합/분할은 qpdf(Apache 2.0) 단독으로 전부 커버 가능하며 가장 가벼운 단일 실행 파일이다. 순수 .NET 대안으로 PDFsharp 6.2.x(MIT, AES-128/256, 병합·분할·암호화 지원, .NET 9 호환)가 외부 의존성 없이 In-Process로 동작해 포터블 EXE에 가장 잘 맞는다.

권장 접근 (.NET 9 / 단일 EXE)

이 프로젝트(단일 포터블 EXE / 상업적 사용 가능 라이선스 우선)에는 'AGPL 회피 + 가능한 한 in-process .NET' 원칙을 권장한다. 1) PDF 압축: 1차로 PDFsharp(MIT) 또는 qpdf(Apache 2.0)로 구조 최적화(object stream 압축, linearize, 중복 객체 제거)를 in-process/경량 CLI로 수행한다. 이미지 다운샘플링이 필요한 '강한 압축' 모드는 이미 보유한 PDFium(Docnet/PDFtoImage)로 페이지를 렌더 후 ImageMagick로 JPEG/품질 조절 재인코딩하여 새 PDF를 만드는 하이브리드를 별도 옵션으로 제공한다(텍스트 선택성은 잃지만 라이선스 안전). Ghostscript는 '고급 압축' 옵션으로만 노출하되, 번들하지 말고 사용자가 별도 설치한 gs를 ExternalDependency로 감지해 쓰는 방식(LibreOffice/H2Orestart와 동일 패턴)으로 AGPL 배포 의무를 회피한다. 2) PDF→DOCX/HTML(역변환): 기존 DocumentProvider의 LibreOffice 경로를 그대로 확장해 입력 매트릭스에 .pdf를 추가한다(soffice --convert-to docx/html/txt). 순수 .NET PdfPig는 'PDF→txt' 같은 빠른 무외부 경로와 텍스트 추출 폴백으로 사용한다. 3) PDF/A·암호화·병합·분할: PDFsharp(MIT, in-process)를 기본 엔진으로, qpdf(Apache 2.0)를 무거운 작업(linearize/복잡 암호)의 외부 폴백으로 둔다. PDF/A 검증은 선택적 veraPDF 연동. 전체적으로 '무료 기본(PDFsharp/qpdf/PDFium/LibreOffice) + 선택적 고급(Ghostscript/유료 SDK, 사용자 설치 감지)' 2계층 전략이 라이선스·포터블성·품질의 균형점이다.

라이브러리 · 도구

이름용도라이선스성숙도
qpdf (CLI + libqpdf)PDF 구조 최적화/object stream 압축, linearize(웹 최적화), 암호화·복호화·비밀번호 변경, 페이지 분할·병합. 이미지 다운샘플링은 안 함(무손실/구조 위주). Apache License 2.0 (v7+ 재라이선스)production / 매우 활발 (v12.4.0, 2026-04; v12.3.2, 2026-01). Windows MSVC 32/64bit 빌드 제공
Ghostscript (gswin64c) + Ghostscript.NET 래퍼-dPDFSETTINGS(/screen·/ebook·/printer·/prepress) 프리셋 + -dDownsampleColorImages/-dColorImageResolution 등 이미지 다운샘플링으로 최고 압축률. -dPDFA로 PDF/A 변환. AGPL v3 (또는 Artifex 상업 라이선스). Ghostscript.NET 래퍼는 MIT지만 네이티브 gs는 AGPLproduction / 활발 (10.x). Ghostscript.NET 래퍼 NuGet은 1.3.3로 다소 정체
MuPDF / mutool (clean)mutool clean으로 폰트/이미지 스트림 압축·garbage collect. 렌더링/멀티포맷(EPUB/XPS) 강점. AGPL v3 (또는 Artifex 상업 라이선스)production / 활발 (1.27.x)
PdfPig (UglyToad.PdfPig)순수 .NET PDF 읽기/텍스트·글자 위치(Letters)·단어(GetWords/NearestNeighbour)·이미지(GetImages) 추출, 기본 PDF 생성·병합. PDF→txt/구조 분석에 적합. Apache License 2.0production / 활발 (v0.1.14, 2026-03; PDFBox 포팅). netstandard2.0
Docnet.CorePDFium(Apache 2.0) .NET Standard 래퍼. 페이지 렌더(비트맵), 텍스트/메타데이터 추출. MIT (네이티브 PDFium은 Apache 2.0)active / 안정 (유지보수 보통)
PDFsharp 6.x순수 .NET PDF 생성·수정·병합·분할, AES-128/256 암호화·복호화, PDF/A·PDF/UA 일부 지원. MIT (상업적 자유, 저작권 고지 유지 시)production / 활발 (6.2.4, .NET 9/10 호환)
DocumentFormat.OpenXml + OpenXmlPowerTools(Clippit)DOCX in-process 생성/편집. PdfPig 추출 텍스트로 간단한 DOCX 조립 시 사용. MIT (둘 다)production / 활발 (OpenXml 3.5.1; PowerTools 4.5.x / Clippit 2026 유지)
LibreOffice (soffice headless)PDF→DOCX/HTML/TXT 역변환, PDF/A export, 문서 상호변환. 이미 통합됨. MPL 2.0 / LGPL 3.0 (상업 사용 가능)production / 매우 활발
veraPDFPDF/A·PDF/UA 적합성 검증(생성 아님). 변환 후 검증 단계. 이중(GPLv3+ / MPLv2+), 둘 다 무료 사용 가능production / 활발 (PDF Association·OPF 유지, ISO 참조 검증기)
IronPDF / Nutrient(PSPDFKit) .NET SDK고충실도 PDF→Word/Excel/PPT 변환, HTML↔PDF, 압축 등 올인원 상업 SDK. 상업(유료, 무료 아님)production / 활발

통합 노트

기존 추상화에 자연스럽게 끼워넣을 수 있다. 핵심은 IConverterProvider / ProviderCapability / ProviderRegistry(N×M 매트릭스)와 ExternalDependency 감지 패턴이 이미 LibreOffice·H2Orestart에서 검증되어 있다는 점이다. 1) PDF 역변환(DOCX/HTML/TXT): 가장 저비용 통합. DocumentProvider.cs의 Inputs 배열에 \".pdf\"를 추가하고 RouteAsync에 PDF 분기를 넣으면 된다. .pdf→{docx,html,txt}는 그대로 SofficeConvertAsync 재사용(soffice는 PDF 입력을 Draw로 열어 변환). .pdf→txt 무외부 폴백은 PdfPig로 page.Text를 모아 쓰는 별도 경로를 추가하면 LibreOffice 없이도 동작. .pdf→md는 기존 패턴대로 pdf→html(soffice)→ReverseMarkdown 체인으로 처리. RoadmapNote/ExternalDependencies는 기존 LibreOffice 의존성 항목 그대로 재사용. 2) PDF 압축/유틸리티(압축·암호화·병합·분할·PDF/A): 같은 \".pdf\"→\".pdf\" 변환은 현 ProviderRegistry가 (input==output)을 ConversionEngine 단계에서 걸러낼 가능성이 높으므로, '동일 확장자 변환'을 옵션 파라미터로 구분하는 새 PdfToolProvider(예: Id \"pdf-tools\")를 신설하는 편이 깔끔하다. ConvertOptions에 PdfCompress(레벨: Light=PDFsharp/qpdf 구조 최적화, Strong=PDFium 렌더+ImageMagick 재인코딩, Max=Ghostscript /screen) 같은 옵션 그룹을 추가한다(기존 Jpeg/Webp/Avif/Tiff/PdfRender 옵션 그룹과 동일한 record 스타일). 압축 강도에 따라 내부적으로 (a) PDFsharp in-process, (b) PDFium+ImageMagick 하이브리드(PdfProvider의 렌더 로직과 ApplyEncoding 재사용 가능), (c) gs CLI 폴백으로 디스패치. 3) 라이선스/배포 경계: PDFsharp·PdfPig·qpdf는 번들 가능(MIT/Apache). Ghostscript·MuPDF·veraPDF·LibreOffice는 절대 EXE에 정적 링크/동봉하지 말고, CheckAvailabilityAsync에서 ExternalToolDetector로 사용자 설치본을 감지해 ProviderAvailability.NotReady(미설치 시) 또는 Ready로 분기한다(현 DocumentProvider.CheckAvailabilityAsync와 동일 패턴). 이렇게 하면 고급 압축/PDF-A는 '사용자가 직접 설치한 도구로만' 동작하여 AGPL 배포 전염을 구조적으로 회피한다. ExternalDependency.DownloadUrl에 gs/veraPDF 다운로드 링크를 넣어 안내. 4) qpdf 통합 방식: SofficeConvertAsync와 동일한 ProcessStartInfo/ArgumentList 패턴으로 QpdfRunner 헬퍼를 하나 만들어 압축(--object-streams=generate --compress-streams=y --recompress-flate), 암호화(--encrypt), 복호화(--decrypt), 분할(--split-pages), 병합(--pages)을 공통 호출. libqpdf P/Invoke는 복잡도가 높아 CLI 호출이 통합 비용 대비 합리적.

HWP/HWPX ↔ DOCX/PDF/HTML 양방향 변환 — Everything2Everything (.NET 9 / WPF) 통합 리서치

핵심 발견

  • **정방향(HWP/HWPX → PDF/이미지)은 이미 동작 중이며 최선의 경로다.** 프로젝트는 HwpxProvider.cs에서 LibreOffice headless(`soffice --convert-to pdf`) + H2Orestart 확장으로 PDF 변환 후 PdfProvider로 위임한다. H2Orestart는 2026년에도 활발히 유지보수 중(v0.7.12, 2026-05-10 릴리스). 이 구조는 옳다.
  • **H2Orestart는 GPLv3 + Java 의존이다.** 라이선스가 GPLv3(LGPL 아님)이므로 EXE에 정적/동적 번들하면 전염성(copyleft) 문제가 생긴다. 단, 현재처럼 별도 .oxt 확장으로 사용자가 LibreOffice에 설치 → 별도 프로세스(soffice.exe)를 외부 호출하는 방식이면 E2E 본체 코드와 라이선스가 분리되어 상업 배포에 안전하다. 추가로 H2Orestart는 내부적으로 JRE/JDK가 필요하다(LibreOffice의 Java 통합 활성화 필수). LibreOffice 빌드에 따라 별도 JRE 설치/설정이 필요할 수 있어 '단일 포터블 EXE' 자족성에 마이너스 요인.
  • **H2Orestart는 import(읽기) 전용이다 — 저장은 ODT로만 가능, HWP/HWPX 쓰기 불가.** 따라서 LibreOffice 경로로는 역방향(DOCX→HWP) 출력이 불가능하다. LibreOffice에 HWP export 필터가 없다.
  • **역방향 HWP/HWPX '쓰기'는 hwplib(HWP) / hwpxlib(HWPX)만이 현실적 오픈소스 해법이다.** 둘 다 neolord0 작성, Apache-2.0(상업 친화적), 2026년까지 활발(hwplib v1.1.5+ 2026-02-04, hwpxlib v1.0.8 2025-11-14). hwplib은 BlankFileMaker/HWPWriter로 빈 HWP 생성·텍스트·표·이미지 삽입까지 가능. **그러나 둘 다 Java 라이브러리이며 '포맷 변환' 기능이 없다** — DOCX를 파싱해서 hwplib 객체 모델로 매핑하는 변환 로직을 직접 구현해야 한다. 고품질 레이아웃 보존 역변환은 사실상 새 변환 엔진을 작성하는 수준이라 비용이 매우 크다.
  • **pyhwp/hwp5(hwp5html)는 AGPLv3 + 반쯤 정체 상태다.** 마지막 안정 릴리스 0.1b15(2020-05-30). HWP5→HTML/ODT/txt 추출은 되지만 HWPX 미지원이고, AGPLv3는 SaaS·배포에 전염성이 강해 상업 배포기에 부적합. 이미 LibreOffice 경로가 있으므로 채택 이점 없음.
  • **hwp.js는 사실상 abandoned다(Apache-2.0, v0.0.3 2020-10, 2020년 유지보수 중단 공지).** 브라우저 HWP 뷰어/파서로 WebView2와 결합 가능성은 있으나, 미완성·구버전이라 신뢰성 낮음. 동일 저자(hahnlee)의 Rust 계열(hwp-rs)과 신생 Rust 생태계(openhwp, HwpForge, hwpers, unhwp)가 더 활발하지만 .NET 직접 연동은 FFI 작업이 추가로 필요.
  • **한컴 공식 경로(HwpAutomation COM / 한글 SDK / Docs Converter)는 품질이 가장 높지만 상업 라이선스 유료다.** HwpCtrl ActiveX/COM은 개인·비상업은 무료이나 '판매되는 솔루션'에 쓰려면 한컴 승인 + 별도 라이선스 필요(contact_sdk@hancom.com). 한글 SDK는 한글 프로그램 설치 없이 HWP/HWPX↔HTML/PDF/ODF 변환 및 약 1,000개 기능 제공, .NET에서 호출 가능하나 유료. 역방향(→HWP)을 네이티브 품질로 보장하는 유일한 길이지만 비용·배포(런타임 동봉) 제약이 큼.
  • **LibreOffice headless 한글 변환 안정성/폰트 이슈가 존재한다.** Old Hangul(옛한글)·제주어 음절, Noto Sans 일부 글리프가 PDF에서 깨지는 보고가 있고, 시스템에 한글 폰트(맑은 고딕/함초롬바탕 등)가 없으면 폰트 치환으로 레이아웃이 틀어진다. 레거시 HWP는 `--infilter="Hwp2002_File"` 지정이 도움이 됨. 복잡한 표·다단·머리말은 레이아웃 손실 가능.

권장 접근 (.NET 9 / 단일 EXE)

단일 포터블 EXE / 상업 라이선스 선호라는 제약을 고려하면 **계층적 전략**이 최적이다. 1) 정방향(HWP·HWPX → PDF/PNG/JPG 등): **현재 HwpxProvider의 LibreOffice + H2Orestart 경로를 유지·강화**한다. 이미 구현되어 있고 H2Orestart가 2026년에도 활발하다. 강화 포인트: (a) `--infilter=\"Hwp2002_File\"`를 .hwp에 한해 추가 지정해 import 필터 명시, (b) 한글 폰트(함초롬·맑은 고딕) 번들 또는 폰트 누락 감지 경고, (c) JRE 미설치 시 명확한 안내. H2Orestart는 GPLv3이지만 '사용자가 LibreOffice에 설치한 확장을 외부 프로세스로 호출'하는 분리 모델이라 E2E 본체 라이선스에 전염되지 않는다. 2) 정방향 HWPX → DOCX/HTML/TXT '편집 가능 포맷' 출력: PDF 외에 DOCX/ODT/HTML 출력 수요가 있으면, **LibreOffice의 `--convert-to docx`/`html`/`txt`** 를 동일 파이프라인에서 노출하면 된다(soffice가 ODT 경유로 DOCX/HTML export 지원). 이게 hwp5html/pyhwp(AGPL) 채택보다 라이선스·유지보수 면에서 우수하다. HwpxProvider의 HwpOutputs 배열에 .docx/.html/.txt/.odt를 추가하고 변환 분기만 PDF 대신 해당 포맷으로 바꾸면 즉시 매트릭스가 확장된다. 3) 역방향(DOCX/PDF/HTML → HWP·HWPX): **현실적으로 고품질 무료 OSS 경로는 없다.** 단기적으로는 'Coming Soon' 또는 미지원으로 두고, 정말 필요하면 두 가지 옵션 — (a) **저품질 수용형**: HTML/DOCX 텍스트·표를 추출해 hwpxlib(Apache-2.0)로 최소 구조의 HWPX를 생성(레이아웃 보존 낮음, Java 브리지 필요), (b) **고품질 유료형**: 한컴 한글 SDK/Docs Converter 상업 라이선스로 별도 Provider 구성. 무료 단일 EXE 원칙을 우선한다면 역방향은 '로드맵'으로 남기고, HWPX 출력만 hwpxlib 기반 베스트에포트로 제공하는 것을 권한다. 요약: 정방향은 LibreOffice 단일 엔진으로 PDF·이미지·DOCX·HTML까지 모두 커버(추가 의존성 0). 역방향은 무료로는 베스트에포트 HWPX만, 네이티브 품질은 유료 한컴 SDK로 분리.

라이브러리 · 도구

이름용도라이선스성숙도
H2Orestart (ebandal)LibreOffice 확장. HWP/HWPX를 LibreOffice에서 import(읽기) → ODT/PDF/DOCX/HTML로 headless 변환. 정방향 변환의 핵심 엔진. GPLv3active (v0.7.12, 2026-05-10)
hwplib (neolord0 / kr.dogfoot)HWP 5.0 바이너리 읽기 AND 쓰기(BlankFileMaker로 새 파일 생성, HWPWriter로 저장). 역방향(→HWP) 출력의 유일한 무료 경로. Apache-2.0active (v1.1.5+, 2026-02-04)
hwpxlib (neolord0)HWPX(OWPML, ZIP+XML) 읽기/쓰기. HWPX 직접 생성·수정. 역방향 HWPX 출력 후보. Apache-2.0active (v1.0.8, 2025-11-14)
pyhwp / hwp5 (hwp5html, mete0r)HWP5 파서. HWP5 → HTML/ODT/txt 추출. AGPLv3+semi-abandoned (마지막 안정 0.1b15, 2020-05-30)
hwp.js (hahnlee)웹 기술 기반 HWP 뷰어/파서(브라우저 렌더링). Apache-2.0abandoned (v0.0.3, 2020-10, 2020년 유지보수 중단 공지)
Rust 생태계 (hwp-rs, openhwp, HwpForge, hwpers, unhwp)HWP/HWPX 파싱·렌더·Markdown 추출. openhwp는 읽기/쓰기 지향. 대부분 MIT/Apache-2.0 (크레이트별 상이, 확인 필요)active/beta (2025-2026 신생, 성숙도 편차 큼)
한컴 한글 SDK / HwpAutomation(COM) / Docs Converter한컴 네이티브 변환. HWP/HWPX ↔ HTML/PDF/ODF/DOCX, 약 1,000개 한글 기능. 역방향(→HWP) 네이티브 품질 보장. 상업(유료, 별도 라이선스). ActiveX/COM은 개인·비상업만 무료production (한컴 공식)

통합 노트

**Provider/Registry 추상화에 끼우는 구체안 (D:\\workspace\\Everything2Everthing\\src\\Everything2Everything.Core)** 현재 HwpxProvider.cs는 IConverterProvider를 구현하고, Capability.SupportedConversions = PairsFromMatrix(HwpInputs, HwpOutputs)로 (입력×출력) 쌍을 선언, ProviderRegistry가 (Input,Output)→Provider 딕셔너리로 라우팅한다. DocxProvider가 동일 패턴(LibreOffice/Word로 PDF→PdfProvider 위임)이라 이를 그대로 따른다. 1) **정방향 출력 포맷 확장 (가장 비용 낮고 효과 큼)**: HwpxProvider의 `HwpOutputs` 배열에 `.docx`, `.html`, `.odt`, `.txt`를 추가한다. ConvertAsync의 분기에서 outExt가 이미지/PDF가 아니면 ConvertWithLibreOfficeAsync의 `--convert-to pdf`를 해당 필터(`docx:\"MS Word 2007 XML\"`, `html:HTML (StarWriter)`, `txt:Text` 등)로 파라미터화한다. 현재 메서드는 pdf 하드코딩이므로 target 포맷·확장자를 인자로 받도록 일반화하면 된다. .hwp 입력 시 `--infilter=\"Hwp2002_File\"`를 ArgumentList에 조건부 추가하면 import 안정성이 오른다. 이렇게 하면 HWP/HWPX → DOCX/HTML/TXT/ODT/PDF/이미지 매트릭스가 한 Provider·한 외부 의존(LibreOffice+H2Orestart)으로 완성된다. 2) **availability 메시지 보강**: CheckAvailabilityAsync에 JRE 미설치 시 안내를 추가(H2Orestart는 LibreOffice의 Java 통합이 꺼져 있으면 동작 안 함). ExternalToolDetector에 한글 폰트(함초롬/맑은 고딕) 존재 여부 체크를 추가해 폰트 누락 시 ProviderAvailability에 경고 Reason을 실어주면 레이아웃 깨짐 사고를 예방. 3) **역방향 Provider 신설(선택)**: 별도 `HwpReverseProvider`(또는 HwpxProvider에 입력 .docx/.html/.pdf → 출력 .hwpx 쌍 추가)를 만들되, 무료 경로는 hwpxlib 기반 베스트에포트로 한정. .NET↔Java 브리지가 필요하므로 (a) IKVM.NET으로 hwpxlib JAR을 .NET 어셈블리화, 또는 (b) 번들한 JRE로 hwpxlib 래퍼 JAR을 자식 프로세스 실행(현재 LibreOffice를 외부 프로세스로 부르는 패턴과 동일해 일관적). 단일 EXE 원칙상 (b)가 기존 외부-프로세스 모델과 잘 맞는다. Capability.Status는 ComingSoon 또는 RequiresExternal로 두고, RoadmapNote에 '레이아웃 보존 제한적, 고품질은 한컴 SDK 필요' 명시. 4) **유료 고품질 역변환은 별도 옵셔널 Provider**: 한컴 SDK가 설치·라이선스된 환경에서만 활성화되는 `HancomSdkProvider`를 COM(dynamic)로 구현(DocxProvider의 ConvertWithWordCom이 Type.GetTypeFromProgID로 Word COM을 dynamic 호출하는 패턴과 동일). CheckAvailabilityAsync에서 ProgID/SDK DLL 존재로 가용성 판단, 없으면 NotReady로 빠지므로 무료 배포본에는 영향 없음. 라이선스 격리 원칙: GPLv3(H2Orestart)·Java 라이브러리(hwplib/hwpxlib)·한컴 SDK 모두 '외부 프로세스 또는 사용자 설치 확장'으로 분리 호출하여 E2E 본체(상업 배포)와 라이선스 경계를 유지한다. 본체에 GPL/AGPL 코드를 링크하지 않는다.

.NET 9 확장 가능 변환기 플러그인 아키텍처 — Everything2Everything용 best practice 리서치

핵심 발견

  • 기존 추상화는 이미 잘 설계된 정적 매트릭스다. IConverterProvider(Capability + CheckAvailabilityAsync + ConvertAsync), ProviderRegistry((input,output) 쌍 사전), ProviderCapability(ConversionPair 리스트)로 구성. 단, 현재 ProviderRegistry는 생성자에서 IEnumerable<IConverterProvider>를 받아 컴파일 타임에 고정된다 — 동적 등록/언로딩 진입점이 없다. 확장성의 첫 단계는 '런타임에 Provider를 추가하는 RegisterDynamic / Rebuild' 메서드 도입이다.
  • Microsoft 공식 .NET 플러그인 튜토리얼(2026-02 갱신)의 핵심 패턴: (1) 공유 계약 어셈블리(PluginBase)를 별도 프로젝트로 분리, (2) 플러그인 프로젝트는 계약을 <Private>false</Private> + <ExcludeAssets>runtime</ExcludeAssets>로 참조해 계약 DLL 중복 로드를 방지(이게 빠지면 같은 인터페이스가 서로 다른 타입으로 인식되어 캐스팅 실패), (3) 플러그인 csproj에 <EnableDynamicLoading>true</EnableDynamicLoading>를 넣어 의존성을 출력으로 복사, (4) 플러그인당 별도 AssemblyLoadContext(ALC) + AssemblyDependencyResolver로 의존성 충돌 격리. Load 오버라이드에서 계약 어셈블리는 null 반환해 default ALC로 fall back시켜야 타입 동일성이 유지된다.
  • 보안/신뢰 경계에 대한 Microsoft의 명시적 경고: '신뢰할 수 없는 코드는 신뢰된 .NET 프로세스에 안전하게 로드할 수 없다. 보안/안정성 경계가 필요하면 OS 또는 가상화 플랫폼이 제공하는 기술을 사용하라.' 즉 in-process ALC는 '버전 격리/핫리로드'용이지 '샌드박스'가 아니다. 진짜 격리(서드파티 untrusted 플러그인, 네이티브 크래시 차단)는 out-of-process 호스트(별도 .exe + IPC) 또는 Windows AppContainer/Job Object로만 달성된다.
  • 언로딩(hot-reload)은 협조적(cooperative)이며 footgun이 많다. collectible ALC를 써도 (a) 정적 캐시(직렬화기/DI 컨테이너가 플러그인 타입 캐시), (b) 해지 안 한 이벤트 핸들러, (c) 살아있는 Timer/Task/Thread, (d) 플러그인 타입이 host-scope 인프라로 누출되면 절대 언로드되지 않는다. 검증은 WeakReference<ALC> + 반복 GC로만 가능. 네이티브 라이브러리(ImageMagick/PDFium/LibreOffice)는 ALC 경계를 무시하므로 in-process 언로딩 대상에서 제외해야 한다.
  • MEF2(System.Composition)는 죽지 않았고 활발히 유지보수 중이다 — 최신 10.0.7(2025), .NET 9/10과 호환(.NET Core 2.0/Standard 2.0 타겟). 다만 이 프로젝트에는 권장하지 않는다: MEF은 런타임 리플렉션 기반 attribute 스캔이라 (a) 단일 포터블 EXE/AOT 친화성이 낮고, (b) 7개뿐인 1급 Provider에는 과도하다. 동적 서드파티 플러그인을 정말 열 때만 가치가 있다.
  • Source generator 방식이 이 프로젝트에 가장 적합한 '자동 등록' 수단이다. AutoRegisterInject(v1.4.1, MIT, netstandard2.0, .NET 9 호환)가 [RegisterScoped]/[RegisterSingleton] 등 attribute로 DI 등록 코드를 컴파일 타임에 생성 — 리플렉션 0, AOT 친화. 1급(in-box) Provider들은 source generator로 자동 등록하고, 동적 외부 플러그인만 ALC로 로드하는 하이브리드가 이상적.
  • 외부 도구 어댑터 패턴은 이미 코드에 존재한다(DocumentProvider.SofficeConvertAsync가 LibreOffice를 ProcessStartInfo로 감쌈). 이를 일반화한 'ExternalToolProvider' 베이스 클래스 + JSON manifest로 FFmpeg/Ghostscript/Pandoc을 균일 인터페이스로 흡수할 수 있다. CliWrap(v3.10.1, MIT, 2026-03 갱신)이 ProcessStartInfo 보일러플레이트(스트림 리다이렉트/취소/exit code/진행률)를 대체할 fluent 래퍼로 강력 추천.
  • 라이선스 함정 2가지가 단일 포터블 EXE 배포에 치명적이다. (1) Ghostscript: AGPL/상업 듀얼 라이선스 — 닫힌 소스 포터블 EXE에 번들하려면 Artifex 상업 라이선스 필요. PDF 처리는 이미 가진 PDFium으로 대체하는 게 안전. (2) FFmpeg: libx264/libx265 등 인기 코덱은 GPL이라 번들 시 앱 전체가 GPL 전염. 반드시 --enable-gpl 없이 빌드한 LGPL 빌드를 '동적 링크'(별도 exe 호출/별도 DLL)로 사용하고 소스 오퍼/저작권 고지를 포함해야 함. Pandoc도 GPL이라 같은 '동적 = 별도 프로세스 호출' 원칙 적용. LibreOffice(MPL-2.0)와 CliWrap/AutoRegisterInject(MIT)는 번들 안전.

권장 접근 (.NET 9 / 단일 EXE)

단일 포터블 EXE/WPF/Windows 11 제약에서는 '3계층 하이브리드'가 최적이다. (계층 1 — In-box Provider, 지금처럼) 7개 1급 Provider는 컴파일 타임에 고정. 단 등록 보일러플레이트를 줄이려면 AutoRegisterInject source generator를 도입해 [RegisterE2EProvider] attribute만 붙이면 자동 등록되게 한다. 리플렉션 없고 AOT/트리밍 친화라 단일 EXE에 이상적. (계층 2 — 외부 도구 어댑터 Provider) 이미 있는 SofficeConvertAsync 패턴을 'ExternalToolProvider' 추상 베이스로 일반화한다. 각 외부 도구(LibreOffice/FFmpeg/Ghostscript/Pandoc)는 코드가 아니라 JSON manifest(tool id, 실행 파일 탐지 경로, 입력/출력 확장자 매트릭스, argument 템플릿, 성공 판정 규칙)로 선언하고, 런타임에 manifest를 읽어 ProviderRegistry에 합성한다. 프로세스 실행은 CliWrap으로 통일(취소/진행률/exit code 처리 일원화). 이렇게 하면 새 도구 추가가 '코드 빌드 없이 manifest + 탐지기 추가'로 끝난다. 외부 프로세스 호출 자체가 천연 격리 경계라서 도구가 크래시해도 앱은 살아있다(가장 가성비 좋은 안정성/보안 경계). (계층 3 — 진짜 동적 플러그인, 필요할 때만) 서드파티가 .NET DLL Provider를 끼우는 시나리오가 생기면 그때 collectible ALC + AssemblyDependencyResolver를 도입한다. 단 이건 untrusted 격리가 아님을 명심하고, 신뢰할 수 없는 플러그인은 out-of-process 워커(별도 exe)로 돌린다. ImageMagick/PDFium/WebView2 같은 네이티브 의존 Provider는 절대 언로드 대상으로 만들지 않는다(네이티브가 ALC 경계를 무시). 결론: 지금 당장 필요한 건 계층 1의 source generator 자동 등록과 계층 2의 manifest 기반 외부 도구 어댑터다. ALC 동적 로딩은 '서드파티 플러그인 마켓'을 실제로 열 때까지 미루는 게 복잡도 대비 합리적이다.

라이브러리 · 도구

이름용도라이선스성숙도
System.Runtime.Loader.AssemblyLoadContext + AssemblyDependencyResolver (BCL 내장)플러그인 DLL을 격리된 컨텍스트에 동적 로드/언로드, 의존성 충돌 해결. .NET 공식 플러그인 메커니즘 MIT (.NET 런타임)production (BCL 내장, .NET Core 3.0~.NET 9/10 안정)
AutoRegisterInjectattribute 기반으로 IConverterProvider 구현체를 컴파일 타임에 DI 자동 등록 (리플렉션/스캔 제거) MITactive (v1.4.1, netstandard2.0, .NET 9 호환)
System.Composition (MEF2)attribute 기반 런타임 플러그인 발견/합성 (Export/Import) MITactive (최신 10.0.7, 2025, .NET 9 호환)
CliWrap외부 CLI 도구(LibreOffice/FFmpeg/Ghostscript/Pandoc)를 fluent하게 실행 — 인자/스트림/취소/진행률/exit code 일원화 MITproduction (v3.10.1, 2026-03 갱신, 활발)
LibreOffice (soffice)문서(DOCX/HTML/TXT/HWP) 변환 외부 엔진 (이미 사용 중) MPL-2.0production
FFmpeg오디오/비디오 변환 어댑터 Provider 후보 LGPL-2.1+ (코어) / 일부 코덱 GPL / nonfreeproduction
GhostscriptPDF/PostScript 변환 어댑터 후보 AGPL-3.0 / 상업 듀얼 (Artifex)production
Pandoc마크다운/문서 포맷 광범위 변환 어댑터 후보 GPL-2.0+production
System.Reflection.Metadata어셈블리를 실제 로드하지 않고 PE/메타데이터만 읽어 플러그인 manifest/attribute를 스캔 (reflection-free discovery) MIT (.NET 런타임)production

통합 노트

현재 추상화에 끼워넣는 구체 단계: (1) ProviderRegistry를 '닫힌 생성자'에서 '증분 등록 가능' 구조로 확장. 현재 생성자가 한 번에 _byPair 사전을 빌드하므로, 동일 인덱싱 로직을 private void Index(IConverterProvider) 로 빼고 public void Register(IConverterProvider)/RegisterRange/Rebuild를 추가한다. 이걸로 source generator 등록 + manifest 기반 어댑터 등록 둘 다 같은 진입점을 쓴다. (2) 계층1 자동등록: IConverterProvider 구현체(HeicProvider/PdfProvider 등)에 [RegisterE2EProvider] 같은 마커를 붙이고 AutoRegisterInject(또는 소형 자작 generator)로 'IEnumerable<IConverterProvider> GetBuiltInProviders()'를 컴파일 타임 생성. App 시작 시 registry.RegisterRange(GetBuiltInProviders()). (3) 계층2 어댑터: 새 추상 클래스 ExternalToolProvider : IConverterProvider 를 만든다. 이 클래스가 (a) ProviderCapability를 manifest의 입력×출력 매트릭스에서 ProviderCapability.PairsFromMatrix로 생성(기존 헬퍼 재사용), (b) CheckAvailabilityAsync는 manifest의 toolDetect 규칙(현 ExternalToolDetector 패턴 일반화)으로 실행 파일 탐지, (c) ConvertAsync는 manifest의 argument 템플릿({input}/{output}/{outdir}/{format} 토큰 치환)을 CliWrap으로 실행하고 결과 파일 존재로 성공 판정. 기존 ExternalDependency 레코드를 manifest의 dependency 섹션과 그대로 매핑. (4) Manifest 로더: tools/*.manifest.json 을 읽어 ExternalToolProvider 인스턴스들을 만들고 registry.RegisterRange. manifest 스키마는 기존 ProviderCapability/ConversionPair/ExternalDependency 모양을 그대로 직렬화한 형태로 잡으면 추상화 변경 최소. (5) 충돌 우선순위: _byPair.TryAdd는 first-wins라 in-box Provider가 동일 쌍을 가지면 manifest 어댑터보다 먼저 등록해 우선권을 준다(현재 동작 유지). 향후 우선순위 필드가 필요하면 ProviderCapability에 Priority(int) 추가 후 Index에서 비교. (6) 안정성: 외부 도구는 CliWrap의 ExecuteAsync에 CancellationToken과 타임아웃을 걸고, 크래시해도 ConvertResult.Fail로 흡수(이미 DocumentProvider가 try/catch로 처리하는 패턴 유지). 진짜 in-process .NET 플러그인(계층3)을 열 때만 collectible ALC를 도입하되, IConverterProvider 계약을 담은 별도 Everything2Everything.Abstractions 어셈블리를 만들고 플러그인은 그것을 <Private>false</Private>로 참조하게 해 타입 동일성을 보장한다.

범용 문서/아카이브/폰트/CAD 변환 커버리지 확장 + 변환기 UX 패턴 (Everything2Everything, .NET 9/WPF/Windows 11)

핵심 발견

  • Pandoc은 사실상의 'universal document 허브'다. pandoc 3.x 기준 43개 입력 / 57개 출력 포맷(마크다운 계열·HTML·LaTeX·DOCX·EPUB·RST·MediaWiki·Org·Textile·JATS 등)을 지원하며, 단일 도구로 N×M 마크업/문서 매트릭스를 한 번에 커버한다. .NET 통합은 SimonCropp의 PandocNet(MIT, v4.0.0 / 2026-04, CliWrap 기반 강타입 래퍼)이 가장 성숙하다. 단 pandoc.exe를 번들하지 않고 PATH 또는 명시 경로로 외부 설치를 요구한다 — 프로젝트가 LibreOffice를 외부 의존성으로 다루는 패턴과 정확히 동일.
  • 프로젝트는 이미 LibreOffice로 DOCX↔HTML↔TXT를 처리하므로, Pandoc은 LibreOffice가 약한 '마크업/경량 텍스트 포맷'(rst, org, latex, mediawiki, asciidoc, textile, ipynb, epub→md 등)을 채우는 보완재로 배치하는 것이 최적이다. 둘은 경쟁이 아니라 라우팅 분담(LibreOffice=오피스 바이너리, Pandoc=마크업/학술)이다.
  • CloudConvert가 '극한' 변환기의 레퍼런스 매트릭스: 212포맷 / 13카테고리(문서23·이미지42·비디오28·오디오21·스프레드시트8·슬라이드11·전자책22·아카이브39·벡터10·CAD3·폰트5·데이터·해시). Everything2Everything의 현재 커버리지(이미지·PDF·문서5종·HEIC·OCR)와 비교하면 아카이브·폰트·전자책·데이터·벡터·오디오/비디오가 미개척 영역.
  • '극한' UX를 만드는 4대 공통 패턴: (1) HandBrake/XnConvert식 배치 큐(queue) — 수백 파일을 한 번에 넣고 순차/병렬 처리 + per-row 진행률(프로젝트는 이미 큐 inline progress bar 보유), (2) 액션 체이닝(XnConvert: resize→watermark→convert를 한 파이프라인으로), (3) 워치/핫 폴더 자동화(폴더에 떨어뜨리면 자동 변환), (4) 명확한 드롭존 + 클릭 업로드 병행 + hover 시 시각 피드백.
  • 라이선스 함정 2건 확인: (a) SixLabors.Fonts는 3.0.0부터 'Split License'로 빌드 타임 라이선스 검증을 강제 — 연매출 1M USD 이상 + 클로즈드소스면 상업 라이선스 구매 필수. 폰트 변환에는 라이선스 비용 없는 LayoutFarm/Typography(MIT 계열) 또는 Aspose.Font(상용) 검토 권장. (b) Xabe.FFmpeg는 CC BY-NC-SA(비상업 전용)라 상업 배포 불가 — 오디오/비디오는 반드시 FFMpegCore(MIT) + FFmpeg 바이너리(LGPL 빌드)로 가야 한다.
  • CAD는 순수 .NET 솔루션이 아직 미성숙: ACadSharp(MIT, v3.6.x)는 DXF/DWG 읽기/쓰기가 가능하나 공식적으로 alpha(일부 엔티티 미구현). 실무 DWG↔DXF 변환은 ODA File Converter(무료, 비오픈소스, 재배포 제약)나 Aspose.CAD(상용)에 의존. LibreDWG는 GPLv3+라 클로즈드소스 EXE에 부적합. CAD는 우선순위 후순위 권장.
  • 아카이브는 SharpCompress(MS-PL/유사 permissive, v0.4x, 2026년에도 활발히 유지보수, 5일 전 커밋)가 단연 최적 — 순수 C#, 무의존, zip/tar/gzip/bzip2/lzip/zstd/7z 쓰기 + RAR/arj/arc 읽기, non-seekable 스트림 + async 지원. 단일 EXE에 그대로 포함 가능.

권장 접근 (.NET 9 / 단일 EXE)

단계적 카테고리 확장을 권장한다. 핵심 원칙은 '프로젝트가 이미 검증한 외부-CLI 호출 패턴(DocumentProvider→soffice --headless --convert-to)을 그대로 복제'하는 것과 '단일 포터블 EXE에 부담을 주지 않도록 순수 관리 코드 라이브러리를 1순위로 채택'하는 것이다.\n\n[1순위 — 순수 .NET, EXE에 바로 포함, 라이선스 깨끗]\n- 아카이브: SharpCompress 추가 → ArchiveProvider 신설. zip/7z/tar/gz/bz2 ↔ (압축/해제). 무의존 순수 C#라 포터블 EXE에 이상적.\n- 데이터: Parquet.Net(MIT, v6.0.3, 순수관리, .NET8/10) + ClosedXML(MIT) + CsvHelper로 csv↔json↔xlsx↔parquet DataProvider. 전부 순수 관리 코드.\n- 벡터: 이미 SkiaSharp 생태계에 가까우므로 Svg.Skia(MIT, v5.0.0)로 svg→png/jpg/webp/pdf. EPS는 ImageMagick+Ghostscript 델리게이트 활용(이미 ImageMagick 보유).\n\n[2순위 — 외부 CLI 의존, LibreOffice와 동일한 ExternalToolDetector 패턴]\n- 마크업/학술 문서: PandocNet(MIT) + pandoc.exe 외부 의존 → PandocProvider. md/rst/org/latex/mediawiki/asciidoc/textile/ipynb/epub 등을 매트릭스로 노출.\n- 전자책: Calibre의 ebook-convert.exe(GPLv3, 별도 프로세스 호출이므로 GPL 전파 없음) → EbookProvider. epub↔mobi↔azw3↔pdf↔docx.\n- 오디오/비디오: FFMpegCore(MIT) + FFmpeg LGPL 바이너리 → MediaProvider. mp4/mkv/mp3/wav/flac/webm 등. (Xabe.FFmpeg는 비상업 라이선스이므로 배제)\n\n[3순위 — 보류/조건부]\n- 폰트: 라이선스 비용 없는 LayoutFarm/Typography로 ttf↔woff/woff2 읽기, woff2 쓰기는 Brotli 압축 필요. SixLabors.Fonts 3.x는 빌드타임 라이선스 강제로 회피. 수요 확인 후 진행.\n- CAD: ACadSharp가 alpha라 신중. ODA File Converter는 재배포 제약. 수요 검증 전까지 보류.\n\nUX는 워치폴더(FileSystemWatcher + 500ms 디바운스 + 파일 잠금 재시도) + CLI 자동화(이미 CliRouter 존재)를 더해 '배치/자동화 3종 세트(드래그앤드롭·핫폴더·CLI)'를 완성하는 것을 권장.

라이브러리 · 도구

이름용도라이선스성숙도
PandocNet (SimonCropp)Pandoc CLI 강타입 .NET 래퍼 — md/rst/org/latex/mediawiki/asciidoc/ipynb/epub 등 40+ 마크업·문서 상호변환의 허브 MITactive (v4.0.0, 2026-04, CliWrap 기반). pandoc.exe 번들 안 함 — 외부 설치 필요
SharpCompress아카이브 압축/해제 — zip/7z/tar/gzip/bzip2/lzip/zstd 쓰기 + rar/arj/arc 읽기 MS-PL 계열 permissive (상업 사용 가능)production (v0.48.x, 2026년 5일 전 커밋, 활발). 순수 C#, 무의존, async 지원
Parquet.Net (aloneguid)Apache Parquet 읽기/쓰기 (데이터 카테고리) MITproduction (v6.0.3, 2026-05, 27M 다운로드). 순수 관리 코드, 무의존, .NET8/10
ClosedXML + CsvHelper + ExcelDataReaderxlsx/csv 읽기·쓰기, json 변환 (데이터 카테고리) MIT (ClosedXML, CsvHelper) / MS-PL (ExcelDataReader)production, 모두 순수 관리 코드
Svg.Skia (wieslawsoltes)SVG → PNG/JPG/WebP/PDF/XPS 래스터화 (벡터 카테고리) MITactive (v5.0.0, 2026-05). SkiaSharp 백엔드 의존(네이티브 자산 필요)
Calibre ebook-convert (CLI)전자책 변환 — epub↔mobi↔azw3↔pdf↔docx, --output-profile kindle 등 GPLv3 (별도 프로세스 호출이므로 앱에 GPL 전파 안 됨)production (Calibre 9.x, 문서 2026-05 갱신). 외부 설치 필요
FFMpegCore (rosenbjerg)오디오/비디오 변환 — mp4/mkv/webm/mp3/wav/flac 등 (FFmpeg/FFProbe 래퍼) MIT (래퍼) + FFmpeg는 LGPL 빌드 사용production, fluent 인자 빌더, sync/async
Magick.NET (이미 보유)이미지 + 벡터(SVG/EPS/AI/PS) + PSD/HEIC. Ghostscript 델리게이트로 EPS/AI/PS 래스터화 Apache-2.0production, 이미 프로젝트에서 사용 중
LayoutFarm/Typography폰트 읽기/변환 — ttf/otf/ttc/woff/woff2 읽기, 글리프 레이아웃 MIT/Apache 계열 (permissive, 라이선스 비용 없음)active이나 변환 API는 SixLabors/Aspose보다 저수준
ACadSharp (DomCR)CAD — DXF/DWG 읽기/쓰기 MITalpha (v3.6.x, 일부 엔티티 미구현·버그 가능)

통합 노트

프로젝트의 추상화는 신규 카테고리 추가에 매우 친화적이다. 각 신규 Provider는 IConverterProvider 3개 멤버(Capability getter, CheckAvailabilityAsync, ConvertAsync)만 구현하면 되고, Everything2EverythingBootstrap.CreateDefault()의 providers 배열에 한 줄 추가하면 ProviderRegistry가 N×M 매트릭스를 자동 인덱싱한다(_byPair / _outputsByInput).\n\n구체적 통합 방법:\n\n1) 매트릭스 선언: ProviderCapability.PairsFromMatrix(Inputs, Outputs)를 그대로 재사용. 예) ArchiveProvider는 Inputs={zip,7z,tar,gz,...}, Outputs={zip,7z,tar,gz}로 선언. PandocProvider는 마크업 포맷 배열로 거대 매트릭스 자동 생성. (단 Pandoc/LibreOffice 매트릭스가 겹치는 pair는 ProviderRegistry가 _byPair.TryAdd로 '먼저 등록된 Provider 우선'이므로 Bootstrap 배열 순서로 라우팅 우선순위 제어 — LibreOffice를 오피스 바이너리에, Pandoc을 마크업에 우선시키려면 순서 조정).\n\n2) 외부 도구 탐지: DocumentProvider.CheckAvailabilityAsync가 ExternalToolDetector.TryFindLibreOfficeSoffice(out _)를 호출하고 NotReady(reason, ExternalDependencies)를 반환하는 패턴을 그대로 복제. ExternalToolDetector에 TryFindPandoc / TryFindCalibreEbookConvert / TryFindFfmpeg / TryFindGhostscript 메서드를 추가하면 됨. ExternalDependency 레코드(Name/Description/DownloadUrl/IsRequired)로 미설치 시 다운로드 안내 UI가 기존 DiagnoseWindow와 연동된다.\n\n3) 외부 CLI 변환: DocumentProvider.SofficeConvertAsync의 ProcessStartInfo(UseShellExecute=false, CreateNoWindow=true, ArgumentList, WaitForExitAsync(ct) + proc.Kill(true) on cancel) 패턴이 pandoc/ebook-convert/ffmpeg에 그대로 적용된다. 출력물 검증(File.Exists(produced)) + targetPath로 Move하는 흐름도 동일. progress?.Report()는 ffmpeg의 경우 stderr의 time= 파싱으로 실제 진행률 산출 가능(FFMpegCore가 OnProgress 콜백 제공).\n\n4) 순수 관리 코드 Provider(SharpCompress/Parquet.Net/ClosedXML/Svg.Skia)는 외부 프로세스 없이 ConvertAsync 내부에서 직접 호출 — CheckAvailabilityAsync는 항상 ProviderAvailability.Ready 반환(Status=Available). MagickProvider가 이미 이 형태이므로 동일 스타일.\n\n5) ConvertOptions 확장: 카테고리별 인코딩 옵션 클래스(JpegEncodingOptions 등)가 이미 있는 패턴을 따라 ArchiveOptions(압축레벨), EbookOptions(output-profile), MediaOptions(코덱/비트레이트), PandocOptions(standalone/toc) 등을 추가. ConvertOptions에 프로퍼티로 노출하면 UI가 자동 바인딩 가능.\n\n6) UX 확장: CliRouter(이미 존재)에 워치폴더 모드 추가 — FileSystemWatcher를 IHosted/백그라운드로 띄우고 Created 이벤트에 500ms 디바운스 타이머 + 파일 잠금 재시도(IOException 시 짧은 지연 후 재시도) + InternalBufferSize 64KB 상향. 변환은 기존 ConversionEngine + ProviderRegistry.TryGet으로 재사용. 큐 inline progress bar(이미 보유)와 결합하면 핫폴더→큐 자동 적재 UX 완성. XnConvert식 액션 체이닝은 ConvertOptions에 후처리 파이프라인(resize→convert) 추가로 확장 가능.

출처

13

코드 심층 분석 (5개 서브시스템)

현재 코드를 직접 읽고 file:line으로 인용한 약점·확장 차단 요소·개선 기회.

Core 추상화 & 변환 엔진 (Everything2Everything.Core)

변환 능력을 IConverterProvider로 추상화하고, ProviderRegistry가 (입력확장자, 출력확장자) 쌍을 단일 홉 딕셔너리로 매핑하며, ConversionEngine이 단일/배치/결합 변환을 오케스트레이션하는 구조다. 양방향 N×M 매트릭스는 ProviderCapability.PairsFromMatrix로 각 Provider가 자기 입력·출력의 데카르트 곱을 선언해 표현하지만, 멀티홉 경로는 엔진이 아니라 각 Provider 내부에 하드코딩(DocxProvider/HwpxProvider→PdfProvider, HeicProvider→MagickProvider, DocumentProvider의 거대 switch)되어 있다. 이미지 중심으로 설계가 견고하게 동작하지만, 진정한 다방향 변환·AI·영상/오디오로 확장하려면 핵심 추상화 자체의 재설계가 필요하다.

설계 약점

  • 멀티홉 경로 탐색의 부재가 가장 큰 부채: ProviderRegistry는 단일 (input,output) 룩업만 하고(ProviderRegistry.cs:43,49) 그래프가 없어, A→B→C 같은 경로는 매번 Provider 내부에 손으로 짜야 한다. DocumentProvider.RouteAsync(DocumentProvider.cs:92-205)는 사실상 사람이 손으로 그린 경로 그래프이며, 형식이 늘어날수록 switch가 조합 폭발한다
  • Provider 간 체이닝이 생성자 주입으로 하드와이어됨: Bootstrap이 new HeicProvider(magick), new DocxProvider(pdf), new OcrProvider(pdf)처럼 의존성을 수동 결선(Everything2EverythingBootstrap.cs:9-21)한다. 새 중간 형식(예: DOCX→PDF→이미지 외에 DOCX→HTML→이미지)을 자동으로 발견할 방법이 없다
  • ProviderRegistry._byPair.TryAdd(ProviderRegistry.cs:22)는 같은 (input,output)을 여러 Provider가 선언하면 '먼저 등록된 것이 이긴다'를 조용히 적용한다. 우선순위/품질 기반 선택이 불가능하고, 충돌이 경고 없이 묻힌다(예: docx→png을 DocxProvider와 잠재적 다른 Provider가 동시 주장 시)
  • ConversionEngine이 ImageMagick에 직접 의존(ConversionEngine.cs:2)하고 CombineAsync/LoadImageForCombine/ApplyCombineEncoding(164-257)에서 MagickImageCollection을 직접 조작 — '결합'이 Provider 추상화 밖에 있어 엔진이 특정 라이브러리에 결합(coupling)되고, 이미지 외 결합(PDF 병합·동영상 concat)으로 확장 불가
  • ConvertOptions(ConvertOptions.cs:17-58)가 11개 sub-record를 가진 갓 오브젝트로, 형식 추가마다 sub-record가 늘어난다. 모든 Provider가 동일한 거대 옵션을 받지만 대부분 무시하고, 영상/오디오/코덱/AI 관련 옵션을 담을 자리가 없다
  • IConverterProvider(IConverterProvider.cs:9-15)가 '단일 파일 경로 in → outputDirectory에 파일 out, IProgress<double>'로 고정되어 있어 스트리밍, 다중 입력(N→1 결합), AI 프롬프트/모델 파라미터, 미디어 메타데이터(코덱·비트레이트·길이) 프로빙을 표현할 수 없다
  • Provider 목록이 컴파일타임 고정(Bootstrap의 배열). 플러그인 DLL 동적 로딩, ProviderStatus.ComingSoon을 실제 구현으로 교체할 확장 지점이 없다
  • 결합 가능 형식이 ConversionEngine의 정적 HashSet(CombinableInputs/Outputs, ConversionEngine.cs:14-23)에 박혀 있어 Provider의 능력 선언과 이중 관리되고 동기화가 깨지기 쉽다

확장성 차단 요소 (file:line)

  • ProviderRegistry.cs:6,43,49 — 매칭이 Dictionary<(Input,Output)> 단일 홉뿐. 멀티홉 경로 탐색(BFS/Dijkstra) API가 전혀 없어 양방향·다방향 극대화의 근본 한계
  • ProviderRegistry.cs:22 — `_byPair.TryAdd`가 동일 변환쌍 충돌을 조용히 첫 등록자 우선으로 삼킴. 비용/품질 가중치 기반 경로 선택 불가
  • ConversionEngine.cs:2 + 164-257 — 엔진이 ImageMagick(MagickImageCollection)에 직접 의존. 결합 로직이 Provider 밖에 있어 추상화 누수, 비이미지 결합 확장 차단
  • IConverterProvider.cs:9-15 — 시그니처가 (sourcePath, outputDirectory, outputExtension, ConvertOptions, IProgress<double>) 단일 파일·단일 출력 디렉터리·실수 진행률로 고정. 다중 입력·스트리밍·AI 파라미터·미디어 프로빙 표현 불가
  • Everything2EverythingBootstrap.cs:9-21 — Provider 인스턴스와 체이닝 의존성이 컴파일타임 하드코딩. 동적 플러그인 로딩/등록 지점 부재
  • ConvertOptions.cs:35-55 — 11개 이미지/문서 중심 sub-record. 영상 코덱/비트레이트/프레임레이트, 오디오, AI(모델·프롬프트·온도) 옵션을 담을 구조가 없고 형식마다 sub-record 증식
  • DocumentProvider.cs:92-205 — 멀티홉 라우팅이 손으로 짠 switch 그래프. 형식 N개에 대해 경로가 O(N^2)로 수동 증식, 새 형식 추가 시 모든 분기 갱신 필요
  • ConvertResult.cs:10 — 결과가 출력 파일 경로 리스트만 담음. 추출 텍스트·AI 응답·메타데이터·중간 산출물 같은 비파일 결과를 표현할 필드 없음

개선 기회

  • ProviderRegistry를 변환 그래프로 승격하고 엔진에 멀티홉 경로 탐색(BFS/Dijkstra) 추가 impact high effort high
  • CombineAsync를 IMultiInputProvider(또는 N→1 Provider 추상화)로 분리해 엔진의 ImageMagick 직접 의존 제거 impact high effort medium
  • IConverterProvider 시그니처를 ConvertRequest/ConvertContext 객체로 일반화하고 다중 입력·비파일 결과·미디어 메타데이터 지원 impact high effort high
  • AI/LLM 변환을 위한 IAiConverterProvider 확장과 ConvertOptions.Ai 옵션 도입 impact high effort medium
  • Provider 동적 로딩(플러그인) 도입 — Bootstrap 하드코딩 제거 impact medium effort high
  • ProviderRegistry 변환쌍 충돌을 명시적 우선순위/진단으로 전환 impact medium effort low
  • ConvertOptions를 형식별 옵션 백(IReadOnlyDictionary 또는 옵션 프로바이더)으로 분해 impact medium effort medium
Provider 매트릭스 전체 (8개 IConverterProvider + ProviderRegistry 디스패치)

8개 Provider(Magick/Heic/Pdf/Docx/Html/Hwpx/Ocr/Document)가 `IConverterProvider`를 구현하고, 각자 `PairsFromMatrix(inputs, outputs)`로 N×M 변환 쌍을 카르테시안 곱으로 선언하면 `ProviderRegistry`가 `(input,output)→provider` 딕셔너리로 평탄화해 디스패치한다. 실제 변환은 대부분 "중간 포맷으로 정규화 후 위임"하는 파이프라인 — 이미지는 Magick로, 문서/한글은 LibreOffice→PDF→Magick로, HTML은 WebView2→PNG/PDF로 수렴한다. 결과적으로 매트릭스는 "거의 모든 것 → 이미지/PDF/텍스트" 방향으로만 풍부하고, 그 역방향(이미지/PDF → 편집가능 문서, HWP 출력, 미디어/아카이브 전 카테고리)이 구조적으로 비어 있다. 양방향·다방향·AI·미디어 코덱이라는 프로젝트 목표 대비 현재는 단방향 래스터라이저 집합에 가깝다.

설계 약점

  • 매트릭스가 사실상 단방향: 거의 모든 입력이 이미지/PDF/텍스트로 '나가기만' 한다. 편집가능 포맷으로 되돌아오는 경로가 OCR(이미지/PDF→txt/docx, 그것도 레이아웃 손실 평문)뿐이고, 진짜 구조 보존 역변환(PDF→DOCX, 이미지→벡터/문서)이 전무.
  • PDF가 입력으로만 풍부하고 출력이 빈약: PdfProvider는 PDF→이미지 렌더링만 한다(PdfProvider.cs:11-12). 이미지/HTML/DOCX→PDF는 각기 다른 Provider가 따로 만들지만, PDF→PDF(압축/병합/분할/회전)와 PDF→DOCX가 없어 프로젝트 핵심 목표인 'PDF 압축'을 어떤 Provider도 수행하지 못함.
  • HWP는 출력 불가가 구조적 한계로 고착: DocumentProvider가 HWP/HWPX를 입력으로만 받고 출력 Outputs 배열에 .hwp가 없다(DocumentProvider.cs:24, RoadmapNote:40). H2Orestart가 쓰기를 지원 안 해 'HWP↔DOCX/PDF/HTML' 목표의 절반(→HWP)이 막혀 있음.
  • DOC/DOCX 출력의 정의 충돌·왕복 불가능: DocxProvider는 .docx를 입력으로만(→PDF/이미지), DocumentProvider는 .docx를 입력이자 출력으로 선언하지만 OCR도 .docx를 출력한다. 'DOCX 편집본을 다시 받는' 일관된 단일 경로가 없고, DOCX→DOCX 같은 동일포맷은 ConversionEngine에서 Skip 처리됨.
  • 외부 프로세스 안정성 취약점: LibreOffice 호출이 3곳(DocxProvider/HwpxProvider/DocumentProvider)에 복붙되어 있고 타임아웃이 전혀 없다 — soffice가 멈추면 WaitForExitAsync가 무한 대기(취소 토큰에만 의존). 또한 soffice는 동일 사용자 프로필을 공유해 동시 인스턴스 충돌 위험이 있으나 직렬화/락이 없음.
  • AI/LLM 통합 지점 부재: ConvertOptions에 OCR 백엔드 문자열('auto')만 있을 뿐, Codex/LLM 호출을 위한 추상화(요약/번역/생성형 변환/캡셔닝)가 인터페이스·옵션·Provider 어디에도 없음. 현재 구조에 AI를 끼워넣을 확장점이 설계되지 않음.
  • 미디어·아카이브·폰트·CAD 카테고리 전무: 영상(mp4/mov/webm), 오디오(mp3/wav/flac), 아카이브(zip/7z/tar), 폰트(ttf/otf/woff), 벡터(svg/eps), CAD(dwg/dxf), 전자책(epub/mobi)을 다루는 Provider가 0개. `ProviderStatus.ComingSoon`은 enum에만 존재하고 이를 사용하는 Provider가 하나도 없어 로드맵 UI가 빈 상태.
  • 레지스트리의 조용한 우선순위 함정: `_byPair.TryAdd`(ProviderRegistry.cs:22)는 첫 등록 Provider가 승리하고 후속은 '말없이 무시'된다. 향후 같은 (input,output) 쌍을 두 Provider가 선언하면(예: PDF 압축 vs PDF 렌더) 부트스트랩 순서에 따라 비결정적으로 한쪽이 사라짐 — 경고도 없음.

확장성 차단 요소 (file:line)

  • IConverterProvider.cs:9-15 — ConvertAsync 시그니처가 단일 sourcePath/단일 outputExtension에 고정. 다방향(N입력→1출력 병합, 1입력→N출력 동시) 변환과 '입력=출력 동일포맷 최적화(PDF압축, 이미지 리인코딩)'가 인터페이스 레벨에서 표현 불가 (동일포맷은 ConversionEngine.cs:88-89에서 무조건 Skip됨).
  • DocumentProvider.cs:24 `Outputs = {.html,.docx,.md,.txt}` 및 :40 RoadmapNote — HWP/HWPX가 출력 배열에서 영구 제외. 양방향 HWP를 위해서는 H2Orestart 쓰기 대체재(또는 자체 HWP writer)가 필요하나 코드 구조상 출력 라우트(RouteAsync, :92-205)에 .hwp 분기 자체가 없음.
  • PdfProvider.cs:11-12 PdfRenderOutputs에 .pdf·.docx 부재 — PDF→PDF(압축/병합) 및 PDF→편집문서 경로가 Provider 차원에서 차단. PDFtoImage 라이브러리는 렌더 전용이라 PDF 쓰기/조작 백엔드(예: PdfPig/QuestPDF/iText) 도입 전까지 확장 불가.
  • ConvertOptions.cs 전체 — 옵션 클래스가 이미지·PDF렌더·HTML렌더·OCR로 한정. Video/Audio/Archive/Ai 옵션 그룹이 없어, 미디어 코덱(비트레이트/해상도/fps)이나 LLM 프롬프트/모델 선택을 전달할 통로가 없음. 새 카테고리는 옵션 모델 확장이 선행되어야 함.
  • MagickProvider.cs:8-19, HeicProvider.cs:11-12, PdfProvider.cs:11-12 등 — 각 Provider가 지원 확장자 배열을 하드코딩. ffmpeg 같은 '수백 포맷 양방향' 백엔드를 PairsFromMatrix로 표현하면 조합 폭발(예: 50입력×50출력=2500쌍)로 레지스트리 메모리·UI 매트릭스가 비현실적 — 능력 기반(capability predicate) 표현이 없음.
  • ProviderRegistry.cs:22 `_byPair.TryAdd` — 동일 (input,output)에 복수 Provider(예: 빠른 변환 vs 고품질 vs AI 변환)를 '선택지'로 공존시키는 모델 부재. 한 쌍당 정확히 하나의 Provider만 허용해 변환 전략 다중화(품질/속도/AI 토글)가 불가능.
  • Everything2EverythingBootstrap.cs:11-21 — Provider 목록이 컴파일타임 하드코딩 배열. 플러그인/동적 등록(외부 DLL, ffmpeg 유무에 따른 조건부 등록)이 없어 신규 카테고리 추가가 항상 코어 재컴파일을 요구.

개선 기회

  • LibreOffice 호출 로직을 단일 SofficeRunner 서비스로 통합 + 타임아웃·직렬화 추가 impact high effort low
  • 이미지 인코딩(ApplyEncoding/ApplyTransforms) 공용 ImageEncoder 헬퍼로 추출 impact medium effort low
  • FFmpeg 기반 미디어 Provider(영상·오디오) 신설 + 능력기반 매트릭스 표현 도입 impact high effort high
  • PDF 쓰기/조작 백엔드 도입으로 PDF→PDF(압축/병합/분할)·PDF→DOCX 경로 개통 impact high effort high
  • AI/LLM 변환 확장점 설계: IConverterProvider에 AI Provider 카테고리 + ConvertOptions.Ai 옵션 그룹 impact high effort high
  • 레지스트리 다중 Provider 공존 + 명시적 우선순위/충돌 경고 모델 impact medium effort medium
  • 아카이브(zip/7z) 및 폰트(ttf↔woff2) Provider 추가로 빈 카테고리 보강 impact medium effort medium
App UI / CLI / Shell (Everything2Everything WPF .NET 9)

WPF FluentWindow 기반 데스크톱 UI로, MainWindow가 큐 관리·매트릭스 출력 필터링·진행 표시·프리뷰·이력을 모두 코드비하인드(MainWindow.xaml.cs 1171줄)에서 직접 처리한다. 출력 형식은 큐 내 모든 입력의 OutputsForFile 교집합으로 1-hop 직접 변환만 필터링하며(RefreshAvailableOutputFormats:836), 멀티홉/체이닝/AI/미디어 개념은 코드 어디에도 없다. CLI(CliRouter)는 5개 verb를 파싱하지만 register/diagnose를 제외하면 모두 GUI 창을 띄우는 런처에 불과해 stdout/exit-code 기반 자동화·파이프라인이 불가능하다. ContextMenuRegistrar는 12개 PopularOutputs를 별도 하드코딩하여 ProviderRegistry 매트릭스와 부분적으로만 동기화된다.

설계 약점

  • MVVM 전무: MainWindow.xaml.cs 1171줄에 View 로직·ViewModel(QueueItem/DateGroup/HistoryRow)·Service 호출(PreviewService/HistoryStorage/engine.ConvertManyAsync)·CSV/JSON 익스포트가 한 파일에 혼재. 명령은 일부 RelayCommand(25-38)지만 대부분 코드비하인드 이벤트 핸들러(OnProcessQueueClick 등)라 테스트·재사용 불가
  • 변환 옵션 UI가 형식별로 확장 불가능한 구조: 사이드바에는 Quality 슬라이더 하나만 있고 ConvertOptions의 10여 종 옵션(PngCompression, AvifSpeed, PdfRender.Dpi, Ocr.Language, HtmlRender.Viewport 등)이 전혀 노출되지 않음. BuildOptions(259-281)는 JPEG/WebP/AVIF Quality만 슬라이더에서 읽고 나머지는 모두 기본값 하드코딩
  • CLI가 진정한 자동화에 부적합: dialog/quick/showmain 모드가 모두 WPF 창을 띄우며(App.xaml.cs:39-57), stdout으로 결과(출력 경로/성공·실패 카운트)를 반환하지 않고 exit code도 Quick 실패 시 1만 반환. JSON 출력·배치 매니페스트·표준입력 파이프·진행 스트리밍이 없어 AI/스크립트가 결과를 파싱 불가
  • 출력 형식 매트릭스가 3곳에 중복 하드코딩: MainWindow.AllFormats(813-827, 12개), ContextMenuRegistrar.PopularOutputs(14-28, 12개), OpenFileDialog 입력 필터(96). 새 형식 추가 시 세 곳을 수동 동기화해야 하며 라벨·정렬·색상 리소스 키가 분산
  • 멀티홉 경로 표현 수단이 UI/엔진 양쪽에 전무: ComboBox는 1-hop 직접 변환만 나열하고, 멀티홉이 생기면 '직접 vs 경유' 구분, 경로 미리보기(예: HWP→PDF→PNG), 중간 형식 선택, 품질 누적 손실 경고를 표현할 자리가 없음
  • QuickProgressWindow는 취소 버튼이 없어(취소 토큰을 ConvertManyAsync에 전달조차 안 함, App.xaml.cs:96) CLI/우클릭 경로의 긴 변환을 중단할 방법이 없음 — 메인 창과 취소 UX가 비대칭
  • 케이퍼빌리티 상태가 시작 시 1회만 점검되고(RefreshCapabilityStatusAsync) 결과를 캐시하지 않아, 형식 선택 시점에 해당 출력이 외부 도구 부재로 실패할지 사전 차단하지 못함 — 변환 실행 후에야 실패 확인
  • 데모 시드 데이터(SeedDemoHistory:667-708)가 프로덕션 코드에 하드코딩되어 첫 실행 시 가짜 14.2MB PNG 등을 이력에 주입, 통계(EST. SPACE SAVED)를 오염시킴

확장성 차단 요소 (file:line)

  • MainWindow.xaml.cs:849-858 — RefreshAvailableOutputFormats가 OutputsForFile(1-hop 직접 변환)의 단순 교집합만 계산. 멀티홉 그래프 탐색(BFS/DFS over ConversionPair)이 없어 'HWP→DOCX 경유 PDF' 같은 간접 경로가 출력 목록에 절대 나타나지 않음
  • ProviderRegistry.cs:52-61 — OutputsForInput이 _outputsByInput 직접 매핑만 반환. 도달 가능 그래프(transitive closure)·경로 비용·중간 형식 개념이 없어 다방향 변환 확장의 근본 차단점
  • MainWindow.xaml.cs:963-976 — UpdateQualityPanelForFormat이 ext를 switch로 하드코딩(.jpg/.webp/.avif만 quality 패널 표시). 형식별 옵션 스키마가 Provider에서 선언적으로 오지 않아, 새 옵션(코덱·비트레이트·OCR 언어·LLM 프롬프트)을 추가하려면 XAML+코드비하인드를 직접 수정해야 함
  • MainWindow.xaml.cs:259-281 — BuildOptions가 ConvertOptions의 일부 필드만 수동 채움. 미디어/AI Provider가 요구할 옵션(영상 코덱, CRF, 프레임레이트, AI 모델명, API 키)을 받을 동적 옵션 바인딩 메커니즘 부재
  • IConverterProvider.cs:9-15 — ConvertAsync 시그니처가 단일 sourcePath→단일 outputExtension 동기 변환 전제. 스트리밍 입력, 다중 출력 산출물(예: 영상→썸네일+자막+트랜스코드), AI 비동기 잡, 진행 중 부분 결과를 표현할 수 없음
  • ContextMenuRegistrar.cs:14-28 — PopularOutputs 배열이 정적이라 AI/미디어 형식(.mp4/.webm 등) 추가 시 카스케이드에 자동 반영되지 않고, 멀티홉 출력도 우클릭 메뉴에 노출 불가
  • App.xaml.cs:39-57 & CliRouter.cs:21-49 — CLI 파서가 옵션 플래그(--quality, --output-dir, --json, --recursive)를 전혀 받지 않고 verb+files 구조만 지원. AI 통합(LLM 프롬프트 전달)·배치 스크립팅을 위한 인자 확장 여지가 구조적으로 막힘

개선 기회

  • 엔진에 변환 그래프 + 멀티홉 경로 탐색 도입 impact high effort high
  • Provider 선언형 옵션 스키마 + 동적 옵션 UI 생성 impact high effort high
  • 헤드리스 CLI 모드 분리 (stdout JSON + exit code + 옵션 플래그) impact high effort medium
  • 출력 형식 매트릭스 단일 SSOT로 통합 impact medium effort low
  • MainWindow를 MVVM으로 분해 impact medium effort high
  • QuickProgressWindow에 취소 + 케이퍼빌리티 사전 점검 impact medium effort medium
인프라 (히스토리 / 프리뷰 / 빌드 / 패키징 / CI-CD / 설정 영속화)

히스토리는 `%LocalAppData%`에 append-only JSONL로 저장하는 정적 헬퍼(HistoryStorage)와 UI 전용 인메모리 컬렉션(HistoryStore)으로 단순 분리돼 있고, 프리뷰는 이미지/PDF/HEIC만 동기 렌더하며 문서·영상은 명시적으로 스텁 처리한다. 빌드/패키징은 framework-dependent MSIX 단일 산출물에 최적화돼 있어 .NET DLL과 C++ Shell DLL만 Layout에 복사하고 FFmpeg/Ghostscript 같은 대형 외부 바이너리 번들링 메커니즘이 전혀 없다. 가장 큰 구조적 공백은 설정 영속화 계층의 완전 부재로, 변환 옵션은 매번 UI에서 재구성되고(BuildOptions) AI API 키·도구 경로·사용자 기본값을 저장할 곳이 없으며, 테스트 인프라도 0이다.

설계 약점

  • 설정 영속화 계층이 전무함 — 저장되는 영구 상태는 history.jsonl 단 하나. ConvertOptions는 MainWindow.BuildOptions()(MainWindow.xaml.cs:259-281)에서 매 실행마다 UI 컨트롤로부터 새로 생성되고 _conflictRule·QualitySlider 값은 앱 재시작 시 소실. AI(Codex/LLM) API 키, LibreOffice/FFmpeg 경로, 사용자 기본 출력 형식을 저장할 메커니즘이 없어 AI 통합·도구 경로 캐싱의 토대가 없음
  • PreviewService가 문서·영상을 원천 미지원 — 문서는 '다음 업데이트에서 지원'(PreviewService.cs:28-29), 영상은 case 자체가 없어 RenderViaMagick(PreviewService.cs:30)로 폴백→MagickImage가 mp4/mov를 못 열어 예외. HWP↔DOCX, 영상 코덱 변환 목표 대비 프리뷰가 핵심 입력 타입을 커버 못함
  • 프리뷰 캐싱 부재 — 같은 파일을 다시 선택할 때마다 디코딩/리사이즈/temp PNG 왕복(RenderHeic의 PreviewService.cs:67-76)을 재수행. 대용량/배치 큐에서 동일 항목 반복 클릭 시 매번 풀 디코딩
  • 히스토리 전체 로드가 비확장적 — File.ReadAllLines(HistoryStorage.cs:27)로 전 파일을 메모리에 적재 후 OrderBy(MainWindow.xaml.cs:663)로 전량 정렬. 영상/배치 변환으로 엔트리가 수만 줄 쌓이면 시작 시 전부 파싱·정렬해야 함(페이징·tail read·만료 정책 없음)
  • 히스토리 도메인 로직이 UI에 결합 — 로드/그룹화/데모 시드(SeedDemoHistory MainWindow.xaml.cs:667-708)가 MainWindow.xaml.cs에 박혀 있고, 실측 데이터가 없으면 가짜 '842.4 MB 절약' 데모가 통계에 섞임(MainWindow.xaml.cs:685,704). 헤드리스/CLI 배치 경로에서 히스토리 기록 재사용 불가
  • 빌드/패키징이 대형 외부 바이너리 번들을 전혀 가정하지 않음 — BuildMsix.ps1:108-109가 publish 산출물 + Shell DLL만 Layout에 복사. FFmpeg(~80MB)/Ghostscript를 동봉하려면 Layout 복사 단계, MSIX 용량(현재 200MB+ 압축 한계 고려), 라이선스(FFmpeg LGPL/GPL) 처리 로직이 모두 없음
  • 테스트 인프라 0 — 솔루션에 테스트 프로젝트가 없음(Glob **/*Test* 무결과). OutputPathHelper 충돌 해결, ConversionEngine 결합 로직, JSONL round-trip 같은 순수 함수조차 자동 검증 없어 양방향 매트릭스 확장 시 회귀 탐지 불가
  • CI가 self-contained/portable EXE를 빌드·게시하지 않음 — README는 portable EXE를 'A) 가장 가벼움'으로 권장하나(README.md:140), 두 워크플로 모두 MSIX만 산출(build.yml:42, release.yml:75). .NET 9 Desktop Runtime 미설치 환경용 self-contained 배포 산출물이 자동화에 없음
  • CI에 NuGet/빌드 캐시가 없어(build.yml 전체) 매 실행마다 Magick.NET·WebView2 등 대형 패키지를 재복원 — 외부 바이너리까지 더해지면 빌드 시간 선형 증가

확장성 차단 요소 (file:line)

  • PreviewService.cs:23-31 — 확장자 switch가 닫힌 구조이고 default가 RenderViaMagick(MagickImage)로 폴백. 영상(.mp4/.mov/.mkv) 프리뷰를 추가하려면 FFmpeg 프레임 추출 case를 직접 삽입해야 하며, 현재는 영상 입력 시 MagickImage 생성자에서 예외 발생
  • PreviewService 전체가 static 클래스 + 하드코딩된 디코더 의존성(ImageMagick/PDFtoImage/Libheif) — 영상/AI 썸네일을 위한 디코더 플러그인 주입(DI) 지점이 없어 IPreviewRenderer 추상화 없이는 확장 불가
  • ConvertOptions(ConvertOptions.cs:17-58)에 영상(코덱/비트레이트/fps)·AI(모델/프롬프트/API키)·압축(PDF 압축 레벨) 옵션 sub-record가 없고, 영속화 대상이 아니라 [JsonSerializable] 직렬화 속성도 없음 — AI 기능 설정과 도구 경로를 담을 영구 설정 모델이 부재
  • BuildMsix.ps1:102-115 Layout 구성 단계가 publish 출력 + 단일 Shell DLL만 복사하도록 하드코딩 — FFmpeg/Ghostscript/Tesseract 같은 외부 바이너리를 동봉하는 Copy-Item 단계와 그 경로를 런타임에 해석하는 메커니즘이 없음
  • Package.appxmanifest:175-177이 runFullTrust 단일 capability만 선언 — AI 기능의 네트워크 호출(internetClient)이나 추가 파일 타입(.mp4/.mov/.webm/.mkv) ItemType 등록(현재 manifest:69-168에 영상 확장자 전무)이 없어 영상 우클릭 메뉴 노출 불가
  • HistoryEntry(HistoryStore.cs:5-15)가 영상/배치 변환 메타데이터(코덱, 해상도, duration, 다중 출력 통계)를 표현할 필드가 부족 — MetaLine 단일 문자열에 의존(HistoryStore.cs:13)해 구조화된 영상 변환 이력 질의 불가
  • 설정/키 저장소가 없어 AI API 키를 DPAPI(ProtectedData)로 암호화 저장할 진입점 자체가 부재 — HistoryStorage.cs의 LocalAppData 패턴은 있으나 평문 JSONL이라 비밀 저장에 부적합

개선 기회

  • ISettingsStore(설정 영속화 계층) 도입 — DPAPI 암호화 + JSON impact high effort medium
  • IPreviewRenderer 추상화 + 영상 프레임 추출(FFmpeg) + 프리뷰 캐시 impact high effort high
  • 외부 바이너리 번들링 파이프라인 (BuildMsix.ps1 확장 + ExternalToolDetector 폴백) impact high effort high
  • 히스토리 영속화의 스트리밍 로드 + 만료/회전 정책 impact medium effort medium
  • 히스토리 도메인 로직을 UI에서 Core로 분리 + 데모 시드 제거 impact medium effort low
  • 테스트 프로젝트 신설 (xUnit) — 순수 함수 우선 impact high effort medium
  • CI 강화 — NuGet 캐시 + self-contained portable 산출물 + 테스트 게이트 impact medium effort low
  • OutputPathHelper 충돌 처리 강건화 + 결합 출력 경로 일관화 impact low effort low
횡단 품질 (에러 처리 / 동시성 / 취소 / 보안 / 메모리 / 테스트)

변환 파이프라인의 횡단 관심사는 비교적 일관된 패턴(IProgress 보고, OperationCanceledException 재던짐, try/finally 임시정리, ConvertResult.Fail 래핑)으로 손코딩되어 있고, 외부 프로세스 호출은 ProcessStartInfo.ArgumentList를 사용해 셸 인젝션을 구조적으로 회피한다. 그러나 배치 변환이 전적으로 순차 for-loop(ConversionEngine.cs:57)라 멀티코어/외부프로세스 대기 시간을 전혀 활용하지 못하고, 취소 토큰 전파가 라이브러리 경계(Magick.NET, Windows OCR, CDP)에서 끊기며, 메모리는 전 페이지/전 프레임을 한꺼번에 디코딩하는 비스트리밍 구조다. 테스트 프로젝트가 전무(0개)하고 NuGet 취약점 경고가 csproj에서 통째로 억제(NU1901-1904)되어, AI/영상/대용량 기능으로 확장하기 전에 품질 안전망이 먼저 필요하다.

설계 약점

  • 배치가 100% 순차 for-loop(ConversionEngine.cs:57-70)다. LibreOffice/OCR/WebView2처럼 대기 시간이 긴 변환에서 단일 파일씩만 처리해 멀티코어를 전혀 못 쓴다. 100장 이미지 변환도 코어 1개만 사용.
  • 취소 토큰이 라이브러리 경계에서 단절된다. Magick.NET의 image.Write/Resize/collection.Coalesce(MagickProvider.cs:84-94, PdfProvider.cs:84-87)는 ct를 받지 않아, 거대 이미지 1장 인코딩 중에는 ThrowIfCancellationRequested 체크 지점 사이에서 취소가 지연된다. Windows OCR engine.RecognizeAsync(OcrProvider.cs:164)도 ct 미전달.
  • CLI quick 경로가 아예 취소 불가능하다: App.RunQuickAsync가 ConvertManyAsync를 CancellationToken 없이 호출(App.xaml.cs:96)하고 QuickProgressWindow에 취소 UI도 없다. 컨텍스트 메뉴 대량 변환을 중단할 방법이 없음.
  • 에러 메시지 채널이 비일관적이다. 코어는 ConvertResult.Message(구조화)로 반환하지만, MainWindow는 catch에서 MessageBox.Show(ex.Message)로만 노출(MainWindow.xaml.cs:569)하고 OnProcessQueueClick은 개별 result.Status가 Failed/Skipped여도 그냥 히스토리에만 적고(MainWindow.xaml.cs:551-561) 사용자에게 실패를 표면화하지 않는다. 실패 파일도 done처럼 큐에서 제거됨(line 563).
  • 메모리가 비스트리밍이다. PDF는 페이지마다 PNG 전체를 MemoryStream에 디코딩(PdfProvider.cs:79-87), OCR은 파일을 MemoryStream→ToArray()→InMemoryRandomAccessStream으로 3중 복사(OcrProvider.cs:148-159), HTML 캡처는 base64 PNG 전체를 메모리에 들고 Magick으로 재디코딩(HtmlProvider.cs:76-98). 8000x8000 같은 대용량/멀티프레임 GIF는 collection.Coalesce()로 전 프레임을 동시에 메모리에 적재(MagickProvider.cs:84).
  • ImageMagick 리소스 한계(ResourceLimits.Memory/Width/Height)가 어디에도 설정돼 있지 않다. 악의적/손상 이미지(decompression bomb)나 거대 RAW가 프로세스 메모리를 무제한 점유 가능. MagickProvider/PdfProvider/HtmlProvider/CombineAsync 전부 무방비.
  • 테스트가 0개다(test 프로젝트/파일 없음 — Glob *Test* 결과 없음). 12종×N 양방향 매트릭스, 라우팅 분기(DocumentProvider.RouteAsync), 충돌 규칙, 취소 경로 모두 회귀 검증이 불가능.
  • csproj가 NuGet 취약점 경고를 통째로 억제한다: NoWarn에 NU1901;NU1902;NU1903;NU1904 (Everything2Everything.Core.csproj:11). 알려진 CVE가 있는 패키지가 들어와도 빌드가 침묵한다. WebView2/OpenXML/Magick은 외부 미디어를 파싱하는 공격면이 큰 라이브러리들이라 위험.
  • _cts 접근에 동시성 보호가 없다. OnProcessQueueClick은 _cts.Token을 await 호출 인자로 직접 읽고(MainWindow.xaml.cs:541) finally에서 _cts=null로 set(line 574)하는데, OnCancelProcessingClick(line 621-622)이 다른 시점에 _cts.Cancel()을 호출한다. UI 스레드 단일 진입으로 대체로 안전하나 _cts 수명/dispose가 명시적이지 않고 CancellationTokenSource.Dispose()가 한 번도 호출되지 않음(누수).

확장성 차단 요소 (file:line)

  • ConversionEngine.cs:57-70 — ConvertManyAsync의 순차 for-loop가 하드코딩됨. 영상 트랜스코딩(파일당 수십 초~분)이나 AI 호출(LLM 왕복 지연)을 추가하면 순차 처리가 치명적 병목이 된다. 병렬도(MaxDegreeOfParallelism) 옵션이 ConvertOptions에 없음(ConvertOptions.cs 전체).
  • IProgress<double> 단일 스칼라 진행 모델(IConverterProvider.ConvertAsync 시그니처)은 영상(프레임/시간코드), AI(토큰 스트리밍), 다단계 파이프라인의 진행을 표현 못 한다. ConvertProgress(Index,Total,CurrentPath,FileProgress) (ConversionEngine.cs:285)도 단일 파일=단일 출력 가정에 묶여 있어 1→N 페이지 분할의 부분 진행을 못 담는다.
  • ConvertResult가 동기 완료 모델(ConvertResult.cs:10-25)이라 스트리밍/증분 출력(영상 인코딩 중 부분 미리보기, LLM 토큰 스트림)을 표현할 타입이 없다. OutputPaths는 변환 끝난 뒤에야 채워짐.
  • 외부 프로세스 실행 로직(ConvertWithLibreOfficeAsync)이 DocxProvider/HwpxProvider/DocumentProvider에 거의 동일하게 3중 복제됨(DocxProvider.cs:113-157, HwpxProvider.cs:107-151, DocumentProvider.cs:238-281). FFmpeg/ghostscript(PDF압축)/codex CLI 같은 새 외부도구를 추가할 때마다 stderr 수집·타임아웃·종료처리·결과검증 보일러플레이트를 또 복붙해야 한다. 공통 ExternalProcessRunner 추상화 부재.
  • 외부 프로세스에 타임아웃이 없다(WaitForExitAsync(ct)만, DocumentProvider.cs:266 등). LibreOffice/Word COM(DocxProvider.cs:159-186)이 hang하면 취소하기 전까지 영원히 대기. 영상/대용량 작업에선 walltime 한계가 필수.
  • stderr를 RedirectStandardError=true로 켜두고도 한 번도 읽지 않는다(DocumentProvider.cs:252, HwpxProvider.cs:116, DocxProvider.cs:122). 파이프 버퍼가 가득 차면 자식 프로세스가 블록될 수 있고, 실패 시 LibreOffice의 실제 오류 사유를 버려 ConvertResult가 'exit N'만 남긴다(DocumentProvider.cs:270). AI/코덱 도구 디버깅이 불가능.

개선 기회

  • 배치 변환에 제한된 병렬 처리(Parallel.ForEachAsync) 도입 impact high effort medium
  • 공통 ExternalProcessRunner 추상화 (타임아웃 + stderr 수집 + Kill 통합) impact high effort medium
  • ImageMagick ResourceLimits 전역 설정 + decompression bomb 방어 impact high effort low
  • 코어 단위 테스트 프로젝트 신설 (xUnit) — 순수 로직 우선 impact high effort medium
  • 실패/건너뜀 결과를 UI에 일관되게 표면화 impact medium effort low
  • CLI quick 경로에 취소 토큰 전파 + NuGet 취약점 경고 재활성화 impact medium effort low
  • 대용량 입력 스트리밍/페이지 단위 메모리 관리 impact medium effort high
  • 외부 프로세스 입력 검증 강화 (LibreOffice 출력 파일명 충돌) impact low effort low