DOCX, XLSX 및 PDF를 하나의 바인더로 병합 — 데모 실행

금요일을 잡아먹던 일

매주 금요일 오후, 약 1년 동안 나는 같은 작은 의식을 가지고 있었다. 계약서는 세 개의 파일로 들어왔다 — Word 형식의 주요 계약서, Excel 형식의 가격 부록, 그리고 파트너의 조건 시트가 PDF 형태로 — 그리고 나는 이를 하나의 깔끔한 PDF로 만들어야 했다. 어렵지 않았다. Word를 열고 PDF로 내보내고, Excel을 열고 PDF로 내보내고, 무료 PDF 병합 앱을 열어 세 파일을 끌어다 놓고 순서를 확인한 뒤 저장하면 된다.

대략 8분 정도 걸렸다. 이를 주당 15건의 계약서에 곱하면 마우스를 움직이는 데만 2시간을 잃는 셈이다. 더 안 좋은 점은, 몇 주마다 누군가가 부록이 첫 페이지에 오도록 파일 이름이 알파벳 순서대로 정렬돼 바인더를 만들었다는 것이다.

이 상황이 익숙하다면, 이 글의 나머지는 내가 그 의식을 코드로 대체한 오후 이야기를 담고 있다.

실제 비용은 시간 자체가 아니라, 50건 중 한 건에서 페이지 순서가 뒤바뀌어 고객이 잘못된 버전에 서명할 때까지 아무도 눈치채지 못하는 경우다.

내가 실제로 원했던 것

“멋진 문서 파이프라인”이 아니다. 단 세 가지만 원했다.

  1. 파일 목록( DOCX, XLSX, PDF가 섞인 어떤 형태든) 을 메서드에 넘기면 하나의 PDF를 반환한다.
  2. 같은 로직을 폴더에 적용해 파일 목록을 자동으로 파악한다.
  3. 완성된 바인더에서 페이지 범위만 추출해 전체 병합을 다시 할 필요 없이 얻는다.

그게 전부다. 라이브러리가 이 세 가지를 깔끔히 처리하지 못한다면, 나는 알 필요가 없다.

설정

  • .NET 6.0 이상
  • GroupDocs.Merger for .NET 24.10+ (임시 라이선스 받기 – 평가용 워터마크 없이 배포 가능)
  • 평소에 직접 병합하던 문서들이 섞여 있는 폴더
dotnet add package GroupDocs.Merger

이것으로 의존성은 끝이다. 외부 변환기, 헤드리스 Office 설치, PDF 조작 라이브러리 따위는 필요 없다.

1단계 — 폴더를 입력으로 사용하기

현실적인 진입점이기 때문에 항상 여기서 시작한다. 실제로는 업로드 핸들러, 이메일 수집 작업, 야간 재무 데이터 덤프 등 다른 무언가가 디렉터리에 파일들을 놓아두고, 내 코드는 그 파일들을 처리해야 한다.

// Pick up every supported file in the drop folder; the PDF wins
// the tie-break for position 0 so the merger keeps the output
// as a PDF regardless of how files are named.
string[] extensions = { ".pdf", ".docx", ".xlsx" };
var files = Directory.EnumerateFiles(folderPath)
    .Where(f => extensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
    .OrderBy(f => Path.GetExtension(f).ToLowerInvariant() == ".pdf" ? 0 : 1)
    .ThenBy(f => f)
    .ToArray();

if (files.Length == 0)
    throw new InvalidOperationException(
        $"No supported documents found in '{folderPath}'.");

OrderBy 트릭이 핵심이다. GroupDocs.Merger는 처음 연 파일의 형식으로 출력 형식을 결정한다 — 만약 첫 번째 파일이 DOCX라면 출력도 DOCX가 된다. 파이프라인이 항상 PDF를 내보내야 하므로, 폴더에 PDF가 있으면 위치 0을 차지하도록 만든다.

두 가지 주의할 점:

  • ToLowerInvariant()는 파트너가 REPORT.PDF 같은 대문자 파일을 보낼 때 필터가 조용히 파일을 놓치지 않게 한다.
  • ThenBy(f)는 출력 순서를 결정적으로 만들기 위해 존재한다. 이것이 없으면 같은 폴더에 대해 두 번 실행했을 때 파일 시스템 상태에 따라 결과가 달라질 수 있다.

2단계 — 병합 자체

정렬된 경로 목록을 얻었으면, 병합 코드는 설명보다 짧다.

Console.WriteLine($"Primary source: {sourcePaths[0]}");
using var merger = new Merger(sourcePaths[0]);

var joinOptions = new JoinOptions();
for (int i = 1; i < sourcePaths.Length; i++)
{
    Console.WriteLine($"Joining: {sourcePaths[i]}");
    merger.Join(sourcePaths[i], joinOptions);
}

merger.Save(outputPath);
Console.WriteLine($"Unified PDF binder saved to: {Path.GetFullPath(outputPath)}");

실제 사용 시 기억할 점 몇 가지:

  • using이 중요하다. Merger는 원본 파일 핸들을 보유한다; 이를 해제하지 않으면 폴더 워커가 입력 파일을 삭제하지 못한다.
  • JoinOptions는 여기서는 비워두었다. 기본값이 95% 상황에 맞기 때문이다. 페이지 범위, 회전, 삽입 위치 등은 필요할 때 여기서 지정한다.
  • Excel 파일이 바인더에 들어갈 때 시트‑페이지 레이아웃은 원본 워크북의 인쇄 영역에 따라 결정된다. XLSX가 38페이지가 나오고 3페이지만 원한다면, 스프레드시트 자체를 수정해야지 JoinOptions에서 고치는 것이 아니다.

저장 직후 항상 추가하는 간단한 검증 코드:

using var verify = new Merger(outputPath);
Console.WriteLine($"Result pages: {verify.GetDocumentInfo().PageCount}");

2초짜리 코드지만, “숨겨진 부록 누락” 버그를 어느 테스트보다 많이 잡아준다.

3단계 — 나중에 슬라이스 추출하기

매번 받는 추가 요청: “표지 페이지만 보내줄 수 있나요?” 혹은 “클라이언트가 서명 페이지만 원해요.” 전체 바인더를 다시 만들고 두 페이지만 전달하는 건 비효율적이다 — 추출 기능을 바로 사용하면 된다.

using var merger = new Merger(binderPath);
merger.ExtractPages(new ExtractOptions(pages));
merger.Save(outputPath);
Console.WriteLine($"Extracted pages [{string.Join(",", pages)}] to " +
    Path.GetFullPath(outputPath));

pages는 유지하고 싶은 1‑기반 페이지 번호 배열(int[])이다. 나머지는 모두 버린다. 결과가 이미 PDF이기 때문에 변환 라운드‑트립이 없어 빠르다.

전·후 비교, 솔직히 말하면

이전 방식 Merger.Join 사용 후
계약당 소요 시간 클릭만으로 5–10분 30초 이하 전체 프로세스
일반적인 실패 페이지 순서가 뒤바뀌어도 눈치 못 챔 파일 목록 순서대로 일관되게 처리
하루 100건 확장성 불가능 — 사람을 고용해야 함 대부분 시간이 남는 한 명의 워커
유지 보수 코드 “Binder Process v4” 라는 Confluence 페이지 하나의 클래스, 약 70줄
출력 PDF 3개와 기도문 페이지 수를 로그로 남길 수 있는 하나의 바인더

가장 신경 쓰는 행은 “실패” 항목이다. 수동 병합은 조용히 실패하지만, 페이지 수를 로그로 남기는 코드는 크게 실패한다.

작은 법률‑테크 팀의 실제 사례

내가 일했던 2인 스타트업에는 아침마다 계약서를 조립하던 파라리걸이 있었다. Word 계약서, Excel 가격표, PDF 부록을 앱으로 이어서 DocuSign에 업로드한다. 한 패키지당 약 8분, 하루 30패키지는 곧 그녀의 전체 아침 작업이 되었다.

그들은 폴더 스캔 방식을 이미 이메일 수신을 감시하던 백엔드 서비스에 넣었다. 패키지당 20초, 페이지 수 로그 한 줄. 파라리걸은 조립 대신 계약 검토에 집중하게 되었다. 다시는 순서가 뒤바뀐 바인더가 배포되지 않았다 — 라이브러리가 마법이라서가 아니라, 파일 목록이 코드에 명시돼 있어 차이를 바로 확인할 수 있었기 때문이다.

string folder = @"C:\IncomingContracts";
string output = @"C:\Processed\ContractPackage.pdf";

var files = CreatePdfBinderFromFolder(folder, output);
Console.WriteLine($"Package created: {files}");

이게 전체 통합 과정이다. 앞선 단계(이메일 리스너, 저장 경로)는 이미 구축돼 있었다.

오늘은 필요 없었지만 내일은 필요할 수도 있는 기능들

같은 라이브러리는 여기서 다루지 않은 다양한 기능을 제공한다. 대략 내가 사용해 본 순서대로:

  • Watermarks on the output – 서명 전 사본에 “DRAFT” 스탬프 삽입
  • Page rotation – 가로로 스캔된 페이지 회전
  • Custom page ordering – 원본 순서와 전달 순서가 다를 때 재정렬
  • PDF encryption – 외부 거래처에 보낼 때 암호화

모두 Merger API 뒤에 숨겨 있다. 전체 목록은 docs에 있다 — “merge”가 가장 기본적인 시작점이며, 필요할 때 다른 기능을 사용할 수 있다.

과거의 나에게 해주고 싶은 말

“그냥 한 메서드만 만들면 돼” 라고 DOCX‑to‑PDF 변환을 직접 구현하려고 하지 말라. 변환은 조용히 부패한다 — 새로운 Office 기능, 스캔 이미지 처리, 임베디드 폰트 등. 그 부분은 다른 도구에 맡기고, 금요일 오후는 파일명 정렬이 아닌 더 의미 있는 일에 쓰라.

다음 단계:

유용한 링크