소개

법무 부서가 여러 계약 PDF를 받을 때, 각 검토자는 종종 기밀 조항을 보호하기 위해 자체 비밀번호를 추가합니다. 비밀번호가 서로 달라 파일을 직접 병합할 수 없기 때문에 이러한 파일을 하나의 바인더로 통합하는 일은 악몽이 됩니다. 수동으로 해제하는 작업은 시간이 많이 걸리고 오류가 발생하기 쉬우며, 특히 수십 개의 PDF를 다룰 때 더욱 그렇습니다.

Password‑merge는 Java용 GroupDocs.Merger 워크플로우로, 이질적인 PDF를 해제하고 병합된 결과를 단일 자격 증명으로 다시 보호합니다. 이 튜토리얼에서는 보호된 PDF를 감지하고, 해제하고, 내용을 병합하며, 필요에 따라 통합 비밀번호를 회전하는 과정을 단계별로 안내합니다.

이 과정을 통해 Merger API를 구성하고, 바이트 스트림을 처리하며, 30줄 미만의 Java 코드로 안전한 결합 PDF를 생성하는 방법을 배울 수 있습니다.

언제 비밀번호로 보호된 PDF를 병합해야 할까요?

비밀번호로 보호된 PDF를 병합하는 것은 보안 정책을 유지하면서 여러 비밀번호를 관리하는 번거로움을 없애고, 검색 가능한 단일 아카이브가 필요할 때 의미가 있습니다. 일반적인 시나리오로는 분기별 재무 보고서 번들, 감사용 계약 바인더, 각 기여자가 서로 다른 비밀번호를 적용한 법률 사건 파일 등이 있습니다. 각 파일을 프로그래밍 방식으로 해제하고 통합 비밀번호를 적용하면 아카이브를 안전하게 유지하면서 후속 검토 프로세스를 단순화할 수 있습니다. 전체 작업을 CI 파이프라인에서 자동화하면 수작업 시간을 크게 절감할 수 있습니다.

사전 요구 사항

  • Java 11 이상
  • GroupDocs.Merger for Java 24.6+ (temporary license)
  • 각 PDF 파일과 선택적으로 해당 비밀번호가 포함된 세트

Maven을 통해 라이브러리를 설치합니다:

mvn dependency:copy -Dartifact=com.groupdocs:groupdocs-merger:24.6

단계 1 – PDF가 비밀번호로 보호되었는지 감지하기

파일을 해제하려 시도하기 전에 실제로 비밀번호가 설정되어 있는지 확인합니다. 이렇게 하면 불필요한 처리를 방지하고 어떤 파일에 자격 증명이 필요한지 로그에 남길 수 있습니다.

// Returns true if the PDF at `path` has an owner or user password
public boolean isDocumentProtected(String path, String password) {
    Merger merger;
    if (password == null || password.isEmpty()) {
        merger = new Merger(path);
    } else {
        merger = new Merger(path, new LoadOptions(password));
    }
    try {
        return merger.isPasswordSet();
    } finally {
        merger.dispose();
    }
}

핵심 포인트:

  • LoadOptions는 알려진 비밀번호를 전달합니다; 비밀번호가 없으면 파일을 일반적으로 엽니다.
  • isPasswordSet()은 소유자 비밀번호와 사용자 비밀번호 모두에 대해 true를 반환합니다.
  • 네이티브 리소스를 해제하기 위해 Merger 인스턴스를 항상 dispose() 해야 합니다.

단계 2 – 각 PDF를 해제하고 원시 바이트 수집하기

키가 파일 경로이고 값이 비밀번호(없을 경우 null)인 맵을 순회합니다. 이 메서드는 해제된 PDF를 나타내는 바이트 배열 리스트를 반환합니다.

public List<byte[]> unlockAll(Map<String, String> sources) throws IOException {
    List<byte[]> unlocked = new ArrayList<>();
    for (Map.Entry<String, String> e : sources.entrySet()) {
        String path = e.getKey();
        String password = e.getValue();
        System.out.println("Unlocking (credentials=" +
                (password != null ? "yes" : "no") + "): " + path);
        if (password == null || password.isEmpty()) {
            unlocked.add(Files.readAllBytes(Paths.get(path)));
        } else {
            LoadOptions opts = new LoadOptions(password);
            ByteArrayOutputStream buf = new ByteArrayOutputStream();
            Merger m = new Merger(path, opts);
            try {
                m.removePassword();
                m.save(buf);
            } finally {
                m.dispose();
            }
            unlocked.add(buf.toByteArray());
        }
    }
    return unlocked;
}

핵심 포인트:

  • removePassword()는 기존 보호를 제거합니다.
  • 복호화된 내용은 메모리 내 처리를 위해 ByteArrayOutputStream에 기록됩니다.
  • 비밀번호가 없는 파일은 흐름을 단순화하기 위해 직접 읽어들입니다.

단계 3 – 해제된 PDF를 병합하고 통합 비밀번호 적용하기

첫 번째 PDF 스트림으로 Merger를 생성한 뒤, 나머지 스트림을 차례로 연결합니다. 마지막으로 AddPasswordOptions를 사용해 통합 비밀번호를 설정합니다.

public void mergeAndProtect(List<byte[]> unlockedPdfs,
                            String unifiedPassword,
                            String outputPath) {
    InputStream first = new ByteArrayInputStream(unlockedPdfs.get(0));
    Merger merger = new Merger(first);
    try {
        for (int i = 1; i < unlockedPdfs.size(); i++) {
            merger.join(new ByteArrayInputStream(unlockedPdfs.get(i)));
        }
        merger.addPassword(new AddPasswordOptions(unifiedPassword));
        merger.save(outputPath);
    } finally {
        merger.dispose();
    }
    System.out.println("Merged output: " + new File(outputPath).getAbsolutePath());
    System.out.println("Unified password: " + unifiedPassword);
}

핵심 포인트:

  • addPassword는 제공된 자격 증명으로 최종 PDF를 암호화합니다.
  • 모든 작업이 메모리 내에서 이루어지며, 최종 파일만 디스크에 기록됩니다.
  • 네이티브 핸들을 해제하기 위해 Merger를 반드시 dispose() 해야 합니다.

단계 4 – 이미 보호된 PDF의 비밀번호 회전 (선택 사항)

조직에서 정기적인 비밀번호 교체를 요구하는 경우, 원본 파일을 다시 병합하지 않고도 자격 증명을 업데이트할 수 있습니다.

public void rotateUnifiedPassword(String path,
                                 String oldPassword,
                                 String newPassword,
                                 String outputPath) {
    Merger merger = new Merger(path, new LoadOptions(oldPassword));
    try {
        merger.updatePassword(new UpdatePasswordOptions(newPassword));
        merger.save(outputPath);
    } finally {
        merger.dispose();
    }
    System.out.println("Rotated output: " + new File(outputPath).getAbsolutePath());
    System.out.println("New password: " + newPassword);
}

핵심 포인트:

  • 현재 비밀번호로 보호된 PDF를 로드합니다.
  • updatePassword가 새 자격 증명으로 교체합니다.
  • PDF 내용을 다시 처리하지 않기 때문에 매우 빠르게 수행됩니다.

실제 적용 사례

다섯 개의 투자자 계약서 각각에 서로 다른 검토자 비밀번호가 적용된 상황을 마주했습니다. 위 단계들을 따라 모든 파일을 해제하고 하나의 바인더로 병합한 뒤, 우리 기업 정책에 맞는 단일 비밀번호를 적용했습니다. 전체 과정은 일반 노트북에서 2분 이내에 완료되었습니다.

모범 사례

  • 비밀번호를 조기에 검증: isDocumentProtected를 사용해 수동 검토가 필요한 파일을 미리 표시합니다.
  • 메모리 사용량 제한: 대용량 PDF의 경우 모든 바이트 배열을 메모리에 보관하지 말고 디스크에 스트리밍합니다.
  • 객체를 즉시 해제: Merger 클래스는 네이티브 리소스를 보유하므로 finally 블록에서 반드시 dispose() 를 호출합니다.
  • 임시 라이선스는 개발용으로만 사용하고, 배포 전에는 정식 라이선스를 획득합니다.

결론

GroupDocs.Merger for Java는 PDF 컬렉션을 해제, 병합 및 재보호하는 깔끔한 API를 제공합니다. 보호 감지, 해제, 통합 비밀번호 적용, 필요 시 비밀번호 회전이라는 네 단계만 따르면 수동 개입 없이 안전한 PDF 바인더를 자동으로 생성할 수 있습니다.

다음 단계:

  • 병합 후 PDF 메타데이터 설정과 같은 추가 옵션을 살펴보세요 (문서).
  • 북마크를 유지하면서 PDF를 병합하는 방법을 배워보세요 (API 참조).
  • 실행 가능한 구현 예제가 포함된 전체 샘플 프로젝트를 GitHub에서 확인하세요.

추가 리소스