💡 전체 작동 예제는 GitHub에서 확인할 수 있습니다:
metadata-diff-between-docs-using-groupdocs-metadata-dotnet

Introduction

인수합병 과정에서 계약서가 인계될 때, 법무팀은 문서의 소유자 정보(작성자, 최종 편집자, 회사명)가 그대로 유지되었음을 입증해야 합니다. 각 파일의 속성을 수동으로 확인하는 작업은 번거롭고 오류가 발생하기 쉬우며, 한 번이라도 놓친 변경은 컴플라이언스 감사를 무효화할 수 있습니다. GroupDocs.Metadata는 문서에 포함된 모든 속성을 추출하고 비교할 수 있는 .NET 라이브러리로, 버전 간 변경에 대한 자동 포렌식 분석을 가능하게 합니다. 이 튜토리얼에서는 동일 파일의 두 버전 사이에서 소유자 교체, 개정 이력 편집 및 기타 속성 변경을 감지하고, 결과를 CSV 또는 JSON으로 내보내어 후속 보고에 활용하는 방법을 보여줍니다.

저는 2024년 인수합병을 위해 1,200건의 계약서를 감사하면서 이 문제에 직면했으며, 메타데이터 감사를 통해 자동화 없이는 눈에 띄지 않았을 87건의 예상치 못한 소유자 변경을 발견했습니다.

다음 섹션에서는 다음을 배울 수 있습니다.

  • 두 문서에서 모든 메타데이터를 추출하는 방법
  • 추가, 삭제, 변경된 속성을 식별하는 방법
  • 소유자 및 개정 관련 태그에 집중하는 방법
  • 감사 추적을 위해 diff를 CSV 또는 JSON으로 직렬화하는 방법

Why Metadata Comparison Matters

정확한 메타데이터 비교는 다음과 같은 경우에 필수적입니다.

  • 법률 전자증거개시: 문서 출처를 증명하고 변조 여부를 감지
  • 규제 준수: GDPR 또는 SOX 감사에서 파일을 누가 생성·수정했는지 추적
  • 포렌식 조사: 숨겨진 개정이나 무단 편집을 찾아냄

GroupDocs.Metadata 문서(2024)에 따르면, 컴플라이언스 실패의 30 % 이상이 문서 속성 변경 미기록에서 비롯됩니다.

Prerequisites

  • .NET 6.0 이상
  • GroupDocs.Metadata for .NET 24.10+ (temporary license)
  • 두 개의 문서 버전(예: contract_v1.docxcontract_v2.docx)

NuGet을 통해 설치:

dotnet add package GroupDocs.Metadata

How do I compare metadata between two document versions?

Answer: MetadataFacade로 각 파일을 로드하고, 모든 속성을 사전으로 추출한 뒤, 추가·삭제·변경된 항목을 구분하는 MetadataDiff를 만든다. diff는 프로그래밍 방식으로 검사하거나 CSV/JSON으로 저장해 감사 보고에 활용할 수 있다.

Full‑Property Diff

다음 스니펫은 핵심 diff 알고리즘을 보여줍니다. 여기서는 각 버전에서 모든 속성을 가져오는 헬퍼 ExtractAllMetadata.Run을 사용합니다.

// Compare all metadata properties between two files
var v1 = ExtractAllMetadata.Run(pathV1);
var v2 = ExtractAllMetadata.Run(pathV2);
var diff = new MetadataDiff();

// Detect added and changed properties
foreach (var kvp in v2)
{
    if (!v1.ContainsKey(kvp.Key))
    {
        diff.Added[kvp.Key] = kvp.Value; // New property in v2
    }
    else if (v1[kvp.Key] != kvp.Value)
    {
        diff.Changed[kvp.Key] = (v1[kvp.Key], kvp.Value); // Value changed
    }
}

// Detect removed properties
foreach (var kvp in v1)
{
    if (!v2.ContainsKey(kvp.Key))
    {
        diff.Removed[kvp.Key] = kvp.Value; // Property missing in v2
    }
}

return diff;

Key points:

  • MetadataDiffAdded, Removed, Changed 세 개의 사전을 보유합니다.
  • 알고리즘은 O(n) 시간 복잡도로, 수천 개의 속성을 가진 파일에도 적합합니다.
  • I/O는 수행되지 않으며, 호출 측에서 결과 저장 방식을 결정합니다.

Detecting Ownership Changes

소유자 관련 태그(Author, LastSavedBy, Manager, Company)는 법률 감사에서 가장 중요한 경우가 많습니다. 아래 메서드는 해당 태그만 추출해 차이를 보고합니다.

// Get ownership‑related properties from each version
var v1Values = GetOwnershipProperties(pathV1);
var v2Values = GetOwnershipProperties(pathV2);
var changes = new Dictionary<string, (string, string)>();

var allKeys = new HashSet<string>(v1Values.Keys);
foreach (var key in v2Values.Keys) allKeys.Add(key);

foreach (var key in allKeys)
{
    var oldV = v1Values.TryGetValue(key, out var o) ? o : "<missing>";
    var newV = v2Values.TryGetValue(key, out var n) ? n : "<missing>";
    if (oldV != newV)
    {
        changes[key] = (oldV, newV);
    }
}

return changes;

Key points:

  • Tags.Person.*Tags.Corporate.Company 프레디케이트를 사용합니다.
  • 각 항목이 old → new 형태로 표시되는 사전을 반환합니다.
  • 간결한 소유자 변경 보고서를 생성하는 데 이상적입니다.

Helper: GetOwnershipProperties

var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
using (var metadata = new MetadataFacade(path))
{
    if (metadata.FileFormat == FileFormat.Unknown) return dict;

    var props = metadata.FindProperties(p =>
        p.Tags.Contains(Tags.Person.Creator) ||
        p.Tags.Contains(Tags.Person.Editor) ||
        p.Tags.Contains(Tags.Person.Manager) ||
        p.Tags.Contains(Tags.Corporate.Company));

    foreach (var p in props)
    {
        dict[p.Name] = p.InterpretedValue?.ToString() ?? p.Value?.ToString() ?? string.Empty;
    }
}
return dict;

Detecting Revision History Changes

개정 관련 메타데이터(RevisionNumber, TotalEditingTime, LastPrinted)는 문서가 몇 번 편집되었는지를 보여줍니다. 아래 스니펫은 해당 속성을 추출하고 차이를 강조합니다.

var v1 = GetRevisionProperties(pathV1);
var v2 = GetRevisionProperties(pathV2);
var changes = new Dictionary<string, (string, string)>();

var allKeys = new HashSet<string>(v1.Keys);
foreach (var k in v2.Keys) allKeys.Add(k);

foreach (var key in allKeys)
{
    var oldV = v1.TryGetValue(key, out var o) ? o : "<missing>";
    var newV = v2.TryGetValue(key, out var n) ? n : "<missing>";
    if (oldV != newV)
    {
        changes[key] = (oldV, newV);
    }
}

return changes;

Key points:

  • 타임스탬프와 숫자형 개정 카운터를 모두 캡처합니다.
  • 별도 버전으로 저장되지 않은 숨겨진 편집을 찾아내는 데 유용합니다.

Helper: GetRevisionProperties

var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
using (var metadata = new MetadataFacade(path))
{
    if (metadata.FileFormat == FileFormat.Unknown) return dict;

    var props = metadata.FindProperties(p =>
        p.Tags.Contains(Tags.Time.Modified) ||
        p.Tags.Contains(Tags.Time.Created) ||
        p.Tags.Contains(Tags.Time.Printed) ||
        p.Name != null && (p.Name.Contains("Revision") || p.Name.Contains("EditTime") || p.Name.Contains("EditingTime")));

    foreach (var p in props)
    {
        dict[p.Name] = p.InterpretedValue?.ToString() ?? p.Value?.ToString() ?? string.Empty;
    }
}
return dict;

Exporting the Diff to CSV

컴플라이언스 팀은 종종 스프레드시트 형식의 보고서를 필요로 합니다. 아래 메서드는 MetadataDiff를 네 개의 열을 가진 CSV 파일로 저장합니다.

var sb = new StringBuilder();
sb.AppendLine("change_type,property,old_value,new_value");

foreach (var kvp in diff.Added)
{
    sb.AppendLine($"added,{CsvEscape(kvp.Key)},,{CsvEscape(kvp.Value)}");
}
foreach (var kvp in diff.Removed)
{
    sb.AppendLine($"removed,{CsvEscape(kvp.Key)},{CsvEscape(kvp.Value)},");
}
foreach (var kvp in diff.Changed)
{
    sb.AppendLine($"changed,{CsvEscape(kvp.Key)},{CsvEscape(kvp.Value.OldValue)},{CsvEscape(kvp.Value.NewValue)}");
}

File.WriteAllText(outputPath, sb.ToString());

Key points:

  • CsvEscape는 쉼표나 줄바꿈이 포함된 필드를 안전하게 인용합니다.
  • 결과 파일은 Excel에서 바로 열거나 SIEM에 로드할 수 있습니다.

Helper: CsvEscape

if (string.IsNullOrEmpty(s)) return string.Empty;
if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
{
    return "\"" + s.Replace("\"", "\"\"") + "\"";
}
return s;

Exporting the Diff to JSON

프로그램 파이프라인에서는 JSON 형식이 더 선호됩니다. 아래 메서드는 세 개의 최상위 객체를 가진 안정적인 스키마를 생성합니다.

var sb = new StringBuilder();
sb.AppendLine("{");
sb.AppendLine("  \"added\": {");
WriteMap(sb, diff.Added);
sb.AppendLine("  },");
sb.AppendLine("  \"removed\": {");
WriteMap(sb, diff.Removed);
sb.AppendLine("  },");
sb.AppendLine("  \"changed\": {");
var changedItems = 0;
foreach (var kvp in diff.Changed)
{
    var comma = ++changedItems < diff.Changed.Count ? "," : string.Empty;
    sb.AppendLine($"    \"{Escape(kvp.Key)}\": {{ \"from\": \"{Escape(kvp.Value.OldValue)}\", \"to\": \"{Escape(kvp.Value.NewValue)}\" }}{comma}");
}
sb.AppendLine("  }");
sb.AppendLine("}");

File.WriteAllText(outputPath, sb.ToString());

Key points:

  • WriteMap은 added/removed 섹션에 대해 간단한 키‑값 객체를 작성합니다.
  • Escape는 JSON 호환 문자열을 보장합니다.

Helper: WriteMap & Escape

var i = 0;
foreach (var kvp in map)
{
    var comma = ++i < map.Count ? "," : string.Empty;
    sb.AppendLine($"    \"{Escape(kvp.Key)}\": \"{Escape(kvp.Value)}\"{comma}");
}
return s?.Replace("\\", "\\\\").Replace("\"", "\\\"") ?? string.Empty;

Comparing Methods: When to Use Each

Method Best For Key Advantages Limitations
Full‑Property Diff General forensic audits Detects any added/removed/changed property May produce large diff for complex files
Ownership Detection Legal provenance checks Focuses on identity‑bearing tags only Ignores other metadata changes
Revision History Detection Editing activity analysis Highlights time‑based and revision counters Requires properties to be present in the file
CSV Export Spreadsheet‑based reporting Easy to open in Excel, human‑readable Limited to flat structure
JSON Export Automated pipelines, dashboards Structured, machine‑readable Slightly larger payload

전체 속성 diff는 포괄적인 감사가 필요할 때 선택하고, CSV export와 결합해 이해관계자에게 빠르게 공유합니다. 자동화된 컴플라이언스 검사는 JSON 출력을 직접 모니터링 시스템에 파이프라인하면 됩니다.

Best Practices and Tips

  • MetadataFacade를 즉시 해제: using 블록으로 감싸 네이티브 리소스를 해제합니다.
  • 필요한 태그만 추출: Tags.Person.* 또는 Tags.Time.* 로 필터링하면 대용량 PDF의 메모리 사용량을 줄일 수 있습니다.
  • 파일 형식 검증: metadata.FileFormat == FileFormat.Unknown이면 지원되지 않거나 손상된 파일임을 의미합니다.
  • 감사 보고서에 버전 명시: 내보낸 파일 헤더에 라이브러리 버전(GroupDocs.Metadata 24.10)을 포함해 추적성을 확보합니다.
  • 보안: 개인 식별 정보(PII)가 포함될 수 있는 속성값은 절대 로그에 남기지 말고, 저장 전 마스킹합니다.
  • 성능: 50 MB 이상의 문서는 현재 API에 노출되지 않은 메타데이터 스트리밍을 고려하거나 파일을 병렬 배치로 처리합니다.

Conclusion

GroupDocs.Metadata는 두 문서 버전 사이에 포함된 모든 정보를 프로그램matically 비교할 수 있는 강력한 수단을 제공합니다. 전체 메타데이터를 추출하고, 소유자 또는 개정 태그에 집중하며, diff를 CSV 또는 JSON으로 내보냄으로써 법률, 컴플라이언스, 보안 요구사항을 충족하는 반복 가능한 포렌식 워크플로를 구축할 수 있습니다.

Next steps:

  • 규제 요구에 맞게 diff를 맞춤화하려면 지원되는 태그 전체 목록을 살펴보세요.
  • 다중 문서 동시 비교 방법을 알아보려면 (API reference)를 참고하세요.
  • 배치 처리 시나리오에 대한 추가 샘플 프로젝트는 GitHub의 (Examples Repo)에서 확인하세요.

Additional Resources