💡 전체 작동 예제는 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.docx및contract_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:
MetadataDiff는Added,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)에서 확인하세요.