IronPDF 튜토리얼 PDF 일괄 처리 C#을 이용한 일괄 PDF 처리: 대규모 문서 워크플로 자동화 아흐마드 소하일 업데이트됨:2월 6, 2026 다운로드 IronPDF NuGet 다운로드 DLL 다운로드 윈도우 설치 프로그램 무료 체험 시작하기 LLM용 사본 LLM용 사본 LLM용 마크다운 형식으로 페이지를 복사하세요 ChatGPT에서 열기 ChatGPT에 이 페이지에 대해 문의하세요 제미니에서 열기 제미니에게 이 페이지에 대해 문의하세요 Grok에서 열기 Grok에게 이 페이지에 대해 문의하세요 혼란 속에서 열기 Perplexity에게 이 페이지에 대해 문의하세요 공유하다 페이스북에 공유하기 트위터에 공유하기 LinkedIn에 공유하기 URL 복사 이메일로 기사 보내기 This article was translated from English: Does it need improvement? Translated View the article in English IronPDF를 사용한 C# 기반 일괄 PDF 처리는 .NET 개발자가 병렬 HTML-PDF 변환 , 대량 병합/분할부터 내장 오류 처리, 재시도 로직 및 체크포인트 기능을 갖춘 비동기 PDF 파이프라인 에 이르기까지 대규모 문서 워크플로를 자동화 할 수 있도록 지원합니다. IronPDF의 스레드 안전 Chromium 엔진과 IDisposable 기반 메모리 관리는 온프레미스, Azure Functions , AWS Lambda 또는 Kubernetes 에서 실행되든 관계없이 높은 처리량의 PDF 자동화를 위해 특별히 설계되었습니다. 요약: 빠른 시작 가이드 이 튜토리얼에서는 병렬 변환 및 대량 작업부터 클라우드 배포 및 복원력 있는 파이프라인 패턴에 이르기까지 C#을 사용한 확장 가능한 PDF 자동화에 대해 다룹니다. 대상: 문서 중심 워크플로우를 담당하는 .NET 개발자 및 아키텍트 - 문서 마이그레이션 프로젝트, 일일 보고서 생성 파이프라인, 규정 준수 개선 작업 또는 순차 처리가 불가능한 아카이브 디지털화 작업 등에 적합합니다. 개발할 내용: Parallel.ForEach를 사용한 병렬 HTML-PDF 변환, 일괄 병합 및 분할 작업, SemaphoreSlim를 사용한 동시성 제어 비동기 파이프라인, 실패 시 건너뛰기 및 재시도 로직을 포함한 오류 처리, 충돌 복구를 위한 체크포인트/재개 패턴, Azure Functions, AWS Lambda 및 Kubernetes용 클라우드 배포 구성. 지원 환경: .NET 6 이상, .NET Framework 4.6.2 이상, .NET Standard 2.0. 모든 렌더링은 IronPDF에 내장된 Chromium 엔진을 사용하므로 헤드리스 브라우저나 외부 서비스가 필요하지 않습니다. 이 접근 방식을 사용하는 경우: 순차 실행으로 처리할 수 있는 것보다 더 많은 PDF 파일을 처리해야 할 때, 예를 들어 대규모 문서 마이그레이션, 시간 제약이 있는 예약 배치 작업 또는 문서 부하가 가변적인 다중 테넌트 플랫폼에서 사용합니다. 기술적으로 중요한 이유: IronPDF의 ChromePdfRenderer는 스레드 안전하며 렌더링별로 상태를 저장하지 않으므로 여러 스레드가 하나의 렌더러 인스턴스를 안전하게 공유할 수 있습니다. .NET의 작업 병렬 라이브러리와 IDisposable on PdfDocument를 함께 사용하면 경쟁 조건이나 메모리 누수 없이 예측 가능한 메모리 동작과 CPU 포화도를 얻을 수 있습니다. 단 몇 줄의 코드로 디렉터리에 있는 모든 HTML 파일을 PDF로 일괄 변환하세요: 지금 바로 NuGet을 사용하여 PDF 만들기를 시작하세요. NuGet 패키지 관리자를 사용하여 IronPDF를 설치하세요. PM > Install-Package IronPdf 다음 코드 조각을 복사하여 실행하세요. using IronPdf; using System.IO; using System.Threading.Tasks; var renderer = new ChromePdfRenderer(); var htmlFiles = Directory.GetFiles("input/", "*.html"); Parallel.ForEach(htmlFiles, htmlFile => { var pdf = renderer.RenderHtmlFileAsPdf(htmlFile); pdf.SaveAs($"output/{Path.GetFileNameWithoutExtension(htmlFile)}.pdf"); }); 실제 운영 환경에서 테스트할 수 있도록 배포하세요. 지금 바로 무료 체험판을 통해 프로젝트에서 IronPDF를 사용해 보세요. 30일 무료 체험 IronPDF를 구매하거나 30일 무료 체험판에 가입한 후, 애플리케이션 시작 부분에 라이선스 키를 입력하세요. IronPdf.License.LicenseKey = "KEY"; IronPdf.License.LicenseKey = "KEY"; $vbLabelText $csharpLabel 지금 바로 무료 체험판을 통해 IronPDF을 프로젝트에서 사용해 보세요. 첫 번째 단계: 무료로 시작하세요 !{--010011000100100101000010010100100100000101010010010110010101111101001110010101010001110100010101010100010111110100100101001110010100110101010001000001010011000100110001001100010111110100001001001100010011110100001101001011--} 목차 문제 이해하기 처리해야 할 PDF 파일이 수천 개에 달할 때 기반 IronPDF 배치 처리 아키텍처 IronPDF 설치 동시성 제어 및 스레드 안전성 대규모 메모리 관리 진행 상황 보고 및 기록 핵심 운영 일반적인 배치 작업 HTML을 PDF로 일괄 변환 PDF 일괄 병합 PDF 일괄 분할 배치 압축 일괄 형식 변환 (PDF/A, PDF/UA) 회복력 탄력적인 배치 파이프라인 구축 오류 처리 및 실패 시 건너뛰기 일시적인 오류에 대한 재시도 로직 충돌 후 재개를 위한 체크포인트 기능 처리 전후 유효성 검사 성능 비동기 및 병렬 처리 패턴 태스크 병렬 라이브러리 통합 동시성 제어(SemaphoreSlim) 비동기/대기 모범 사례 기억력 고갈 방지 배포 배치 작업용 클라우드 배포 지속 가능한 함수를 사용하는 Azure Functions AWS Lambda와 Step Functions Kubernetes 작업 스케줄링 비용 최적화 전략 모든 것을 종합하기 실제 파이프라인 사례 처리해야 할 PDF 파일이 수천 개 있을 때 일괄 PDF 처리는 특정 분야에만 필요한 요구 사항이 아니라 기업 문서 관리의 일상적인 부분입니다. 이러한 상황이 발생하는 시나리오는 모든 산업 분야에서 발생하며, 공통적인 특징은 한 번에 하나씩 일을 처리하는 것이 불가능하다는 것입니다. 문서 마이그레이션 프로젝트는 가장 흔한 원인 중 하나입니다. 조직이 하나의 문서 관리 시스템에서 다른 시스템으로 전환할 때 수천 개(때로는 수백만 개)의 문서를 변환, 재포맷 또는 재태그해야 합니다. 기존의 보험금 청구 시스템에서 새로운 시스템으로 전환하는 보험 회사는 50만 건에 달하는 TIFF 형식의 청구 문서를 검색 가능한 PDF 파일로 변환해야 할 수도 있습니다. 새로운 사건 관리 플랫폼으로 이전하는 로펌은 흩어져 있는 서류들을 통합된 사건 파일로 병합해야 할 수도 있습니다. 이것들은 일회성 작업이지만, 규모가 방대하고 실수를 용납하지 않습니다. 일일 보고서 생성은 동일한 문제의 정상 상태 버전입니다. 수천 명의 고객을 위해 일일 포트폴리오 보고서를 작성하는 금융 기관, 모든 출고 컨테이너에 대한 선적 명세서를 생성하는 물류 회사, 수백 개의 부서에 걸쳐 매일 환자 요약을 작성하는 의료 시스템 등 이 모든 곳에서는 순차 처리가 허용 가능한 시간 범위를 훨씬 초과하는 규모의 PDF 출력물이 생성됩니다. 오전 6시까지 1만 건의 보고서를 준비해야 하는데 데이터가 자정까지 확정되지 않는다면, 보고서를 하나씩 처리하는 데 6시간을 쓸 여유가 없습니다. 기록물 디지털화는 마이그레이션과 규정 준수의 교차점에 있습니다. 수십 년간 종이 기록을 보유해 온 정부 기관, 대학 및 기업들은 표준을 준수하는 형식(일반적으로 PDF/A)으로 문서를 디지털화하고 보관해야 하는 의무에 직면해 있습니다. 그 양은 어마어마합니다. 미국 국립기록보관소(NARA)만 해도 영구 보존을 위해 수백만 페이지에 달하는 연방 기록물을 접수합니다. 따라서 나중에 누락된 부분이 발견되지 않도록 보존 과정이 충분히 신뢰할 수 있어야 합니다. 규정 준수 문제 해결은 종종 가장 시급한 계기가 됩니다. 감사를 통해 문서 보관소가 새로 시행되는 표준을 충족하지 못하는 것으로 드러날 경우(예: 저장된 송장이 전자 송장 발행 규정에 따른 PDF/A-3 형식을 준수하지 않거나, 의료 기록에 섹션 508에서 요구하는 접근성 태그가 없는 경우), 기존 보관소 전체를 새로운 표준에 맞춰 재처리해야 합니다. 압박감은 크고, 마감 기한은 촉박하며, 처리해야 할 작업량은 보관된 데이터의 양에 따라 달라집니다. 이러한 모든 시나리오에서 핵심 과제는 동일합니다. 즉, 대량의 PDF 작업을 안정적이고 효율적으로 처리하면서 메모리 부족 현상이 발생하거나 오류 발생 시 미완성 작업이 남는 일이 없도록 하는 방법입니다. IronPDF 배치 처리 아키텍처 구체적인 작업에 들어가기 전에 IronPDF가 동시 워크로드를 처리하도록 설계된 방식과 그 위에 배치 파이프라인을 구축할 때 어떤 아키텍처적 결정을 내려야 하는지 이해하는 것이 중요합니다. IronPDF 설치 중 NuGet을 통해 IronPDF를 설치하세요: Install-Package IronPdf Install-Package IronPdf SHELL 또는 .NET CLI를 사용하여 다음과 같이 할 수 있습니다. dotnet add package IronPdf dotnet add package IronPdf SHELL IronPDF는 .NET Framework 4.6.2 이상, .NET Core, .NET 5부터 .NET 10까지, 그리고 .NET Standard 2.0을 지원합니다. Windows, Linux, macOS 및 Docker 컨테이너에서 실행되므로 온프레미스 배치 작업과 클라우드 네이티브 배포 모두에 적합합니다. 프로덕션 일괄 처리를 위해서는 PDF 작업이 시작되기 전에 애플리케이션 시작 시 License.LicenseKey를 사용하여 라이선스 키를 설정하십시오. 이를 통해 모든 스레드의 모든 렌더링 호출이 파일별 워터마크 없이 전체 기능 세트에 액세스할 수 있습니다. 동시성 제어 및 스레드 안전성 IronPDF의 Chromium 기반 렌더링 엔진은 스레드 안전성을 보장합니다. 여러 스레드에 걸쳐 여러 개의 ChromePdfRenderer 인스턴스를 생성하거나 단일 인스턴스를 공유할 수 있습니다. IronPDF는 내부 동기화를 처리합니다. 일괄 처리에 대한 공식 권장 사항은 .NET의 내장 기능인 Parallel.ForEach를 사용하는 것입니다. 이 기능은 사용 가능한 모든 CPU 코어에 작업을 자동으로 분산합니다. 즉, "스레드 안전"이라는 말은 "무제한 스레드 사용"을 의미하는 것은 아닙니다. 각 동시 PDF 렌더링 작업은 메모리를 소비합니다(Chromium 엔진은 DOM 구문 분석, CSS 레이아웃 및 이미지 래스터화를 위한 작업 공간이 필요합니다). 메모리가 제한된 시스템에서 너무 많은 병렬 작업을 실행하면 성능이 저하되거나 OutOfMemoryException 오류가 발생할 수 있습니다. 적절한 동시 처리 수준은 하드웨어에 따라 다릅니다. 64GB RAM을 탑재한 16코어 서버는 8~12개의 동시 렌더링을 무리 없이 처리할 수 있습니다. 8GB 메모리를 가진 4코어 VM은 2~4개로 제한될 수 있습니다. 이를 제어하려면 ParallelOptions.MaxDegreeOfParallelism를 사용하세요. 처음에는 사용 가능한 CPU 코어의 절반 정도로 설정한 다음, 메모리 사용량에 따라 조정하세요. 대규모 메모리 관리 일괄 PDF 처리에서 가장 중요한 고려 사항은 메모리 관리입니다. 모든 PdfDocument 객체는 메모리에 PDF의 전체 바이너리 콘텐츠를 저장하며, 이러한 객체를 해제하지 않으면 처리되는 파일 수에 비례하여 메모리 사용량이 증가합니다. 핵심 규칙: 항상 using 문을 사용하거나 PdfDocument 객체에서 Dispose()를 명시적으로 호출하십시오 . IronPDF의 PdfDocument는 IDisposable를 구현하며, 배치 시나리오에서 메모리 문제가 발생하는 가장 일반적인 원인은 해제 실패입니다. 처리 루프의 각 반복에서는 PdfDocument 객체를 생성하고 작업을 수행한 후 해제해야 합니다. 특별한 이유가 있고 이를 처리할 충분한 메모리가 없는 한, PdfDocument 객체를 목록이나 컬렉션에 누적해서는 안 됩니다. 대량 배치 처리를 위한 메모리 관리 전략으로 폐기 외에도 다음과 같은 방법을 고려해 보세요. 모든 데이터를 한 번에 로드하는 대신, 데이터를 부분적으로 처리합니다 . 5만 개의 파일을 처리해야 한다면, 모든 파일을 목록에 나열한 다음 반복 처리하는 대신 100개 또는 500개씩 배치로 처리하여 가비지 컬렉터가 처리 간에 메모리를 회수할 수 있도록 하세요. 매우 큰 배치 처리의 경우 청크 간에 강제로 가비지 컬렉션을 수행합니다 . 일반적으로 GC가 자체적으로 관리하도록 두는 것이 좋지만, 배치 처리는 청크 경계 사이에서 GC.Collect()를 호출하여 메모리 압력이 누적되는 것을 방지할 수 있는 드문 시나리오 중 하나입니다. GC.GetTotalMemory() 또는 프로세스 수준 메트릭을 사용하여 메모리 사용량을 모니터링하세요 . 메모리 사용량이 임계값(예: 사용 가능한 RAM의 80%)을 초과하면 GC가 따라잡을 수 있도록 처리를 일시 중지합니다. 진행 상황 보고 및 기록 일괄 작업 완료에 몇 시간이 걸리는 경우, 작업 진행 상황을 파악하는 것은 선택 사항이 아니라 필수적입니다. 최소한 각 파일의 시작과 완료 시간을 기록하고, 성공/실패 횟수를 추적하며, 남은 예상 시간을 제공해야 합니다. 병렬 작업을 실행할 때는 스레드 안전 카운터를 위해 Interlocked.Increment를 사용하고, 출력 과부하를 방지하기 위해 모든 파일에 로그를 기록하는 대신 50개 또는 100개 파일마다 정기적으로 로그를 기록하십시오. System.Diagnostics.Stopwatch를 사용하여 경과 시간을 추적하고 초당 파일 처리 속도를 계산하여 의미 있는 예상 완료 시간을 확인하세요. 운영 환경의 배치 작업의 경우, 모니터링 대시보드에서 배치 프로세스에 직접 연결하지 않고도 실시간 상태를 표시할 수 있도록 진행 상황을 영구 저장소(데이터베이스, 파일 또는 메시지 큐)에 기록하는 것을 고려하십시오. 일반적인 배치 작업 아키텍처 기반이 마련되었으니, 이제 가장 일반적인 배치 작업과 해당 IronPDF 구현 방식을 살펴보겠습니다. HTML을 PDF로 일괄 변환 HTML을 PDF로 변환하는 작업은 가장 일반적인 일괄 작업입니다. 템플릿을 사용하여 송장을 생성하든, HTML 문서 라이브러리를 PDF로 변환하든, 웹 애플리케이션에서 동적 보고서를 렌더링하든, 패턴은 동일합니다. 입력값을 순회하고, 각각을 렌더링한 다음, 출력을 저장하는 것입니다. 입력 (HTML 파일 5개) INV-2026-001 INV-2026-002 INV-2026-003 INV-2026-004 INV-2026-005 이 구현은 ChromePdfRenderer와 Parallel.ForEach를 사용하여 모든 HTML 파일을 동시에 처리하고, MaxDegreeOfParallelism를 통해 병렬 처리를 제어하여 처리량과 메모리 사용량의 균형을 맞춥니다. 각 파일은 RenderHtmlFileAsPdf로 렌더링되고 출력 디렉터리에 저장되며, 스레드 안전 Interlocked 카운터를 통해 진행 상황이 추적됩니다. :path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-html-to-pdf.cs using IronPdf; using System; using System.IO; using System.Threading.Tasks; using System.Threading; // Configure paths string inputFolder = "input/"; string outputFolder = "output/"; Directory.CreateDirectory(outputFolder); string[] htmlFiles = Directory.GetFiles(inputFolder, "*.html"); Console.WriteLine($"Found {htmlFiles.Length} HTML files to convert"); // Create renderer instance (thread-safe, can be shared) var renderer = new ChromePdfRenderer(); // Track progress int processed = 0; int failed = 0; // Process in parallel with controlled concurrency var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount / 2 }; Parallel.ForEach(htmlFiles, options, htmlFile => { try { string fileName = Path.GetFileNameWithoutExtension(htmlFile); string outputPath = Path.Combine(outputFolder, $"{fileName}.pdf"); using var pdf = renderer.RenderHtmlFileAsPdf(htmlFile); pdf.SaveAs(outputPath); Interlocked.Increment(ref processed); Console.WriteLine($"[OK] {fileName}.pdf"); } catch (Exception ex) { Interlocked.Increment(ref failed); Console.WriteLine($"[ERROR] {Path.GetFileName(htmlFile)}: {ex.Message}"); } }); Console.WriteLine($"\nComplete: {processed} succeeded, {failed} failed"); $vbLabelText $csharpLabel 산출 각 HTML 송장은 해당 PDF 파일로 변환됩니다. 위 이미지는 5개의 배치 출력물 중 하나인 INV-2026-001.pdf를 보여줍니다. 템플릿 기반 생성(예: 송장, 보고서)의 경우 일반적으로 렌더링하기 전에 데이터를 HTML 템플릿에 병합합니다. 이 방법은 간단합니다. HTML 템플릿을 한 번 로드하고, string.Replace를 사용하여 레코드별 데이터(고객 이름, 합계, 날짜)를 삽입한 다음, 채워진 HTML을 병렬 루프 내의 RenderHtmlAsPdf에 전달합니다. IronPDF는 Parallel.ForEach 대신 async/await를 사용하려는 시나리오를 위해 RenderHtmlAsPdfAsync도 제공합니다. 비동기 패턴에 대해서는 뒷부분에서 자세히 다루겠습니다. PDF 일괄 병합 여러 PDF 파일을 하나의 문서로 병합하는 작업은 법률(사건 파일 문서 병합), 금융(월별 명세서를 분기별 보고서로 통합) 및 출판 워크플로에서 흔히 사용됩니다. :path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-merge.cs using IronPdf; using System; using System.IO; using System.Linq; using System.Collections.Generic; string inputFolder = "documents/"; string outputFolder = "merged/"; Directory.CreateDirectory(outputFolder); // Group PDFs by prefix (e.g., "invoice-2026-01-*.pdf" -> one merged file) var pdfFiles = Directory.GetFiles(inputFolder, "*.pdf"); var groups = pdfFiles .GroupBy(f => Path.GetFileName(f).Split('-').Take(3).Aggregate((a, b) => $"{a}-{b}")) .Where(g => g.Count() > 1); Console.WriteLine($"Found {groups.Count()} groups to merge"); foreach (var group in groups) { string groupName = group.Key; var filesToMerge = group.OrderBy(f => f).ToList(); Console.WriteLine($"Merging {filesToMerge.Count} files into {groupName}.pdf"); try { // Load all PDFs for this group var pdfDocs = new List<PdfDocument>(); foreach (string filePath in filesToMerge) { pdfDocs.Add(PdfDocument.FromFile(filePath)); } // Merge all documents using var merged = PdfDocument.Merge(pdfDocs); merged.SaveAs(Path.Combine(outputFolder, $"{groupName}-merged.pdf")); // Dispose source documents foreach (var doc in pdfDocs) { doc.Dispose(); } Console.WriteLine($" [OK] Created {groupName}-merged.pdf ({merged.PageCount} pages)"); } catch (Exception ex) { Console.WriteLine($" [ERROR] {groupName}: {ex.Message}"); } } Console.WriteLine("\nMerge complete"); $vbLabelText $csharpLabel 많은 파일을 병합할 때는 메모리 사용량에 유의하십시오. PdfDocument.Merge 메서드는 모든 소스 문서를 동시에 메모리에 로드합니다. 수백 개의 대용량 PDF 파일을 병합해야 하는 경우, 단계적으로 병합하는 것을 고려해 보세요. 10~20개 파일씩 묶어 중간 문서를 만든 다음, 이 중간 문서들을 병합하는 방식입니다. PDF 일괄 분할 여러 페이지로 구성된 PDF 파일을 개별 페이지(또는 페이지 범위)로 분할하는 것은 병합의 역순입니다. 스캔한 문서 묶음을 개별 기록으로 분리해야 하는 우편물 처리 과정이나, 복합 문서를 분리해야 하는 인쇄 워크플로에서 흔히 사용됩니다. 입력 아래 코드는 병렬 루프에서 CopyPage를 사용하여 개별 페이지를 추출하고 각 페이지에 대해 별도의 PDF 파일을 생성하는 방법을 보여줍니다. 대안으로 SplitByRange 도우미 함수를 사용하면 개별 페이지가 아닌 페이지 범위를 추출하는 방법을 알 수 있으며, 이는 대용량 문서를 더 작은 부분으로 나누는 데 유용합니다. :path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-split.cs using IronPdf; using System; using System.IO; using System.Threading.Tasks; string inputFolder = "multipage/"; string outputFolder = "split/"; Directory.CreateDirectory(outputFolder); string[] pdfFiles = Directory.GetFiles(inputFolder, "*.pdf"); Console.WriteLine($"Found {pdfFiles.Length} PDFs to split"); var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount / 2 }; Parallel.ForEach(pdfFiles, options, pdfFile => { string baseName = Path.GetFileNameWithoutExtension(pdfFile); try { using var pdf = PdfDocument.FromFile(pdfFile); int pageCount = pdf.PageCount; Console.WriteLine($"Splitting {baseName}.pdf ({pageCount} pages)"); // Extract each page as a separate PDF for (int i = 0; i < pageCount; i++) { using var singlePage = pdf.CopyPage(i); string outputPath = Path.Combine(outputFolder, $"{baseName}-page-{i + 1:D3}.pdf"); singlePage.SaveAs(outputPath); } Console.WriteLine($" [OK] Created {pageCount} files from {baseName}.pdf"); } catch (Exception ex) { Console.WriteLine($" [ERROR] {baseName}: {ex.Message}"); } }); // Alternative: Extract page ranges instead of individual pages void SplitByRange(string inputFile, string outputFolder, int pagesPerChunk) { using var pdf = PdfDocument.FromFile(inputFile); string baseName = Path.GetFileNameWithoutExtension(inputFile); int totalPages = pdf.PageCount; int chunkNumber = 1; for (int startPage = 0; startPage < totalPages; startPage += pagesPerChunk) { int endPage = Math.Min(startPage + pagesPerChunk - 1, totalPages - 1); using var chunk = pdf.CopyPages(startPage, endPage); chunk.SaveAs(Path.Combine(outputFolder, $"{baseName}-chunk-{chunkNumber:D3}.pdf")); chunkNumber++; } } Console.WriteLine("\nSplit complete"); $vbLabelText $csharpLabel 산출 2페이지는 별도의 PDF 파일(annual-report-page-2.pdf)로 추출되었습니다. IronPDF의 CopyPage 및 CopyPages 메서드는 지정된 페이지를 포함하는 새로운 PdfDocument 객체를 생성합니다. 저장 후에는 원본 문서와 추출된 각 페이지 문서를 모두 삭제해야 합니다. 배치 압축 저장 비용이 중요한 경우나 대역폭이 제한된 연결을 통해 PDF를 전송해야 하는 경우, 일괄 압축을 사용하면 아카이브 용량을 크게 줄일 수 있습니다. IronPDF는 이미지 품질/크기를 줄이기 위한 CompressImages 방식과 구조적 메타데이터를 제거하기 위한 CompressStructTree 방식, 이렇게 두 가지 압축 방식을 제공합니다. 새로운 CompressAndSaveAs API(버전 2025.12에서 도입)는 여러 최적화 기술을 결합하여 뛰어난 압축률을 제공합니다. :path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-compression.cs using IronPdf; using System; using System.IO; using System.Threading.Tasks; using System.Threading; string inputFolder = "originals/"; string outputFolder = "compressed/"; Directory.CreateDirectory(outputFolder); string[] pdfFiles = Directory.GetFiles(inputFolder, "*.pdf"); Console.WriteLine($"Found {pdfFiles.Length} PDFs to compress"); long totalOriginalSize = 0; long totalCompressedSize = 0; int processed = 0; var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount / 2 }; Parallel.ForEach(pdfFiles, options, pdfFile => { string fileName = Path.GetFileName(pdfFile); string outputPath = Path.Combine(outputFolder, fileName); try { long originalSize = new FileInfo(pdfFile).Length; Interlocked.Add(ref totalOriginalSize, originalSize); using var pdf = PdfDocument.FromFile(pdfFile); // Apply compression with JPEG quality setting (0-100, lower = more compression) pdf.CompressAndSaveAs(outputPath, 60); long compressedSize = new FileInfo(outputPath).Length; Interlocked.Add(ref totalCompressedSize, compressedSize); Interlocked.Increment(ref processed); double reduction = (1 - (double)compressedSize / originalSize) * 100; Console.WriteLine($"[OK] {fileName}: {originalSize / 1024}KB → {compressedSize / 1024}KB ({reduction:F1}% reduction)"); } catch (Exception ex) { Console.WriteLine($"[ERROR] {fileName}: {ex.Message}"); } }); double totalReduction = (1 - (double)totalCompressedSize / totalOriginalSize) * 100; Console.WriteLine($"\nCompression complete:"); Console.WriteLine($" Files processed: {processed}"); Console.WriteLine($" Total original: {totalOriginalSize / 1024 / 1024}MB"); Console.WriteLine($" Total compressed: {totalCompressedSize / 1024 / 1024}MB"); Console.WriteLine($" Overall reduction: {totalReduction:F1}%"); $vbLabelText $csharpLabel 압축과 관련하여 몇 가지 유의할 점이 있습니다. JPEG 품질 설정을 60 미만으로 설정하면 대부분의 이미지에서 눈에 띄는 화질 저하 현상이 발생합니다. ShrinkImage 옵션은 일부 구성에서 왜곡을 일으킬 수 있습니다. 전체 배치를 실행하기 전에 대표 샘플로 테스트하십시오. 구조 트리(CompressStructTree)를 제거하면 압축된 PDF에서 텍스트 선택 및 검색에 영향을 미치므로 이러한 기능이 필요하지 않을 때만 사용하십시오. 일괄 형식 변환(PDF/A, PDF/UA) 기존 아카이브를 표준 규격(장기 보관용 PDF/A 또는 접근성 향상용 PDF/UA)으로 변환하는 것은 가장 가치 있는 일괄 작업 중 하나입니다. IronPDF는 모든 PDF/A 버전(버전 2025.11에 추가된 PDF/A-4 포함)과 PDF/UA 규격 준수(버전 2025.12에 추가된 PDF/UA-2 포함)를 지원합니다. 입력 이 예제는 각 PDF 파일을 PdfDocument.FromFile로 로드한 다음, SaveAsPdfA와 PdfAVersions.PdfA3b 매개변수를 사용하여 PDF/A-3b로 변환합니다. 대안적인 ConvertToPdfUA 함수는 SaveAsPdfUA를 사용하여 접근성 규정 준수 변환을 보여주지만, PDF/UA는 적절한 구조적 태그가 있는 원본 문서를 필요로 합니다. :path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-format-conversion.cs using IronPdf; using System; using System.IO; using System.Threading.Tasks; using System.Threading; string inputFolder = "originals/"; string outputFolder = "pdfa-archive/"; Directory.CreateDirectory(outputFolder); string[] pdfFiles = Directory.GetFiles(inputFolder, "*.pdf"); Console.WriteLine($"Found {pdfFiles.Length} PDFs to convert to PDF/A-3b"); int converted = 0; int failed = 0; var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount / 2 }; Parallel.ForEach(pdfFiles, options, pdfFile => { string fileName = Path.GetFileName(pdfFile); string outputPath = Path.Combine(outputFolder, fileName); try { using var pdf = PdfDocument.FromFile(pdfFile); // Convert to PDF/A-3b for long-term archival pdf.SaveAsPdfA(outputPath, PdfAVersions.PdfA3b); Interlocked.Increment(ref converted); Console.WriteLine($"[OK] {fileName} → PDF/A-3b"); } catch (Exception ex) { Interlocked.Increment(ref failed); Console.WriteLine($"[ERROR] {fileName}: {ex.Message}"); } }); Console.WriteLine($"\nConversion complete: {converted} succeeded, {failed} failed"); // Alternative: Convert to PDF/UA for accessibility compliance void ConvertToPdfUA(string inputFolder, string outputFolder) { Directory.CreateDirectory(outputFolder); string[] files = Directory.GetFiles(inputFolder, "*.pdf"); Parallel.ForEach(files, pdfFile => { string fileName = Path.GetFileName(pdfFile); try { using var pdf = PdfDocument.FromFile(pdfFile); // PDF/UA requires proper tagging - ensure source is well-structured pdf.SaveAsPdfUA(Path.Combine(outputFolder, fileName)); Console.WriteLine($"[OK] {fileName} → PDF/UA"); } catch (Exception ex) { Console.WriteLine($"[ERROR] {fileName}: {ex.Message}"); } }); } $vbLabelText $csharpLabel 산출 생성된 PDF 파일은 외관상 바이트 단위로 원본과 동일하지만, 아카이빙 시스템을 위한 PDF/A-3b 규격 준수 메타데이터를 포함하고 있습니다. 형식 변환은 특히 조직이 기존 아카이브가 규제 표준을 충족하지 못한다는 사실을 발견하는 규정 준수 개선 프로젝트에서 매우 중요합니다. 일괄 처리 패턴은 간단하지만 유효성 검사 단계가 매우 중요합니다. 변환이 완료된 것으로 간주하기 전에 각 파일이 실제로 규정 준수 검사를 통과했는지 항상 확인해야 합니다. 검증에 대한 자세한 내용은 아래의 복원력 섹션에서 다룹니다. 탄력적인 배치 파이프라인 구축 100개의 파일에서는 완벽하게 작동하지만 50,000개 파일 중 4,327번째 파일에서 오류가 발생하는 배치 파이프라인은 유용하지 않습니다. 복원력, 즉 오류를 적절하게 처리하고, 일시적인 오류를 재시도하며, 충돌 후 재개하는 능력은 프로덕션 수준의 파이프라인과 프로토타입을 구분하는 핵심 요소입니다. 오류 처리 및 실패 시 건너뛰기 가장 기본적인 복원력 패턴은 실패 시 건너뛰기입니다. 즉, 단일 파일 처리에 실패하면 오류를 기록하고 전체 배치를 중단하는 대신 다음 파일로 계속 진행합니다. 당연하게 들리지만, Parallel.ForEach를 사용할 때는 의외로 놓치기 쉽습니다. 병렬 작업에서 처리되지 않은 예외는 AggregateException로 전파되어 루프가 종료됩니다. 다음 예제는 실패 시 건너뛰기 및 재시도 로직을 함께 보여줍니다. 각 파일을 try-catch 블록으로 감싸서 오류를 적절하게 처리하고, 내부 재시도 루프에서 지수 백오프를 사용하여 IOException 및 OutOfMemoryException와 같은 일시적인 예외를 처리합니다. :path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-error-handling-retry.cs using IronPdf; using System; using System.IO; using System.Threading.Tasks; using System.Threading; using System.Collections.Concurrent; string inputFolder = "input/"; string outputFolder = "output/"; string errorLogPath = "error-log.txt"; Directory.CreateDirectory(outputFolder); string[] htmlFiles = Directory.GetFiles(inputFolder, "*.html"); var renderer = new ChromePdfRenderer(); var errorLog = new ConcurrentBag<string>(); int processed = 0; int failed = 0; int retried = 0; const int maxRetries = 3; var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount / 2 }; Parallel.ForEach(htmlFiles, options, htmlFile => { string fileName = Path.GetFileNameWithoutExtension(htmlFile); string outputPath = Path.Combine(outputFolder, $"{fileName}.pdf"); int attempt = 0; bool success = false; while (attempt < maxRetries && !success) { attempt++; try { using var pdf = renderer.RenderHtmlFileAsPdf(htmlFile); pdf.SaveAs(outputPath); success = true; Interlocked.Increment(ref processed); if (attempt > 1) { Interlocked.Increment(ref retried); Console.WriteLine($"[OK] {fileName}.pdf (succeeded on attempt {attempt})"); } else { Console.WriteLine($"[OK] {fileName}.pdf"); } } catch (Exception ex) when (IsTransientException(ex) && attempt < maxRetries) { // Transient error - wait and retry with exponential backoff int delayMs = (int)Math.Pow(2, attempt) * 500; Console.WriteLine($"[RETRY] {fileName}: {ex.Message} (attempt {attempt}, waiting {delayMs}ms)"); Thread.Sleep(delayMs); } catch (Exception ex) { // Non-transient error or max retries exceeded Interlocked.Increment(ref failed); string errorMessage = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | {fileName} | {ex.GetType().Name} | {ex.Message}"; errorLog.Add(errorMessage); Console.WriteLine($"[ERROR] {fileName}: {ex.Message}"); } } }); // Write error log if (errorLog.Count > 0) { File.WriteAllLines(errorLogPath, errorLog); } Console.WriteLine($"\nBatch complete:"); Console.WriteLine($" Processed: {processed}"); Console.WriteLine($" Failed: {failed}"); Console.WriteLine($" Retried: {retried}"); if (failed > 0) { Console.WriteLine($" Error log: {errorLogPath}"); } // Helper to identify transient exceptions worth retrying bool IsTransientException(Exception ex) { return ex is IOException || ex is OutOfMemoryException || ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) || ex.Message.Contains("locked", StringComparison.OrdinalIgnoreCase); } $vbLabelText $csharpLabel 일괄 처리가 완료되면 오류 로그를 검토하여 어떤 파일에서 오류가 발생했는지, 그리고 그 이유는 무엇인지 파악하십시오. 일반적인 오류 원인으로는 손상된 소스 파일, 암호로 보호된 PDF, 소스 콘텐츠의 지원되지 않는 기능, 매우 큰 문서에서의 메모리 부족 등이 있습니다. 일시적 오류에 대한 재시도 로직 일부 실패는 일시적인 것이므로 다시 시도하면 성공할 수 있습니다. 여기에는 파일 시스템 경합(다른 프로세스가 파일을 잠근 상태), 일시적인 메모리 부족(가비지 컬렉션이 아직 처리하지 못한 상태), HTML 콘텐츠에서 외부 리소스를 로드할 때 발생하는 네트워크 시간 초과 등이 포함됩니다. 위의 코드 예제는 지수 백오프를 사용하여 이러한 상황을 처리합니다. 즉, 짧은 지연 시간으로 시작하여 재시도할 때마다 지연 시간을 두 배로 늘리고 최대 재시도 횟수(일반적으로 3회)로 제한합니다. 핵심은 재시도 가능한 실패와 재시도 불가능한 실패를 구분하는 것입니다. IOException (파일 잠김) 또는 OutOfMemoryException (일시적인 부하) 오류가 발생하면 다시 시도해 볼 가치가 있습니다. ArgumentException (잘못된 입력) 또는 지속적인 렌더링 오류가 발생하는 경우, 다시 시도해도 도움이 되지 않으며 시간과 리소스만 낭비하게 됩니다. 충돌 후 재개를 위한 체크포인트 설정 배치 작업이 몇 시간에 걸쳐 5만 개의 파일을 처리할 때, 3만 5천 번째 파일에서 오류가 발생하더라도 처음부터 다시 시작해야 하는 것은 바람직하지 않습니다. 체크포인트 기능(어떤 파일이 성공적으로 처리되었는지 기록하는 기능)을 사용하면 중단했던 부분부터 다시 작업을 재개할 수 있습니다. :path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-checkpointing.cs using IronPdf; using System; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Threading; using System.Collections.Generic; string inputFolder = "input/"; string outputFolder = "output/"; string checkpointPath = "checkpoint.txt"; string errorLogPath = "errors.txt"; Directory.CreateDirectory(outputFolder); // Load checkpoint - files already processed successfully var completedFiles = new HashSet<string>(); if (File.Exists(checkpointPath)) { completedFiles = new HashSet<string>(File.ReadAllLines(checkpointPath)); Console.WriteLine($"Resuming from checkpoint: {completedFiles.Count} files already processed"); } // Get files to process (excluding already completed) string[] allFiles = Directory.GetFiles(inputFolder, "*.html"); string[] filesToProcess = allFiles .Where(f => !completedFiles.Contains(Path.GetFileName(f))) .ToArray(); Console.WriteLine($"Files to process: {filesToProcess.Length} (skipping {completedFiles.Count} already done)"); var renderer = new ChromePdfRenderer(); var checkpointLock = new object(); int processed = 0; int failed = 0; var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount / 2 }; Parallel.ForEach(filesToProcess, options, htmlFile => { string fileName = Path.GetFileName(htmlFile); string baseName = Path.GetFileNameWithoutExtension(htmlFile); string outputPath = Path.Combine(outputFolder, $"{baseName}.pdf"); try { using var pdf = renderer.RenderHtmlFileAsPdf(htmlFile); pdf.SaveAs(outputPath); // Record success in checkpoint (thread-safe) lock (checkpointLock) { File.AppendAllText(checkpointPath, fileName + Environment.NewLine); } Interlocked.Increment(ref processed); Console.WriteLine($"[OK] {baseName}.pdf"); } catch (Exception ex) { Interlocked.Increment(ref failed); // Log error for review lock (checkpointLock) { File.AppendAllText(errorLogPath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | {fileName} | {ex.Message}{Environment.NewLine}"); } Console.WriteLine($"[ERROR] {fileName}: {ex.Message}"); } }); Console.WriteLine($"\nBatch complete:"); Console.WriteLine($" Newly processed: {processed}"); Console.WriteLine($" Failed: {failed}"); Console.WriteLine($" Total completed: {completedFiles.Count + processed}"); Console.WriteLine($" Checkpoint saved to: {checkpointPath}"); $vbLabelText $csharpLabel 체크포인트 파일은 완료된 작업에 대한 영구적인 기록 역할을 합니다. 파이프라인이 시작되면 체크포인트 파일을 읽고 이미 성공적으로 처리된 파일은 건너뜁니다. 파일 처리가 완료되면 해당 파일의 경로가 체크포인트 파일에 추가됩니다. 이 방식은 간단하고 파일 기반이며 외부 종속성이 필요하지 않습니다. 보다 복잡한 시나리오의 경우, 특히 여러 워커가 서로 다른 머신에서 파일을 병렬로 처리하는 경우 데이터베이스 테이블이나 분산 캐시(예: Redis)를 체크포인트 저장소로 사용하는 것을 고려해 보세요. 처리 전후 유효성 검사 검증은 탄력적인 파이프라인의 시작과 끝을 장식하는 핵심 요소입니다. 전처리 검증은 처리 시간 낭비를 막기 위해 문제가 있는 입력값을 잡아내고, 후처리 검증은 결과물이 품질 및 규정 준수 요건을 충족하는지 확인합니다. 입력 이 구현에서는 처리 루프를 PreValidate 및 PostValidate 헬퍼 함수로 감쌉니다. 사전 유효성 검사는 처리 전에 파일 크기, 콘텐츠 유형 및 기본 HTML 구조를 확인합니다. 사후 검증을 통해 출력 PDF의 페이지 수가 유효하고 파일 크기가 적절한지 확인하고, 검증에 성공한 파일은 별도의 폴더로 이동시키고, 실패한 파일은 수동 검토를 위해 거부 폴더로 보냅니다. :path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-validation.cs using IronPdf; using System; using System.IO; using System.Threading.Tasks; using System.Threading; using System.Collections.Concurrent; string inputFolder = "input/"; string outputFolder = "output/"; string validatedFolder = "validated/"; string rejectedFolder = "rejected/"; Directory.CreateDirectory(outputFolder); Directory.CreateDirectory(validatedFolder); Directory.CreateDirectory(rejectedFolder); string[] inputFiles = Directory.GetFiles(inputFolder, "*.html"); var renderer = new ChromePdfRenderer(); int preValidationFailed = 0; int processingFailed = 0; int postValidationFailed = 0; int succeeded = 0; var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount / 2 }; Parallel.ForEach(inputFiles, options, inputFile => { string fileName = Path.GetFileNameWithoutExtension(inputFile); string outputPath = Path.Combine(outputFolder, $"{fileName}.pdf"); // Pre-validation: Check input file if (!PreValidate(inputFile)) { Interlocked.Increment(ref preValidationFailed); Console.WriteLine($"[SKIP] {fileName}: Failed pre-validation"); return; } try { // Process using var pdf = renderer.RenderHtmlFileAsPdf(inputFile); pdf.SaveAs(outputPath); // Post-validation: Check output file if (PostValidate(outputPath)) { // Move to validated folder string validatedPath = Path.Combine(validatedFolder, $"{fileName}.pdf"); File.Move(outputPath, validatedPath, overwrite: true); Interlocked.Increment(ref succeeded); Console.WriteLine($"[OK] {fileName}.pdf (validated)"); } else { // Move to rejected folder for manual review string rejectedPath = Path.Combine(rejectedFolder, $"{fileName}.pdf"); File.Move(outputPath, rejectedPath, overwrite: true); Interlocked.Increment(ref postValidationFailed); Console.WriteLine($"[REJECT] {fileName}.pdf: Failed post-validation"); } } catch (Exception ex) { Interlocked.Increment(ref processingFailed); Console.WriteLine($"[ERROR] {fileName}: {ex.Message}"); } }); Console.WriteLine($"\nValidation summary:"); Console.WriteLine($" Succeeded: {succeeded}"); Console.WriteLine($" Pre-validation failed: {preValidationFailed}"); Console.WriteLine($" Processing failed: {processingFailed}"); Console.WriteLine($" Post-validation failed: {postValidationFailed}"); // Pre-validation: Quick checks on input file bool PreValidate(string filePath) { try { var fileInfo = new FileInfo(filePath); // Check file exists and is readable if (!fileInfo.Exists) return false; // Check file is not empty if (fileInfo.Length == 0) return false; // Check file is not too large (e.g., 50MB limit) if (fileInfo.Length > 50 * 1024 * 1024) return false; // Quick content check - must be valid HTML string content = File.ReadAllText(filePath); if (string.IsNullOrWhiteSpace(content)) return false; if (!content.Contains("<html", StringComparison.OrdinalIgnoreCase) && !content.Contains("<!DOCTYPE", StringComparison.OrdinalIgnoreCase)) { return false; } return true; } catch { return false; } } // Post-validation: Verify output PDF meets requirements bool PostValidate(string pdfPath) { try { using var pdf = PdfDocument.FromFile(pdfPath); // Check PDF has at least one page if (pdf.PageCount < 1) return false; // Check file size is reasonable (not just header, not corrupted) var fileInfo = new FileInfo(pdfPath); if (fileInfo.Length < 1024) return false; return true; } catch { return false; } } $vbLabelText $csharpLabel 산출 5개 파일 모두 유효성 검사를 통과하여 유효성 검사 완료 폴더로 이동되었습니다. 전처리 유효성 검사는 빨라야 합니다. 명백히 잘못된 입력값을 확인하는 것이지, 전체 처리를 수행하는 것이 아닙니다. 특히 출력물이 특정 표준(PDF/A, PDF/UA)을 통과해야 하는 규정 준수 변환의 경우, 사후 처리 검증을 더욱 철저하게 수행할 수 있습니다. 사후 처리 유효성 검사에 실패한 파일은 그대로 승인하는 대신 수동 검토 대상으로 표시해야 합니다. 비동기 및 병렬 처리 패턴 IronPDF는 스레드 기반 병렬 처리와 async/await(비동기 I/O)를 모두 지원합니다. 각 기능을 언제 사용해야 하는지, 그리고 어떻게 효과적으로 조합해야 하는지를 이해하는 것이 처리량을 극대화하는 데 핵심입니다. 태스크 병렬 라이브러리 통합 Parallel.ForEach는 CPU 집약적인 배치 작업에 대한 가장 간단하고 효과적인 접근 방식입니다. IronPDF의 렌더링 엔진은 CPU 집약적입니다(HTML 구문 분석, CSS 레이아웃, 이미지 래스터화). Parallel.ForEach는 이 작업을 사용 가능한 모든 코어에 자동으로 분산합니다. :path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-tpl.cs using IronPdf; using System; using System.IO; using System.Threading.Tasks; using System.Threading; using System.Diagnostics; string inputFolder = "input/"; string outputFolder = "output/"; Directory.CreateDirectory(outputFolder); string[] htmlFiles = Directory.GetFiles(inputFolder, "*.html"); var renderer = new ChromePdfRenderer(); Console.WriteLine($"Processing {htmlFiles.Length} files with {Environment.ProcessorCount} CPU cores"); int processed = 0; var stopwatch = Stopwatch.StartNew(); // Configure parallelism based on system resources // Rule of thumb: ProcessorCount / 2 for memory-intensive operations var options = new ParallelOptions { MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) }; Console.WriteLine($"Max parallelism: {options.MaxDegreeOfParallelism}"); // Use Parallel.ForEach for CPU-bound batch operations Parallel.ForEach(htmlFiles, options, htmlFile => { string fileName = Path.GetFileNameWithoutExtension(htmlFile); string outputPath = Path.Combine(outputFolder, $"{fileName}.pdf"); try { // Render HTML to PDF using var pdf = renderer.RenderHtmlFileAsPdf(htmlFile); pdf.SaveAs(outputPath); int current = Interlocked.Increment(ref processed); // Progress reporting every 10 files if (current % 10 == 0) { double elapsed = stopwatch.Elapsed.TotalSeconds; double rate = current / elapsed; double remaining = (htmlFiles.Length - current) / rate; Console.WriteLine($"Progress: {current}/{htmlFiles.Length} ({rate:F1} files/sec, ~{remaining:F0}s remaining)"); } } catch (Exception ex) { Console.WriteLine($"[ERROR] {fileName}: {ex.Message}"); } }); stopwatch.Stop(); double totalRate = processed / stopwatch.Elapsed.TotalSeconds; Console.WriteLine($"\nComplete:"); Console.WriteLine($" Files processed: {processed}/{htmlFiles.Length}"); Console.WriteLine($" Total time: {stopwatch.Elapsed.TotalSeconds:F1}s"); Console.WriteLine($" Average rate: {totalRate:F1} files/sec"); Console.WriteLine($" Time per file: {stopwatch.Elapsed.TotalMilliseconds / processed:F0}ms"); // Memory monitoring helper (call between chunks for large batches) void CheckMemoryPressure() { const long memoryThreshold = 4L * 1024 * 1024 * 1024; // 4 GB long currentMemory = GC.GetTotalMemory(forceFullCollection: false); if (currentMemory > memoryThreshold) { Console.WriteLine($"Memory pressure detected ({currentMemory / 1024 / 1024}MB), forcing GC..."); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); } } $vbLabelText $csharpLabel MaxDegreeOfParallelism 옵션은 매우 중요합니다. 이 설정이 없으면 TPL은 사용 가능한 모든 코어를 사용하려고 시도하여 각 렌더링 작업이 리소스 집약적일 경우 메모리 과부하를 초래할 수 있습니다. 이 값은 시스템에서 사용 가능한 RAM을 일반적인 렌더링당 메모리 사용량(복잡한 HTML의 경우 동시 렌더링당 보통 100~300MB)으로 나눈 값으로 설정하십시오. 동시성 제어(SemaphoreSlim) Parallel.ForEach에서 제공하는 것보다 더 세밀한 동시성 제어가 필요한 경우(예: 비동기 I/O와 CPU 집약적 렌더링을 혼합하는 경우) SemaphoreSlim를 사용하면 동시에 실행되는 작업 수를 명시적으로 제어할 수 있습니다. 패턴은 간단합니다. 원하는 동시 실행 제한(예: 동시 렌더링 4개)을 지정하여 SemaphoreSlim를 생성하고, 각 렌더링 전에 WaitAsync를 호출하고, 그 후 finally 블록 내에서 Release를 호출합니다. 그런 다음 Task.WhenAll를 사용하여 모든 작업을 시작하십시오. 이 패턴은 파이프라인에 I/O 중심 단계(블롭 스토리지에서 파일 읽기, 데이터베이스에 결과 쓰기)와 CPU 중심 단계(PDF 렌더링)가 모두 포함된 경우 특히 유용합니다. 세마포어는 CPU 집약적인 렌더링 동시성을 제한하는 동시에 I/O 집약적인 단계는 속도 저하 없이 진행될 수 있도록 합니다. Async/await 모범 사례 IronPDF는 RenderHtmlAsPdfAsync, RenderUrlAsPdfAsync, RenderHtmlFileAsPdfAsync를 포함한 비동기 렌더링 방식을 제공합니다. 이러한 방식은 웹 애플리케이션(요청 스레드 차단이 허용되지 않는 경우) 및 PDF 렌더링과 비동기 I/O 작업을 혼합하는 파이프라인에 이상적입니다. 일괄 처리를 위한 몇 가지 중요한 비동기 모범 사례: 동기식 IronPDF 메서드를 래핑하기 위해 Task.Run를 사용하지 마십시오 . 대신 기본 제공되는 비동기식 메서드를 사용하십시오. 동기 메서드를 Task.Run로 감싸는 것은 스레드 풀 스레드를 낭비하고 아무런 이점 없이 오버헤드를 추가합니다. 비동기 작업에서 .Result 또는 .Wait()를 사용하지 마십시오. 이는 호출 스레드를 차단하여 UI 또는 ASP.NET 컨텍스트에서 교착 상태를 유발할 수 있습니다. 항상 await를 사용하세요. 모든 작업을 한 번에 기다리는 대신 Task.WhenAll 호출을 일괄 처리하세요 . 만약 10,000개의 작업이 있고 그 모든 작업에 대해 동시에 Task.WhenAll를 호출하면 10,000개의 동시 작업이 실행됩니다. 대신 .Chunk(10) 또는 이와 유사한 방식을 사용하여 그룹별로 처리하고 각 그룹을 순차적으로 기다리십시오. 메모리 고갈 방지 메모리 부족은 일괄 PDF 처리에서 가장 흔한 오류 원인입니다. 방어적인 접근 방식은 각 렌더링 전에 GC.GetTotalMemory()를 사용하여 메모리 사용량을 모니터링하고 사용량이 임계값(예: 4GB 또는 사용 가능한 RAM의 80%)을 초과하면 수집을 트리거하는 것입니다. 계속 진행하기 전에 가능한 한 많은 메모리를 확보하려면 GC.Collect()를 호출한 다음 GC.WaitForPendingFinalizers()를 호출하고 두 번째로 GC.Collect()를 호출하십시오. 이렇게 하면 약간의 지연이 발생하지만, 파일 번호 30,000에서 OutOfMemoryException 오류가 발생하여 전체 배치 작업이 중단되는 치명적인 상황을 방지할 수 있습니다. TPL 섹션의 MaxDegreeOfParallelism 스로틀링 및 메모리 관리 섹션의 using 해제 패턴을 결합하면 동시 실행 제한, 적극적인 해제, 안전 밸브를 통한 모니터링이라는 3단계 메모리 문제 방어 체계를 구축할 수 있습니다. 배치 작업을 위한 클라우드 배포 최근 배치 처리는 점점 더 클라우드에서 실행되고 있으며, 클라우드에서는 워크로드 요구에 맞춰 컴퓨팅 리소스를 확장하고 사용한 만큼만 비용을 지불할 수 있습니다. IronPDF는 모든 주요 클라우드 플랫폼에서 실행됩니다. 각 플랫폼에 맞는 배치 파이프라인 아키텍처를 설계하는 방법을 알아보세요. Azure Functions와 Durable Functions Azure Durable Functions는 팬아웃/팬인 패턴에 대한 기본 오케스트레이션 기능을 제공하므로 PDF 일괄 처리에 적합합니다. 오케스트레이터 기능은 여러 액티비티 기능 인스턴스에 작업을 분산시키며, 각 인스턴스는 파일의 하위 집합을 처리합니다. 오케스트레이터는 팬아웃 루프에서 CallActivityAsync를 호출하고, 각 액티비티 함수는 ChromePdfRenderer를 인스턴스화하고, 파일 청크를 처리하고, 오케스트레이터는 결과를 수집합니다. Azure Functions 사용 시 주요 고려 사항: 기본 사용 계획은 함수 호출당 5분의 시간 제한이 있으며 메모리 사용량이 제한됩니다. 일괄 처리를 위해서는 더 긴 타임아웃과 더 많은 메모리를 지원하는 프리미엄 또는 전용 플랜을 사용하십시오. IronPDF는 전체 .NET 런타임(축약되지 않은)을 필요로 하므로, 함수 앱이 적절한 런타임 식별자를 사용하여 .NET 8 이상용으로 구성되어 있는지 확인하십시오. AWS Lambda와 Step Functions를 함께 사용 AWS Step Functions는 Azure Durable Functions와 유사한 오케스트레이션 기능을 제공합니다. 상태 머신의 각 단계는 파일 덩어리를 처리하는 Lambda 함수를 호출합니다. Lambda 핸들러는 S3 객체 키 배치를 수신하고, 각 PDF 파일에 PdfDocument.FromFile를 로드하고, 처리 파이프라인(압축, 형식 변환 등)을 적용한 후, 결과를 출력 S3 버킷에 다시 기록합니다. AWS Lambda는 최대 실행 시간이 15분이며 저장 공간이 제한되어 있습니다(기본값 512MB, 최대 10GB까지 구성 가능). 대규모 배치 작업의 경우 Step Functions를 사용하여 워크로드를 여러 부분으로 나누고 각 부분을 별도의 Lambda 호출에서 처리하세요. 중간 결과를 로컬 저장소 대신 S3에 저장하세요. Kubernetes 작업 스케줄링 자체 Kubernetes 클러스터를 운영하는 조직의 경우, 배치 PDF 처리는 Kubernetes 작업 및 크론 작업에 적합합니다. 각 파드는 큐(Azure Service Bus, RabbitMQ 또는 SQS)에서 파일을 가져와 IronPDF로 처리한 다음 결과를 객체 스토리지에 기록하는 배치 워커를 실행합니다. 작업자 루프는 이전 섹션에서 다룬 것과 동일한 패턴을 따릅니다. 메시지를 큐에서 꺼내고, ChromePdfRenderer.RenderHtmlAsPdf() 또는 PdfDocument.FromFile()를 사용하여 문서를 처리하고, 결과를 업로드하고, 메시지를 확인합니다. 동일한 try-catch에 복원력 패턴의 재시도 로직을 사용하여 처리를 래핑하고 SemaphoreSlim를 사용하여 Pod별 동시성을 제어합니다. IronPDF는 공식 Docker 지원을 제공하며 Linux 컨테이너에서 실행됩니다. 컨테이너 운영 체제에 맞는 네이티브 런타임 패키지와 함께 IronPdf NuGet 패키지를 사용하십시오(예: Linux 기반 이미지의 경우 IronPdf.Linux). Kubernetes의 경우 IronPDF의 메모리 요구 사항(일반적으로 동시 실행량에 따라 Pod당 512MB~2GB)에 맞는 리소스 요청 및 제한을 정의하십시오. Horizontal Pod Autoscaler는 큐 깊이에 따라 워커를 확장할 수 있으며, 체크포인트 패턴을 통해 파드가 제거되더라도 작업 손실이 발생하지 않도록 보장합니다. 비용 최적화 전략 클라우드 배치 처리는 리소스 할당을 신중하게 고려하지 않으면 비용이 많이 들 수 있습니다. 다음은 가장 큰 효과를 가져오는 전략들입니다. 컴퓨팅 리소스를 적절하게 조정하세요. PDF 렌더링은 GPU 리소스 사용량이 많은 작업이 아니라 CPU와 메모리 리소스를 많이 사용하는 작업입니다. 범용 또는 메모리 최적화 인스턴스 대신 컴퓨팅 최적화 인스턴스(Azure의 C 시리즈, AWS의 C 유형)를 사용하세요. 가격 대비 렌더링 효율이 더 좋아질 겁니다. 중단을 허용할 수 있는 배치 워크로드에는 스팟/선점 가능 인스턴스를 사용하십시오 . 일괄 PDF 처리는 체크포인트 기능 덕분에 본질적으로 작업 재개가 가능하므로, 일반적으로 주문형 처리보다 60~90% 할인된 가격으로 제공되는 스팟 가격 책정에 이상적인 솔루션입니다. 일정이 허락한다면 , 혼잡하지 않은 시간대에 진행하세요 . 많은 클라우드 서비스 제공업체는 야간 및 주말에 더 낮은 가격이나 더 높은 서비스 가용성을 제공합니다. 미리 압축하고 한 번만 저장하세요. 압축을 별도의 단계로 실행하는 대신 처리 파이프라인의 일부로 실행하십시오. 처음부터 압축된 PDF를 저장하면 아카이브 수명 동안의 지속적인 저장 비용을 절감할 수 있습니다. 저장 공간을 계층화하세요. 자주 액세스하는 처리된 PDF 파일은 자주 사용되는 저장 공간(핫 스토리지)에 저장해야 합니다. 자주 액세스하지 않는 PDF 파일은 쿨 티어 또는 아카이브 티어(Azure Cool/Archive, AWS S3 Glacier)로 이동해야 합니다. 이것만으로도 저장 비용을 50~80% 절감할 수 있습니다. 실제 파이프라인 사례 이제 수집 → 검증 → 처리 → 보관 → 보고의 전체 워크플로를 보여주는 완벽한 프로덕션급 배치 파이프라인으로 모든 것을 통합해 보겠습니다. 이 예제는 HTML 송장 템플릿 디렉토리를 처리하고, 이를 PDF로 렌더링하고, 출력을 압축하고, 보관 규정 준수를 위해 PDF/A-3b로 변환하고, 결과를 검증하고, 마지막으로 요약 보고서를 생성합니다. 위의 일괄 변환 예시에서 사용한 것과 동일한 5개의 HTML 송장을 사용하여... :path=/static-assets/pdf/content-code-examples/tutorials/batch-pdf-processing-csharp/batch-processing-full-pipeline.cs using IronPdf; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Threading; using System.Collections.Concurrent; using System.Diagnostics; using System.Text.Json; // Configuration var config = new PipelineConfig { InputFolder = "input/", OutputFolder = "output/", ArchiveFolder = "archive/", ErrorFolder = "errors/", CheckpointPath = "pipeline-checkpoint.json", ReportPath = "pipeline-report.json", MaxConcurrency = Math.Max(1, Environment.ProcessorCount / 2), MaxRetries = 3, JpegQuality = 70 }; // Initialize folders Directory.CreateDirectory(config.OutputFolder); Directory.CreateDirectory(config.ArchiveFolder); Directory.CreateDirectory(config.ErrorFolder); // Load checkpoint for resume capability var checkpoint = LoadCheckpoint(config.CheckpointPath); var results = new ConcurrentBag<ProcessingResult>(); var stopwatch = Stopwatch.StartNew(); // Get files to process string[] allFiles = Directory.GetFiles(config.InputFolder, "*.html"); string[] filesToProcess = allFiles .Where(f => !checkpoint.CompletedFiles.Contains(Path.GetFileName(f))) .ToArray(); Console.WriteLine($"Pipeline starting:"); Console.WriteLine($" Total files: {allFiles.Length}"); Console.WriteLine($" Already processed: {checkpoint.CompletedFiles.Count}"); Console.WriteLine($" To process: {filesToProcess.Length}"); Console.WriteLine($" Concurrency: {config.MaxConcurrency}"); var renderer = new ChromePdfRenderer(); var checkpointLock = new object(); var options = new ParallelOptions { MaxDegreeOfParallelism = config.MaxConcurrency }; Parallel.ForEach(filesToProcess, options, inputFile => { var result = new ProcessingResult { FileName = Path.GetFileName(inputFile), StartTime = DateTime.UtcNow }; try { // Stage: Pre-validation if (!ValidateInput(inputFile)) { result.Status = "PreValidationFailed"; result.Error = "Input file failed validation"; results.Add(result); return; } string baseName = Path.GetFileNameWithoutExtension(inputFile); string tempPath = Path.Combine(config.OutputFolder, $"{baseName}.pdf"); string archivePath = Path.Combine(config.ArchiveFolder, $"{baseName}.pdf"); // Stage: Process with retry PdfDocument pdf = null; int attempt = 0; bool success = false; while (attempt < config.MaxRetries && !success) { attempt++; try { pdf = renderer.RenderHtmlFileAsPdf(inputFile); success = true; } catch (Exception ex) when (IsTransient(ex) && attempt < config.MaxRetries) { Thread.Sleep((int)Math.Pow(2, attempt) * 500); } } if (!success || pdf == null) { result.Status = "ProcessingFailed"; result.Error = "Max retries exceeded"; results.Add(result); return; } using (pdf) { // Stage: Compress and convert to PDF/A-3b for archival pdf.SaveAsPdfA(tempPath, PdfAVersions.PdfA3b); } // Stage: Post-validation if (!ValidateOutput(tempPath)) { File.Move(tempPath, Path.Combine(config.ErrorFolder, $"{baseName}.pdf"), overwrite: true); result.Status = "PostValidationFailed"; result.Error = "Output file failed validation"; results.Add(result); return; } // Stage: Archive File.Move(tempPath, archivePath, overwrite: true); // Update checkpoint lock (checkpointLock) { checkpoint.CompletedFiles.Add(result.FileName); SaveCheckpoint(config.CheckpointPath, checkpoint); } result.Status = "Success"; result.OutputSize = new FileInfo(archivePath).Length; result.EndTime = DateTime.UtcNow; results.Add(result); Console.WriteLine($"[OK] {baseName}.pdf ({result.OutputSize / 1024}KB)"); } catch (Exception ex) { result.Status = "Error"; result.Error = ex.Message; result.EndTime = DateTime.UtcNow; results.Add(result); Console.WriteLine($"[ERROR] {result.FileName}: {ex.Message}"); } }); stopwatch.Stop(); // Generate report var report = new PipelineReport { TotalFiles = allFiles.Length, ProcessedThisRun = results.Count, Succeeded = results.Count(r => r.Status == "Success"), PreValidationFailed = results.Count(r => r.Status == "PreValidationFailed"), ProcessingFailed = results.Count(r => r.Status == "ProcessingFailed"), PostValidationFailed = results.Count(r => r.Status == "PostValidationFailed"), Errors = results.Count(r => r.Status == "Error"), TotalDuration = stopwatch.Elapsed, AverageFileTime = results.Any() ? TimeSpan.FromMilliseconds(stopwatch.Elapsed.TotalMilliseconds / results.Count) : TimeSpan.Zero }; string reportJson = JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(config.ReportPath, reportJson); Console.WriteLine($"\n=== Pipeline Complete ==="); Console.WriteLine($"Succeeded: {report.Succeeded}"); Console.WriteLine($"Failed: {report.PreValidationFailed + report.ProcessingFailed + report.PostValidationFailed + report.Errors}"); Console.WriteLine($"Duration: {report.TotalDuration.TotalMinutes:F1} minutes"); Console.WriteLine($"Report: {config.ReportPath}"); // Helper methods bool ValidateInput(string path) { try { var info = new FileInfo(path); if (!info.Exists || info.Length == 0 || info.Length > 50 * 1024 * 1024) return false; string content = File.ReadAllText(path); return content.Contains("<html", StringComparison.OrdinalIgnoreCase) || content.Contains("<!DOCTYPE", StringComparison.OrdinalIgnoreCase); } catch { return false; } } bool ValidateOutput(string path) { try { using var pdf = PdfDocument.FromFile(path); return pdf.PageCount > 0 && new FileInfo(path).Length > 1024; } catch { return false; } } bool IsTransient(Exception ex) => ex is IOException || ex is OutOfMemoryException || ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase); Checkpoint LoadCheckpoint(string path) { if (File.Exists(path)) { string json = File.ReadAllText(path); return JsonSerializer.Deserialize<Checkpoint>(json) ?? new Checkpoint(); } return new Checkpoint(); } void SaveCheckpoint(string path, Checkpoint cp) => File.WriteAllText(path, JsonSerializer.Serialize(cp)); ata classes s PipelineConfig public string InputFolder { get; set; } = ""; public string OutputFolder { get; set; } = ""; public string ArchiveFolder { get; set; } = ""; public string ErrorFolder { get; set; } = ""; public string CheckpointPath { get; set; } = ""; public string ReportPath { get; set; } = ""; public int MaxConcurrency { get; set; } public int MaxRetries { get; set; } public int JpegQuality { get; set; } s Checkpoint public HashSet<string> CompletedFiles { get; set; } = new(); s ProcessingResult public string FileName { get; set; } = ""; public string Status { get; set; } = ""; public string Error { get; set; } = ""; public long OutputSize { get; set; } public DateTime StartTime { get; set; } public DateTime EndTime { get; set; } s PipelineReport public int TotalFiles { get; set; } public int ProcessedThisRun { get; set; } public int Succeeded { get; set; } public int PreValidationFailed { get; set; } public int ProcessingFailed { get; set; } public int PostValidationFailed { get; set; } public int Errors { get; set; } public TimeSpan TotalDuration { get; set; } public TimeSpan AverageFileTime { get; set; } $vbLabelText $csharpLabel 산출 배치 처리 결과를 보여주는 파이프라인 보고서. 이 파이프라인은 제어된 동시성을 통한 병렬 처리, 실패 시 건너뛰기를 포함한 파일별 오류 처리, 일시적인 오류에 대한 재시도 로직, 충돌 후 재개를 위한 체크포인트, 사전 및 사후 처리 유효성 검사, 명시적 해제를 통한 메모리 관리, 그리고 최종 요약 보고서를 포함한 포괄적인 로깅 등 이 튜토리얼에서 다룬 모든 패턴을 통합합니다. 이 파이프라인의 출력은 압축된 PDF/A-3b 규격 아카이브 파일이 담긴 디렉토리, 처리 재개를 위한 체크포인트 파일, 처리할 수 없었던 파일에 대한 오류 로그, 그리고 처리 통계가 포함된 요약 보고서입니다. 이것이 바로 대규모 PDF 일괄 처리 작업에 필요한 패턴입니다. 다음 단계 대규모 PDF 일괄 처리는 단순히 루프에서 렌더링 메서드를 호출하는 것만으로는 충분하지 않습니다. 동시성, 메모리 관리, 오류 처리 및 배포에 대한 신중한 아키텍처와 이 모든 것을 작동시키는 데 필요한 적절한 라이브러리가 필요합니다. IronPDF는 스레드 안전 렌더링 엔진, 비동기 API, 압축 도구 및 형식 변환 기능을 제공하여 모든 .NET 배치 PDF 파이프라인의 기반을 형성합니다. 매일 아침 해 뜨기 전에 수천 개의 PDF 파일을 생성하는 야간 보고서 생성기를 구축하든, 기존 문서 아카이브를 PDF/A 규격 에 맞게 마이그레이션하든, Kubernetes에 클라우드 네이티브 처리 서비스를 구축하든, 이 튜토리얼의 패턴은 검증된 프레임워크를 제공합니다. 제어된 동시성을 통한 병렬 처리로 높은 처리량을 유지합니다. 개별 파일에서 문제가 발생하더라도 실패 시 건너뛰기 및 재시도 로직을 통해 파이프라인이 계속 진행되도록 합니다. 체크포인트 기능을 사용하면 진행 상황을 절대 잃지 않습니다. 클라우드 배포 패턴을 사용하면 워크로드에 맞춰 컴퓨팅 성능을 확장할 수 있습니다. 건축을 시작할 준비가 되셨나요? IronPDF를 다운로드 하고 무료 평가판을 사용해 보세요. 단일 파일 렌더링부터 수십만 개 파일로 구성된 배치 파이프라인까지 모든 작업을 동일한 라이브러리로 처리할 수 있습니다. 확장성, 배포 또는 특정 사용 사례에 대한 아키텍처 관련 질문이 있으시면 엔지니어링 지원팀에 문의하십시오 . 저희는 다양한 규모의 배치 파이프라인 구축을 지원해 왔으며, 귀사도 성공적으로 구축할 수 있도록 기꺼이 도와드리겠습니다. 자주 묻는 질문 C#에서 일괄 PDF 처리란 무엇인가요? C#을 이용한 PDF 일괄 처리란 C# 프로그래밍 언어를 사용하여 수많은 PDF 문서를 동시에 자동으로 처리하는 것을 의미합니다. 이 방법은 대규모 문서 워크플로 자동화에 이상적입니다. IronPDF는 일괄 PDF 처리를 어떻게 지원할 수 있습니까? IronPDF는 C# 환경에서 PDF 일괄 처리를 간소화하는 강력한 도구와 라이브러리를 제공합니다. 병렬 처리를 지원하여 수천 개의 PDF 파일을 동시에 효율적으로 처리할 수 있습니다. IronPDF에서 병렬 처리를 사용하면 어떤 이점이 있습니까? IronPDF의 병렬 처리 기능을 사용하면 PDF 파일을 더욱 빠르고 효율적으로 일괄 처리할 수 있습니다. 이 방식은 리소스 활용도를 극대화하고 처리 시간을 크게 단축합니다. IronPDF를 클라우드 플랫폼에 배포하여 일괄 처리할 수 있습니까? 네, IronPDF는 Azure Functions, AWS Lambda, Kubernetes와 같은 클라우드 플랫폼에 배포할 수 있어 확장 가능하고 유연한 일괄 PDF 처리가 가능합니다. IronPDF는 일괄 PDF 처리 중 발생하는 오류를 어떻게 처리하나요? IronPDF는 일괄 PDF 처리 중 안정성을 보장하는 오류 처리 및 재시도 로직 기능을 포함하고 있습니다. 이러한 기능을 통해 수동 개입 없이 오류를 관리하고 수정할 수 있습니다. IronPDF를 사용한 PDF 처리에서 재시도 로직의 역할은 무엇인가요? IronPDF의 재시도 로직은 일시적인 문제가 일괄 처리 워크플로를 중단시키지 않도록 보장합니다. 오류가 발생할 경우 IronPDF는 실패한 문서를 자동으로 재처리할 수 있습니다. C#이 일괄 PDF 처리에 적합한 언어인 이유는 무엇입니까? C#은 방대한 라이브러리와 프레임워크를 갖춘 강력한 프로그래밍 언어로, 일괄 PDF 처리에 이상적입니다. IronPDF와 완벽하게 통합되어 효율적인 문서 자동화를 구현할 수 있습니다. IronPDF는 처리 과정에서 PDF 문서의 보안을 어떻게 보장합니까? IronPDF는 암호화 및 암호 보호 기능을 제공하여 PDF 문서를 안전하게 처리할 수 있도록 지원하며, 처리된 문서의 기밀성과 보안을 보장합니다. 기업에서 일괄 PDF 처리를 활용하는 사례는 어떤 것들이 있을까요? 기업들은 대량 송장 생성, 문서 디지털화, 대규모 보고서 배포와 같은 작업에 일괄 PDF 처리를 활용합니다. IronPDF는 문서 워크플로우를 자동화하고 간소화하여 이러한 사용 사례를 지원합니다. IronPDF는 다양한 PDF 형식과 버전을 처리할 수 있습니까? 네, IronPDF는 다양한 PDF 형식과 버전을 처리하도록 설계되어 일괄 처리 작업에서 호환성과 유연성을 보장합니다. 아흐마드 소하일 지금 바로 엔지니어링 팀과 채팅하세요 풀스택 개발자 아흐마드는 C#, Python 및 웹 기술에 탄탄한 기반을 갖춘 풀스택 개발자입니다. 그는 확장 가능한 소프트웨어 솔루션 구축에 깊은 관심을 가지고 있으며, 실제 응용 프로그램에서 디자인과 기능이 어떻게 조화를 이루는지 탐구하는 것을 즐깁니다. Iron Software 팀에 합류하기 전, 아흐마드는 자동화 프로젝트와 API 통합 업무를 담당하며 성능 향상과 개발자 경험 개선에 주력했습니다. 그는 여가 시간에 UI/UX 아이디어를 실험하고, 오픈 소스 도구에 기여하며, 복잡한 주제를 더 쉽게 이해할 수 있도록 기술 문서를 작성하는 데 몰두하기도 합니다. 시작할 준비 되셨나요? Nuget 다운로드 17,527,568 | 버전: 2026.2 방금 출시되었습니다 NuGet 무료 다운로드 총 다운로드 수: 17,527,568 라이선스 보기