💡 전체 작업 예제가 GitHub에 있습니다:
metadata-diff-between-docs-using-groupdocs-metadata-dotnet

소개

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

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

다음 섹션에서는 다음을 배웁니다.

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

메타데이터 비교가 중요한 이유

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

  • 법적 e‑discovery: 문서 출처를 입증하고 변조 여부를 감지
  • 규제 준수: GDPR 또는 SOX 감사를 위해 파일을 누가 생성·수정했는지 추적
  • 포렌식 조사: 숨겨진 수정이나 무단 편집을 발견

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

사전 요구 사항

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

NuGet을 통해 설치:

dotnet add package GroupDocs.Metadata

두 문서 버전 간 메타데이터를 어떻게 비교하나요?

답변: 각 파일을 MetadataFacade로 로드하고, 모든 속성을 사전으로 추출한 뒤, 추가·제거·변경된 항목을 구분하는 MetadataDiff를 구축합니다. 차이는 프로그래밍 방식으로 검사하거나 CSV/JSON으로 기록하여 감사 보고에 활용할 수 있습니다.

전체 속성 차이

다음 스니펫은 핵심 차이 알고리즘을 보여줍니다. 여기서는 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;

핵심 포인트:

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

소유권 변경 감지

소유권 관련 태그(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;

핵심 포인트:

  • Tags.Person.*Tags.Corporate.Company 프레디케이트를 사용합니다.
  • 각 항목이 이전 → 새 값을 보여주는 사전을 반환합니다.
  • 간결한 소유권 변경 보고서를 생성하는 데 이상적입니다.

도우미: 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;

수정 이력 변경 감지

수정 관련 메타데이터(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;

핵심 포인트:

  • 타임스탬프와 숫자형 수정 카운터를 모두 포착
  • 별도 버전으로 저장되지 않은 숨겨진 편집을 발견하는 데 유용

도우미: 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;

차이를 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());

핵심 포인트:

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

도우미: CsvEscape

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

차이를 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());

핵심 포인트:

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

도우미: 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;

메서드 비교: 언제 사용해야 할까

메서드 적합 상황 주요 장점 제한 사항
전체 속성 차이 일반 포렌식 감사 추가/제거/변경된 모든 속성을 감지 복잡한 파일의 경우 큰 차이 결과가 생성될 수 있음
소유권 감지 법적 출처 확인 식별 태그에만 집중 다른 메타데이터 변경은 무시
수정 이력 감지 편집 활동 분석 시간 기반 및 수정 카운터 강조 파일에 해당 속성이 존재해야 함
CSV 내보내기 스프레드시트 기반 보고 Excel에서 쉽게 열 수 있으며 사람이 읽기 쉬움 평면 구조에만 제한
JSON 내보내기 자동 파이프라인, 대시보드 구조화된, 기계가 읽을 수 있음 조금 더 큰 페이로드

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

모범 사례 및 팁

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

결론

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

다음 단계:

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

추가 리소스