💡 전체 작동 예제는 GitHub에서 확인할 수 있습니다:
remove-pii-from-office-metadata-java
컴플라이언스 과제: 왜 수동 메타데이터 검토는 규모에 맞지 않는가
레코드 팀이 외부 감사인에게 200개의 문서를 전달합니다. 누군가는 모든 페이지를 읽었습니다. 하지만 아무도 속성을 읽지 않았으며, 개인 데이터는 바로 그 속성에 들어 있습니다: Author에 있는 분석가, LastSavedBy에 있는 두 번째 분석가, Manager에 있는 부서장, Company에 있는 자회사, 마감 전날 밤의 LastPrinted 타임스탬프, 그리고 SharePoint를 통과한 모든 문서에는 승인자 ID와 워크플로 경로가 있습니다.
Metadata sanitization은 Word, Excel, PowerPoint 파일에서 신원 정보를 담고 있는 속성을 제거하고, 결과를 다시 읽어 남아 있는 항목을 보고하는 Java용 GroupDocs.Metadata 워크플로입니다. 이 문서는 컴플라이언스 팀이 구축할 워크플로를 단계별로 설명합니다: 어떤 속성 그룹이 존재하는지, 각 그룹에 맞는 제거 규칙은 무엇인지, 전체 삭제가 대상 삭제를 대체하는 시점, 그리고 검증이 체크리스트가 아니라 동일 작업 안에 포함되어야 하는 이유 등을 다룹니다.
규모 문제는 제거가 어렵기 때문이 아니라, 수동 검토가 기록을 남기지 않기 때문입니다. 감사인이 “이 파일에서 어떤 필드가 언제 제거되었는가?”를 물으면 숫자가 필요하지만, 속성 대화상자는 아무것도 제공하지 않습니다.
일반적인 정리 도구가 여기서 통하지 않는 이유
Windows 속성 대화상자는 한 번에 하나의 파일만 편집하고, 일부 필드만 접근합니다. Office의 Document Inspector는 대화형으로 실행되므로 야간 작업에 사용할 수 없습니다. 두 방법 모두 사용자 정의 OOXML 파트를 건드리지 않으며, 파이프라인이 다시 읽을 수 있는 형태로 기록하지도 않습니다.
docProps/core.xml 및 docProps/custom.xml을 직접 편집하는 팀은 유지보수 부담을 안게 됩니다: 필드당, 포맷당 XPath 식을 작성하고, 생산 애플리케이션이 이름을 바꿀 때마다 다시 검토해야 합니다. 또한 분류 질문을 잘못 처리합니다. 패키지마다 속성 이름이 다르기 때문에 이름 목록이 조용히 오래되어 버리고, 더 이상 매치되지 않는 규칙이 이미 깨끗한 파일과 동일하게 보입니다.
해결책: 레코드 워크플로에 적용하는 GroupDocs.Metadata
Java용 GroupDocs.Metadata는 모든 작업을 하나의 속성 검색 엔진을 통해 수행합니다. Specification 객체가 어떤 속성이 매치되는지 결정하고, removeProperties는 매치된 각 속성을 삭제하면서 영향을 받은 개수를 반환하며, findProperties는 동일한 프레디케이트를 읽기 전용으로 실행합니다. 속성에는 태그가 포함되어 있어 Tags.getPerson().getCreator()와 같이 포맷이나 패키지에 관계없이 저자 스타일 필드를 식별할 수 있습니다.
Java에는 removeProperties에 대한 람다 오버로드가 없는데, 이는 여기서 장점이 됩니다: 각 규칙이 객체이며, 객체는 재사용 가능하기 때문입니다. 그룹을 정리하는 동일한 Specification 인스턴스를 검증 스캔에 전달하면, 검증이 정리와 어긋나지 않게 됩니다.
단계별 정화 파이프라인 구현
단계 1 – 태그로 신원 그룹 삭제
.or(...) 로 결합된 네 개의 태그 사양이 creator, editor, manager, company 를 포괄합니다. 다른 것은 이동하지 않으므로 Title, Subject, Keywords 는 레코드 인덱스에 그대로 남습니다.
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.removeProperties(
new ContainsTagSpecification(Tags.getPerson().getCreator())
.or(new ContainsTagSpecification(Tags.getPerson().getEditor()))
.or(new ContainsTagSpecification(Tags.getPerson().getManager()))
.or(new ContainsTagSpecification(Tags.getCorporate().getCompany())));
metadata.save(outputPath);
return affected;
}
FileFormat.Unknown 방어 코드는 겉보기에보다 중요합니다. 이 검사를 빼면 읽을 수 없는 파일이 0개의 제거를 반환하게 되고, 호출자는 이를 처음부터 깨끗한 문서와 구분할 수 없습니다.
단계 2 – 이름으로 필드 패밀리 삭제
댓글 스레드, 리비전 카운터, 서버 필드에는 태그가 없으므로 이름으로 매치합니다. 작은 Specification 서브클래스가 가변 인자 문자열 목록을 받아 세 번의 패스를 하나의 클래스로 처리합니다:
public class NameContainsSpec extends Specification {
private final String[] needles;
public NameContainsSpec(String... needles) {
this.needles = needles;
}
@Override
public boolean isSatisfiedBy(MetadataProperty candidate) {
String name = candidate.getName();
if (name == null) return false;
for (String n : needles) {
if (name.contains(n)) return true;
}
return false;
}
}
편집 타임라인은 레코드 컴플라이언스 팀이 가장 신경 쓰는 부분입니다. 리비전 수와 마지막 인쇄 날짜가 문서가 어떻게 생성됐는지를 설명하기 때문입니다:
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.removeProperties(new NameContainsSpec(
"Revision", "TrackedChange", "LastPrinted", "TotalEditingTime", "EditTime"));
metadata.save(outputPath);
return affected;
}
댓글 패스와 SharePoint 패스는 서로 다른 서브스트링 목록을 사용한 동일 호출입니다: 리뷰 흔적을 위한 Comment, Reviewer, Reviewed와 문서‑서버 필드를 위한 Server, Workflow, Approver, ContentType, Template.
단계 3 – 경계에서 전체 삭제
파일이 조직을 떠날 때는 선택적 삭제가 의미가 없습니다. 한 번의 호출로 라이브러리가 감지하는 모든 메타데이터 패키지를 삭제합니다. 여기에는 대상 프레디케이트가 찾지 못하는 사용자 정의 OOXML 파트도 포함됩니다:
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.sanitize();
metadata.save(outputPath);
return affected;
}
단계 4 – 남은 항목 검증 및 분류
스캔은 findProperties 로 모든 규칙의 합집합을 실행하고, 히트를 두 리스트로 분류합니다. 빈 값과 0 카운터는 건너뛰고, 이름이 Comment, Revision, Inspection 으로 시작하는 항목은 메타데이터가 아니라 본문 내용에 대한 래퍼로 처리합니다:
String value = "";
if (p.getValue() != null && p.getValue().getRawValue() != null) {
value = String.valueOf(p.getValue().getRawValue());
}
if (value.isEmpty() || value.equals("0") || value.equals("0.0")) continue;
String entry = p.getName() + "=" + value;
String name = p.getName() == null ? "" : p.getName();
if (name.startsWith("Comment") || name.startsWith("Revision")
|| name.startsWith("Inspection")) {
report.contentLevelLeaks.add(entry);
} else {
report.metadataLeaks.add(entry);
}
이 분할이 합격/불합격 신호를 정직하게 유지합니다. 메타데이터 누수는 반드시 비어 있어야 하고, 콘텐츠 레벨 누수는 정보 제공용으로 남깁니다. Word 댓글과 추적 변경 저자는 word/document.xml 내부에 존재하며, 메타데이터 라이브러리는 이를 편집하지 않고 보고만 합니다; 이를 제거하려면 Aspose.Words와 같은 콘텐츠 편집 라이브러리가 필요합니다.
언제 대상 패스가 전체 정화보다 나은가?
문서가 아직 작업 중일 때입니다. 리뷰어 사이를 오가는 파일은 검색 및 레코드 분류를 위해 Title, Subject, Keywords 가 필요하고, sanitize() 는 이 세 가지를 모두 삭제합니다. 협업 중에는 신원 및 댓글 패스를 실행하고 설명 필드는 유지한 뒤, 파일이 외부 파티로 넘어가는 순간 전체 삭제를 수행합니다.
실제 워크플로: 외부 감사인을 위한 내보내기 작업
야간 작업을 상상해 보세요. 문서 ID 목록을 읽고, 각 파일을 스테이징 경로에 복사한 뒤, 신원 패스와 서버 패스를 적용하고, 조직을 떠나는 것으로 표시된 파일에 대해 sanitize() 를 호출합니다. 그런 다음 저장된 복사본에 대해 누수 검사를 실행합니다. 각 단계는 파일당 하나의 로그 라인에 영향을 받은 개수를 추가하고, 메타데이터 누수 리스트가 비어 있지 않으면 작업을 실패 처리합니다.
한 번은 40개의 파일에 대해 제거된 항목이 0개라고 보고된 작업을 구현했는데, 결과가 깨끗한 배치처럼 보였습니다. 입력 폴더에 레거시 .doc 바이너리가 있었고, 포맷 방어 코드가 모든 파일에 대해 조기에 반환했으며, 로그에는 “제거할 것이 없음”과 “읽을 것이 없음”을 구분하는 내용이 없었습니다. 포맷을 카운트와 함께 로깅하면 문제가 해결되었습니다.
비즈니스 영향: 이 변화가 가져오는 것
| 측면 | 수동 검토 | GroupDocs.Metadata 파이프라인 |
|---|---|---|
| 커버리지 | 속성 대화상자에 보이는 필드 | 라이브러리가 감지하는 모든 패키지, 사용자 정의 OOXML 파트 포함 |
| 작업 기록 | 누군가 적은 메모 | 파일당 및 속성 그룹당 영향을 받은 개수 |
| 재현성 | 수행자에 따라 달라짐 | 규칙당 하나의 Specification, 모든 파일에 동일하게 적용 |
| 검증 | 파일을 다시 열어 확인 | findProperties 스캔과 양방향 분류 |
| 규모 | 파일당 하나씩 | 동일한 여섯 작업을 내보내기 폴더 전체에 반복 실행 |
GroupDocs.Metadata가 적용되는 다른 시나리오
같은 속성 엔진이 읽기와 삭제를 모두 수행합니다. 두 버전의 문서 메타데이터를 비교하면 어떤 편집 라운드가 변경했는지 확인할 수 있어, 소유권 분쟁이나 프로세스 외에서 재작성된 파일을 찾는 데 유용합니다. 이름 대신 metadata tags 를 사용하면 DOCX, XLSX, PPTX, PDF, 이미지 포맷 전반에 걸쳐 두 경우 모두 이식성이 확보됩니다.
Java용 GroupDocs.Metadata 시작하기
pom.xml 에 GroupDocs Java 저장소를 추가하고 com.groupdocs:groupdocs-metadata 에 의존성을 선언합니다. 라이선스 없이 평가 모드로 실행할 수 있으며, 샘플 DOCX에 대해 여섯 가지 작업을 모두 수행하고 카운트를 확인할 수 있습니다. 신원 패스부터 시작하고, 문서 소스에 따라 필드‑패밀리 패스를 추가하며, 프로덕션에 투입하기 전에 누수 검사를 반드시 포함시키세요.