소개

법무팀이나 포렌식 분석가가 문서가 변조되지 않았음을 입증해야 할 때, 눈에 보이는 내용만으로는 충분하지 않습니다. 작성자, 생성 날짜, 개정 번호와 같은 숨겨진 속성은 파일을 누가 언제 건드렸는지를 드러낼 수 있습니다. 이러한 미묘한 변화를 문서 버전 간에 감지하는 것은 각 속성을 수동으로 검사해야 하는 일반적인 고통 포인트이며, 이는 시간 소모가 크고 오류가 발생하기 쉬운 작업입니다.

GroupDocs.Metadata for Java는 모든 메타데이터 필드를 추출하고 두 버전 간에 구조화된 차이를 계산하는 프로그래밍 방식을 제공합니다. 이 튜토리얼에서는 전체 메타데이터 차이, 소유권 변경 감지, 개정 기록 분석이라는 세 가지 실용적인 접근 방식을 비교합니다. 각 방법은 간결하고 복사‑붙여넣기 가능한 코드로 시연되며, 결과를 CSV 또는 JSON으로 내보내어 감사 보고에 활용하는 방법도 보여줍니다.

여러 당사자가 수개월에 걸쳐 편집한 계약서를 검토하던 중에 이 문제를 겪었습니다. 눈에 보이는 텍스트는 동일했지만, 소유권 필드가 조용히 변경되어 있었습니다.

두 문서 버전 사이에서 어떤 메타데이터 필드가 변경되었는지 어떻게 알 수 있나요?

GroupDocs.Metadata는 각 파일을 로드하고 모든 접근 가능한 속성을 맵에 추출한 뒤, 키를 순회하면서 추가, 제거, 수정 항목을 분류합니다. 라이브러리는 기본 및 사용자 정의 태그를 모두 처리하므로 형식‑특정 파서를 작성할 필요 없이 전체 그림을 얻을 수 있습니다. 결과는 MetadataDiff 객체이며, 이를 조회하거나 규정 준수 보고서를 위해 직렬화할 수 있습니다.

사전 요구 사항

  • Java 8 이상
  • GroupDocs.Metadata for Java 24.7 (temporary license)
  • 비교하려는 두 문서 파일 (예: contract_v1.pdfcontract_v2.pdf)

설치

Maven을 통해 의존성을 추가합니다:

<dependency>
    <groupId>com.groupdocs</groupId>
    <artifactId>groupdocs-metadata</artifactId>
    <version>24.7</version>
</dependency>

방법 1 – 전체 메타데이터 차이

이 방법은 두 버전에서 모든 메타데이터 속성을 추출하고 추가, 제거, 변경된 항목을 보고합니다.

// CompareMetadataSets.run – returns a MetadataDiff object
Map<String, String> v1 = ExtractAllMetadata.run(pathV1);
Map<String, String> v2 = ExtractAllMetadata.run(pathV2);
MetadataDiff diff = new MetadataDiff();

// Detect added and changed properties
for (Map.Entry<String, String> e : v2.entrySet()) {
    String key = e.getKey();
    String val = e.getValue();
    if (!v1.containsKey(key)) {
        diff.added.put(key, val);               // New property in v2
    } else if (!v1.get(key).equals(val)) {
        diff.changed.put(key, new String[]{v1.get(key), val}); // Value changed
    }
}
// Detect removed properties
for (Map.Entry<String, String> e : v1.entrySet()) {
    if (!v2.containsKey(e.getKey())) {
        diff.removed.put(e.getKey(), e.getValue());
    }
}
return diff;

핵심 포인트:

  • 포괄적: 사용자 정의 태그를 포함한 모든 태그를 캡처합니다.
  • 단순 맵 로직: 외부 차이 라이브러리가 필요 없습니다.
  • 결과 객체: added, removed, changed 맵이 추가 처리에 바로 사용 가능합니다.

💡 팁: 규제 준수를 위한 전체 감사 추적이 필요할 때 사용하세요.

방법 2 – 소유권 변경 감지

법적 분쟁은 종종 누가 문서를 만들었거나 편집했는지에 달려 있습니다. 이 방법은 Creator, Editor, Manager, Company와 같은 사람‑관련 태그에 초점을 맞춥니다.

// DetectOwnershipChanges.run – returns a map of changed ownership fields
Map<String, String> v1 = readOwnership(pathV1);
Map<String, String> v2 = readOwnership(pathV2);
Set<String> allKeys = new HashSet<>(v1.keySet());
allKeys.addAll(v2.keySet());
Map<String, String[]> changes = new LinkedHashMap<>();
for (String key : allKeys) {
    String oldVal = v1.getOrDefault(key, "<missing>");
    String newVal = v2.getOrDefault(key, "<missing>");
    if (!oldVal.equals(newVal)) {
        changes.put(key, new String[]{oldVal, newVal});
    }
}
return changes;

readOwnership은 관련 태그만 추출합니다:

Map<String, String> result = new LinkedHashMap<>();
try (Metadata metadata = new Metadata(path)) {
    if (metadata.getFileFormat() == FileFormat.Unknown) return result;
    for (MetadataProperty p : metadata.findProperties(
            new ContainsTagSpecification(Tags.getPerson().getCreator())
                .or(new ContainsTagSpecification(Tags.getPerson().getEditor()))
                .or(new ContainsTagSpecification(Tags.getPerson().getManager()))
                .or(new ContainsTagSpecification(Tags.getCorporate().getCompany())))) {
        String value = "";
        if (p.getValue() != null && p.getValue().getRawValue() != null) {
            value = String.valueOf(p.getValue().getRawValue());
        }
        result.put(p.getName(), value);
    }
}
return result;

핵심 포인트:

  • 대상 지정: 신원 관련 속성만 검사합니다.
  • 명확한 출력: 각 항목이 [old, new] 값을 보여주는 맵을 반환합니다.
  • 규정 준수 준비: 전자증거개시(e‑discovery) 또는 계약‑소유권 분쟁에 최적입니다.

💡 팁: 폭넓은 차이가 필요하면 전체 차이와 함께 사용하세요.

방법 3 – 개정 기록 변경 감지

개정 번호, 편집 타임스탬프, 인쇄 날짜는 최종 사용자에게 보이지 않지만 포렌식 타임라인에 필수적입니다. 이 방법은 시간‑관련 태그만 분리합니다.

Map<String, String> v1 = readRevision(pathV1);
Map<String, String> v2 = readRevision(pathV2);
Set<String> allKeys = new HashSet<>(v1.keySet());
allKeys.addAll(v2.keySet());
Map<String, String[]> changes = new LinkedHashMap<>();
for (String key : allKeys) {
    String oldVal = v1.getOrDefault(key, "<missing>");
    String newVal = v2.getOrDefault(key, "<missing>");
    if (!oldVal.equals(newVal)) {
        changes.put(key, new String[]{oldVal, newVal});
    }
}
return changes;

readRevision은 Modified, Created, Printed 타임스탬프를 추출합니다:

Map<String, String> result = new LinkedHashMap<>();
try (Metadata metadata = new Metadata(path)) {
    if (metadata.getFileFormat() == FileFormat.Unknown) return result;
    for (MetadataProperty p : metadata.findProperties(
            new ContainsTagSpecification(Tags.getTime().getModified())
                .or(new ContainsTagSpecification(Tags.getTime().getCreated()))
                .or(new ContainsTagSpecification(Tags.getTime().getPrinted())))) {
        String value = "";
        if (p.getValue() != null && p.getValue().getRawValue() != null) {
            value = String.valueOf(p.getValue().getRawValue());
        }
        result.put(p.getName(), value);
    }
}
return result;

핵심 포인트:

  • 타임라인 재구성: 파일이 몇 번 편집·인쇄되었는지 보여줍니다.
  • 수치적 차이: 의심스러운 급속 개정을 감지하는 데 유용합니다.
  • 경량: 세 개 태그만 조회하므로 실행 속도가 빠릅니다.

💡 팁: 특정 마감일 이후에 문서가 변경되지 않았음을 입증해야 할 때 사용하세요.

방법 비교: 언제 어떤 방법을 사용해야 할까?

방법 최적 활용 상황 주요 장점 제한 사항
전체 메타데이터 차이 전체 감사, 규제 준수 모든 속성을 캡처(사용자 정의 태그 포함) 매우 큰 파일은 메모리 사용량 증가
소유권 변경 감지 법적 소유권 분쟁, e‑discovery 사람‑관련 필드에 집중, 가독성 높음 다른 메타데이터는 무시
개정 기록 감지 타임라인 포렌식, 변경 로그 검증 타임스탬프와 개정 번호만 추출 내용 수준 변화는 표시되지 않음

컴플라이언스 목표에 맞는 방법을 선택하세요. 많은 경우 전체 차이를 실행한 뒤 소유권이나 개정 섹션을 추가로 파고드는 조합이 가장 많은 인사이트를 제공합니다.

차이 내보내기

MetadataDiff를 얻은 후에는 결과를 공유해야 할 때가 많습니다. 아래는 두 가지 간단한 내보내기 예시입니다.

CSV 내보내기

StringBuilder sb = new StringBuilder();
sb.append("change_type,property,old_value,new_value\n");
for (Map.Entry<String, String> e : diff.added.entrySet()) {
    sb.append("added,").append(esc(e.getKey()))
      .append(",,").append(esc(e.getValue())).append("\n");
}
for (Map.Entry<String, String> e : diff.removed.entrySet()) {
    sb.append("removed,").append(esc(e.getKey()))
      .append(",").append(esc(e.getValue()))
      .append(",\n");
}
for (Map.Entry<String, String[]> e : diff.changed.entrySet()) {
    sb.append("changed,").append(esc(e.getKey()))
      .append(",").append(esc(e.getValue()[0]))
      .append(",").append(esc(e.getValue()[1])).append("\n");
}
Files.write(Paths.get(outputPath), sb.toString().getBytes(StandardCharsets.UTF_8));

JSON 내보내기

StringBuilder sb = new StringBuilder();
sb.append("{\n");
sb.append("  \"added\": {\n");
writeMap(sb, diff.added);
sb.append("  },\n");
sb.append("  \"removed\": {\n");
writeMap(sb, diff.removed);
sb.append("  },\n");
sb.append("  \"changed\": {\n");
int i = 0;
for (Map.Entry<String, String[]> e : diff.changed.entrySet()) {
    String comma = ++i < diff.changed.size() ? "," : "";
    sb.append("    \"").append(escape(e.getKey()))
      .append("\": { \"from\": \"")
      .append(escape(e.getValue()[0])).append("\", \"to\": \"")
      .append(escape(e.getValue()[1])).append("\" }")
      .append(comma).append("\n");
}
sb.append("  }\n");
sb.append("}\n");
Files.write(Paths.get(outputPath), sb.toString().getBytes(StandardCharsets.UTF_8));

두 내보내기 모두 쉼표와 따옴표를 안전하게 처리하는 헬퍼 메서드(esc, escape, writeMap)에 의존합니다.

모범 사례 및 팁

  • 차이 범위 지정: 대용량 PDF의 경우 소유권 또는 개정 태그만 제한하면 처리 시간이 단축됩니다.
  • 파일 형식 검증: 속성을 순회하기 전에 항상 metadata.getFileFormat() != FileFormat.Unknown를 확인하세요.
  • 리소스 해제: try‑with‑resources(try (Metadata metadata = new Metadata(path)) { … })를 사용해 네이티브 핸들을 해제합니다.
  • 버전 일관성: 두 문서가 동일한 파일 형식 버전인지 확인하세요; DOCX와 오래된 DOC를 혼합하면 오해의 소지가 있습니다.
  • 보안: 원시 메타데이터 값을 공개 API에 노출하지 마세요; CSV/JSON 작성 시 esc/escape 헬퍼를 사용합니다.
  • 성능: 대량 수집을 위해 CSV로 내보내면 SIEM에 쉽게 삽입할 수 있고, JSON은 사람 읽기 좋은 감사 로그에 적합합니다.

결론

GroupDocs.Metadata for Java는 메타데이터 포렌식을 간단하게 만들어 줍니다. 전체 차이, 소유권 감지, 개정 기록 분석 방법을 활용하면 숨겨진 변화를 드러내고, 법적 증거를 지원하며, 규정 준수 요구를 충족하는 견고한 감사 파이프라인을 구축할 수 있습니다. CSV 또는 JSON으로 내보내면 보고 도구나 데이터 웨어하우스 파이프라인과 원활히 통합됩니다.

다음 단계:

  • 사용자 정의 메타데이터를 필터링하기 위한 고급 태그 사양을 탐색하세요 GroupDocs.Metadata Java docs.
  • 배치로 여러 문서를 비교하는 방법을 배우세요 API reference.
  • 공식 샘플 프로젝트에서 엔드‑투‑엔드 구현을 확인하세요 GitHub examples.

추가 리소스