💡 전체 작동 예제는 GitHub에서 확인할 수 있습니다:
scrub-office-document-pii-dotnet

The Old Way Was Painful

이 절차는 다음과 같습니다. 문서를 열고, File → Info → Check for Issues → Inspect Document 로 이동한 뒤 체크 박스를 선택하고 Remove All 을 클릭합니다. 새 이름으로 저장하고, 파일을 닫은 뒤 다음 파일을 엽니다. 40개의 파일을 처리한 뒤에야 누군가가 Inspect Document 가 레코드 인덱스 키에 사용되는 Title 도 제거했으며, 한 시간 전에 보낸 복사본에는 여전히 SharePoint 승인자 ID가 남아 있었다는 것을 발견합니다. 이는 파일이 다른 애플리케이션에서 저장될 때 해당 필드가 다시 기록되었기 때문입니다.

Metadata PII 제거는 .NET용 GroupDocs.Metadata 기능으로, Office 문서에서 신원 정보를 담고 있는 속성을 프로그래밍 방식으로 삭제하고 남은 내용을 보고합니다. 수동 절차는 세 가지 문제점이 있습니다. 파일 몇 개만 처리해도 확장성이 없고, 어떤 필드를 삭제할지 전부 혹은 전무 방식이며, 삭제된 내용에 대한 기록이 남지 않습니다. 이 문서에서는 동일한 작업을 .NET 버전으로, 속성 그룹별로 하나씩 수행하는 방법을 보여줍니다.

실제로 어떤 메타데이터가 들어 있는지 아는 것이 도움이 됩니다. 검토 라운드를 거친 Word 파일은 일반적으로 AuthorLastSavedBy(저장한 Windows 계정), ManagerCompany(기업 템플릿), 수정 횟수, TotalEditingTime, LastPrinted 타임스탬프, 댓글 스레드 카운터 등을 포함합니다. SharePoint가 연계되면 승인자 식별자, 워크플로 경로, 콘텐츠 유형 URI, 문서가 생성된 템플릿 등도 추가됩니다. 이 모든 정보는 페이지에 표시되지 않으며 동일한 파일 안에 함께 저장됩니다.

There’s a Better Way

GroupDocs.Metadata for .NET의 모든 기능은 하나의 속성 검색 엔진을 통해 동작합니다. RemovePropertiesMetadataProperty에 대한 람다식을 받아 해당 람다식이 허용하는 모든 속성을 삭제하고 삭제된 개수를 반환합니다. FindProperties 는 같은 람다식을 실행하지만 실제 삭제는 하지 않습니다. 속성에는 태그가 포함될 수 있어, Tags.Person.Creator 와 같이 태그를 사용하면 형식이나 패키지에 관계없이 저자와 같은 필드를 일관되게 식별할 수 있습니다. 이는 애플리케이션마다 다른 실제 이름을 매칭하는 번거로움을 없애줍니다.

이렇게 하면 하나의 버튼 대신 세 가지 형태의 정리가 가능합니다. 신원 필드에 대한 태그 기반 패스, 댓글·수정과 같은 그룹에 대한 이름 기반 패스, 그리고 아무 것도 남기지 않을 때 사용하는 Sanitize() 입니다. 세 가지 모두 숫자를 반환하며, 이 숫자가 바로 감사 가능한 결과를 제공합니다.

The New Way: A Predicate per Property Group

Step 1 - Clear the names

신원 그룹을 커버하는 네 가지 태그 검사가 있습니다. 설명 메타데이터는 그대로 두며, 이는 Document Inspector의 Remove All 과 차별화되는 점입니다.

using (var metadata = new Metadata(inputPath))
{
    if (metadata.FileFormat == FileFormat.Unknown) return 0;
    var affected = metadata.RemoveProperties(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));
    metadata.Save(outputPath);
    return affected;
}

FileFormat.Unknown 검사는 0이라는 반환값이 정직하도록 보호해 줍니다. 이 검사가 없으면 읽을 수 없는 파일과 정상 파일이 호출자에게 동일하게 보일 수 있습니다.

Step 2 - Clear the families around them

댓글, 수정, 서버 필드에는 태그가 없으므로 이름으로 매칭합니다. 편집 타임라인은 가장 자주 잊히는 그룹이며, 문서가 어떻게 생성되었는지를 나타내는 중요한 정보입니다.

using (var metadata = new Metadata(inputPath))
{
    if (metadata.FileFormat == FileFormat.Unknown) return 0;
    var affected = metadata.RemoveProperties(p =>
        p.Name != null && (
            p.Name.Contains("Revision") ||
            p.Name.Contains("TrackedChange") ||
            p.Name.Contains("LastPrinted") ||
            p.Name.Contains("TotalEditingTime") ||
            p.Name.Contains("EditTime")));
    metadata.Save(outputPath);
    return affected;
}

부분 문자열 매칭은 의도적인 설계입니다. CommentsCountComment, TotalEditingTimeEditTime 을 정확한 이름 리스트 없이도 포착합니다. SharePoint 관련 패스는 동일한 호출에 Server, Workflow, Approver, ContentType, Template 을 추가하면 됩니다.

Step 3 - Wipe everything, then check the result

신뢰 경계에서는 네 번의 패스를 하나의 호출로 대체합니다.

using (var metadata = new Metadata(inputPath))
{
    if (metadata.FileFormat == FileFormat.Unknown) return 0;
    var affected = metadata.Sanitize();
    metadata.Save(outputPath);
    return affected;
}

그 다음은 수동 절차에 없던 검증 단계입니다. FindProperties 로 제거용 프레디케이트를 재사용해 남은 속성을 두 리스트로 분류합니다.

foreach (var p in props)
{
    var value = p.InterpretedValue?.ToString() ?? p.Value?.ToString() ?? string.Empty;
    if (string.IsNullOrWhiteSpace(value)) continue;
    if (value == "0" || value == "0.0") continue;

    var entry = $"{p.Name}={value}";
    var name = p.Name ?? string.Empty;
    if (name.StartsWith("Comment") || name.StartsWith("Revision"))
        report.ContentLevelLeaks.Add(entry);
    else
        report.MetadataLeaks.Add(entry);
}

MetadataLeaks 가 비어 있어야 파일이 완전히 정리된 것으로 간주됩니다. ContentLevelLeaks 는 정보용이며, Word 댓글 및 추적 변경 저자는 word/document.xml 에 존재하는 본문 콘텐츠이므로 메타데이터 API 대신 Aspose.Words 와 같은 콘텐츠 편집 라이브러리를 사용해야 합니다.

Why not just call Sanitize on everything?

대부분의 문서는 아직 사용 중이기 때문입니다. Sanitize() 는 감지된 모든 패키지를 삭제하며, 여기에는 레코드 시스템과 검색 인덱스가 의존하는 Title, Subject, Keywords 도 포함됩니다. 파일이 내부에서 순환하는 동안에는 대상 패스를 사용해 설명 메타데이터는 유지하고, 조직을 떠나는 복사본에 대해서만 전체 삭제를 수행하십시오.

Side-by-Side: Before vs. After

Manual inspection GroupDocs.Metadata for .NET
Selectivity 전체 제거, 설명 메타데이터 포함 속성 그룹당 하나의 프레디케이트
Coverage 대화 상자가 노출하는 필드 라이브러리가 감지하는 모든 패키지, 사용자 정의 OOXML 파트 포함
Record 없음 작업당 반환되는 영향을 받은 개수
Verification 다시 열어 확인 FindProperties 스캔과 메타데이터 및 콘텐츠 수준 목록
Batch of 200 files 200번 클릭 한 번의 루프, 다섯 번의 작업, 파일당 한 줄 로그

동작을 바꾸는 행은 Record 행입니다. 모든 패스가 개수를 반환하면 정리 작업은 누군가가 기억해 두어야 할 단계가 아니라 파이프라인이 검증할 수 있는 데이터가 됩니다. 테스트의 임계값, 감사 테이블의 필드, 야간 작업을 실패하게 하는 조건 등으로 활용할 수 있습니다. 이는 수동 절차가 어떤 수준의 규율에서도 제공할 수 없는 결과입니다.

Real-World Example: The Pre-Send Hook

지원 포털에서 직원이 고객 티켓에 문서를 첨부할 수 있습니다. 이제 첨부 파일 핸들러는 파일이 저장되기 전에 신원 패스와 서버 패스를 실행하고, 두 개의 카운트를 티켓에 기록한 뒤 저장된 복사본에 대해 누수 검사를 수행합니다. 메타데이터 누수 리스트가 비어 있지 않으면 해당 속성을 명시한 메시지와 함께 업로드가 거부되어, 파일을 첨부한 사람이 고객에게 전달되기 전에 즉시 문제를 인지할 수 있습니다.

이 훅이 실용적인 이유는 두 가지입니다. 패스가 새 경로에 기록되므로 원본 파일은 직원 개인 저장소에 그대로 남아 자동화 단계에 의해 손실되지 않습니다. 또한 카운트가 첨부 파일 옆 티켓 레코드에 저장되므로 “이 문서에서 무엇이 제거되었는가”에 대한 답이 추정이 아닌 저장된 숫자가 됩니다.

실제 템플릿에 이 검사를 처음 적용했을 때, Manager 값이 정체성 패스에 의해 몇 초 전에 삭제되었지만 기업 템플릿이 저장 시 다시 기록된 것을 확인했습니다. 삭제 호출은 문서대로 동작했으며, 문제는 그 주변 파이프라인이었고, 읽어 본 결과에서만 문제가 드러났습니다.

What Else Can You Do with GroupDocs.Metadata?

동일한 프레디케이트 엔진을 활용할 수 있습니다. 두 버전의 문서 속성을 비교하면 소유권 변경 및 리뷰 과정 외의 재작성 내역을 파악할 수 있으며, metadata scrubbing overview 에서는 인터랙티브 도구가 API 기반 패스와 어떻게 보완되는지 설명합니다. 태그 시스템이 포맷을 초월하므로 여기서 만든 신원 프레디케이트는 PDF, 이미지, 오디오 파일에도 별도 수정 없이 적용됩니다.

이식성을 고려하는 것이 좋습니다. MetadataProperty 에 대한 람다식으로 작성된 정리 규칙은 일반 C# 코드이므로 공유 라이브러리에 보관하고, 고정 문서에 대한 단위 테스트를 수행한 뒤, 필요에 따라 다양한 서비스에서 사용할 수 있습니다. 예를 들어 내보내기 엔드포인트, 정기 레코드 작업, 릴리스 전 문서 첨부 파일을 정리하는 빌드 단계 등에서 적용할 수 있습니다. 규칙은 한 곳에 유지되고 호출 위치만 바뀝니다.

Conclusion

네 개의 대상 패스, 하나의 전체 정리, 하나의 검증 스캔으로 구성된 이 세트는 Office 문서에 대한 실용적인 범위를 모두 포괄합니다. 파일이 순환 중일 때는 설명 메타데이터를 유지하고, 조직을 떠날 때는 모든 메타데이터를 삭제하며, 결과를 언제든 증명할 수 있습니다. 샘플을 복제하고 실제 리뷰 라운드를 거친 문서에 실행해 보세요. 반환되는 영향을 받은 개수는 예상보다 높게 나오는 경우가 많으며, 이것이 바로 이 접근법의 핵심 포인트입니다.

Additional Resources