Factur-X 인보이스를 다 만들었고 컨테이너 검사는 하나도 빠짐없이 통과합니다. 카탈로그에 /AF 배열이 있고, EmbeddedFiles 이름 트리는 올바른 파일 명세로 해석되며, 임베드된 factur-x.xml은 Alternative라는 올바른 /AFRelationship을 가지고, 내장 ValidateFacturXInvoice는 1을 반환합니다. 그런 다음 세무 포털이 쓰는 참조 검사기인 veraPDF에 같은 파일을 넣으면, 문서 전체가 유효한 PDF/A-3이 아니라고 판정합니다. 구조는 맞습니다. 문제는 메타데이터이고, 이 실패는 전자 인보이스 워크플로 전체에서 놓치기 가장 쉬운 축에 듭니다
그 이유는 끝까지 이해할 값어치가 있습니다. 보이는 페이지나 첨부와는 아무 상관이 없고 XMP가 자기 자신을 어떻게 서술하는지와 전적으로 관련된, 한 부류의 PDF/A 결함을 설명해 주기 때문입니다. 이것이 초록불 컨테이너 검사 뒤에 숨어 있는 함정입니다
파일을 실패시키는 네 가지 속성
Factur-X 인보이스는 하류 소프트웨어가 임베드된 XML을 파싱하지 않고도 인보이스 프로파일을 읽을 수 있도록 XMP 패킷에 커스텀 속성 네 개를 씁니다. 이들은 fx 접두부 아래 Factur-X 네임스페이스에 자리합니다. fx:DocumentFileName, fx:DocumentType, fx:Version, fx:ConformanceLevel입니다. 이 PDF가 버전 1.0의 factur-x.xml이라는 EN 16931 인보이스를 담고 있다는 사실을 리더가 알기 위해 필요한 바로 그 메타데이터입니다
그런데 이 넷 중 어느 것도 PDF/A가 미리 정의한 XMP 스키마에 속하지 않습니다. Dublin Core, XMP Basic, PDF, PDF/A 식별 스키마는 준수 리더가 알고 있지만 fx:는 그렇지 않습니다. veraPDF가 XMP를 훑다가 자신이 모르는 네임스페이스의 속성에 닿으면, 그 속성이 무슨 뜻인지 알려 줄 선언을 찾습니다. 그 선언이 없으면 ISO 19005-3 6.6.2.3.1 조항 위반으로 보고하는데, 이 조항은 미리 정의된 스키마에서 온 것이 아닌 모든 속성이 PDF/A 확장 스키마에 기술되어야 한다고 요구합니다. 선언되지 않은 속성 넷, 파일이 거부될 경로 넷, 그리고 그중 어느 것도 컨테이너 검사에는 보이지 않습니다
PDF/A는 왜 맨 커스텀 속성을 거부할까요
PDF/A가 무엇을 위한 규격인지 떠올리기 전까지 이 규칙은 현학적으로 보입니다. 이 포맷은 2026년의 관례를 한 번도 들어 본 적 없는 소프트웨어가 수십 년 뒤에도 파일을 열고 이해할 수 있게 하려고 존재합니다. 준수 리더는 참조할 외부 레지스트리 없이 문서만으로 그 문서를 이해할 수 있어야 합니다
커스텀 메타데이터는 파일이 자기 자신에 대한 설명을 함께 지니지 않는 한 그 약속을 깨뜨립니다. 맨 fx:ConformanceLevel 속성만 주어지면, 미래의 리더는 fx 접두부가 어떤 네임스페이스 URI에 묶이는지, 값이 텍스트인지 날짜인지 정수인지, 그 속성이 문서 자체를 서술하는지 외부 자원을 서술하는지 알 수 없습니다. PDF/A 확장 스키마 메커니즘이 그 틈을 메웁니다. 이 메커니즘은 파일이 고정된 XMP 구조 안에서 네임스페이스와 접두부를, 그리고 각 속성마다 값 타입과 internal 또는 external 범주를 선언하게 해 줍니다. 그 선언이 존재하는 순간 속성은 자기 서술적이 되고 6.6.2.3.1 조항이 충족됩니다. 선언이 없으면 검증기는 그 속성을 이해 불가로 취급하고 파일을 실패시키는 것 말고는 선택지가 없습니다. 여기서 범주 구분이 중요합니다. 이런 인보이스 속성은 PDF 프로세서 바깥에서 오는 데이터를 서술하므로 internal이 아니라 external로 선언됩니다
확장 스키마 선언에 담기는 내용
선언은 XMP 패킷 안의 rdf:Description이며 AIIM이 정의한 세 네임스페이스 pdfaExtension, pdfaSchema, pdfaProperty를 사용합니다. pdfaExtension:schemas 백 안에는 Factur-X 스키마의 이름을 대고 pdfaSchema:namespaceURI와 pdfaSchema:prefix를 제시한 다음 네 속성을 pdfaSchema:property 시퀀스에 나열하는 스키마 항목 하나가 들어갑니다. 각 속성은 이름과 Text라는 pdfaProperty:valueType, 그리고 external이라는 pdfaProperty:category를 지닙니다. 아래 예시 마크업이 그 블록의 모양을 보여 줍니다
<rdf:Description rdf:about=""
xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"
xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#">
<pdfaExtension:schemas>
<rdf:Bag>
<rdf:li rdf:parseType="Resource">
<pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>
<pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
<pdfaSchema:prefix>fx</pdfaSchema:prefix>
<pdfaSchema:property>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<pdfaProperty:name>DocumentFileName</pdfaProperty:name>
<pdfaProperty:valueType>Text</pdfaProperty:valueType>
<pdfaProperty:category>external</pdfaProperty:category>
<pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
</rdf:li>
<!-- DocumentType, Version, ConformanceLevel도 같은 방식으로 선언 -->
</rdf:Seq>
</pdfaSchema:property>
</rdf:li>
</rdf:Bag>
</pdfaExtension:schemas>
</rdf:Description>
네임스페이스 URI와 접두부는 고정된 문자열이 아닙니다. 프로파일을 따라갑니다. Factur-X 문서는 fx 접두부와 함께 urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#을 쓰지만, zugferd-invoice.xml로 선택된 ZUGFeRD 2.0 파일은 자체 스키마 이름 아래 다른 URI로 해석됩니다. 확장 스키마는 속성 블록이 실제로 쓰는 것과 같은 네임스페이스 URI를 선언해야 하며, 그렇지 않으면 검증기는 여전히 둘을 연결하지 못합니다. PDF Library for Delphi는 여러분이 넘긴 파일 이름과 버전에서 두 값을 모두 도출하므로 선언과 속성 블록은 언제나 일치합니다
헬퍼가 양쪽 절반을 함께 쓰는 방법
PDF Library for Delphi에서는 그 XML을 손으로 조립하지 않습니다. 문서를 PDF/A-3 모드에 넣고 메서드 하나를 부르면 됩니다. 가장 먼저 정할 것은 준수 플래그입니다. Factur-X는 PDF/A-3을 요구하기 때문입니다. SetPDFAMode(7)을 호출하면 PDF/A-3u 레벨이 선택되어 식별 스키마에서 pdfaid:part는 3으로, pdfaid:conformance는 U로 설정됩니다. 이제 XMP 패킷은 인보이스 메타데이터가 추가되기 전에 이미 올바른 part와 conformance를 지닙니다
var
FileID: Integer;
begin
PDF.SetPDFAMode(7); // PDF/A-3u: pdfaid:part=3, conformance=U
PDF.NewDocument;
// 사람이 읽는 인보이스 페이지를 여기서 그립니다
FileID := PDF.AddFacturXAssociatedFileFromString(
InvoiceXML, // 원시 UTF-8 XML 바이트
'EN16931', // ConformanceLevel
'factur-x.xml', // 임베드 파일 이름
'Factur-X invoice XML', // /Desc 텍스트
'Alternative', // /AFRelationship
'1.0', // 프로파일 버전
''); // 선택적 국가 코드
if FileID = 0 then
Exit; // PDF/A-3이 아니거나 XML/프로파일 불일치
PDF.SaveToFile('factur-x.pdf');
end;
AddFacturXAssociatedFileFromString 호출 한 번이 실패하던 파일에 빠져 있던 일을 해냅니다. 여러분이 지정한 관계로 XML을 PDF/A-3 연관 파일로 임베드하고, 선택된 프로파일의 스키마 이름, 네임스페이스 URI, 접두부와 함께 네 개의 fx 속성을 기록합니다. 문서를 저장할 때 ApplyFacturXMetadata라는 내부 단계가 속성 블록과 그에 대응하는 pdfaExtension:schemas 선언을 모두 XMP 패킷에 주입하므로, 커스텀 속성은 이미 서술된 상태로 도착합니다. 문서가 PDF/A-3 모드가 아니거나 XML이 선언된 프로파일과 맞지 않으면 메서드는 0을 반환하는데, 이는 애초에 잘못된 인보이스가 파일에 들어가지 못하게 막는 것과 같은 가드입니다
컨테이너 검사가 볼 수 없는 사각지대
이 부분은 분명히 이름 붙여 둘 만합니다. 버그가 숨는 이유가 바로 여기에 있기 때문입니다. ValidateFacturXInvoice는 컨테이너를 검사합니다. 카탈로그에 /AF 항목이 있는지, EmbeddedFiles 이름 트리가 존재하는지, 인보이스 XML이 있는지, 임베드 파일 이름이 프로파일과 맞는지, XML 안의 가이드라인 ID가 준수 수준과 일치하는지, /AFRelationship이 PDF/A-3이 허용하는 값인지 확인합니다. 모두 실제 검사이며 실제 결함을 잡아냅니다. GetFacturXValidationIssues는 MissingCatalogAF, NotPDFA3, ConformanceGuidelineMismatch, InvalidAFRelationship, InvalidFileNameProfile 같은 식별자로 그 결함들을 이름과 함께 보고합니다
검사하지 않는 것은 XMP 확장 스키마가 존재하고 올바른지 여부입니다. 컨테이너는 흠잡을 데 없지만 fx 속성이 선언되지 않은 파일은 모든 이슈 검사를 통과하고 1을 반환합니다. 그 목록 어디에도 pdfaExtension:schemas 블록을 들여다보는 항목이 없기 때문입니다. 손으로 만든 인보이스나, 선언 없이 속성 블록만 쓴 파이프라인이 만들어 낸 인보이스가 내장 검증기를 유유히 통과하고도 여전히 6.6.2.3.1 조항에서 veraPDF에 걸리는 이유가 정확히 이것입니다. 컨테이너 검증기와 PDF/A 메타데이터 검증기는 서로 다른 질문에 답하며, 두 번째 질문에 답하는 것은 완전한 PDF/A 검사기뿐입니다
어느 계층이 깨졌는지 알기 위한 이슈 읽기
두 계층은 서로 독립적으로 실패하므로, 올바른 진단 습관은 컨테이너 이슈를 먼저 읽고 깨끗한 결과를 오직 컨테이너에 대한 진술로만 받아들이는 것입니다. 결코 PDF/A 메타데이터에 대한 진술로 받아들이지 마십시오. 내장 검증을 돌리고 이슈 목록을 모은 다음, 외부 도구에 손을 뻗기 전에 그것부터 처리하십시오
var
Issues: WideString;
begin
if PDF.ValidateFacturXInvoice = 0 then
begin
Issues := PDF.GetFacturXValidationIssues('|');
// 컨테이너 수준 식별자, 예를 들면:
// MissingCatalogAF, NotPDFA3, MissingEmbeddedFilesNameTree,
// ConformanceGuidelineMismatch, InvalidAFRelationship
WriteLn('Container issues: ', Issues);
end
else
WriteLn('Container OK; verify XMP extension schema with a PDF/A checker.');
end;
그 호출이 이슈 이름을 돌려주면 잘못은 컨테이너에 있고 메시지가 어느 부분인지 알려 줍니다. 깨끗하게 돌아왔는데도 veraPDF가 여전히 파일을 거부한다면 잘못은 거의 언제나 XMP 확장 스키마이고, 해법은 속성 블록을 손수 만드는 대신 AddFacturXAssociatedFileFromString이 메타데이터를 쓰게 맡기는 것입니다. 이 두 질문을 머릿속에서 갈라 두는 습관이 영문 모를 거부를 한 줄짜리 진단으로 바꿔 줍니다. 컨테이너 문제는 이슈 목록으로 드러나고, 스키마 선언 문제는 PDF/A 검증기로만 드러나며, 둘을 혼동하는 것이 버그를 숨겨 주는 요인입니다
파일이 빌드를 떠나기 전에 프리플라이트 패스를 돌리는 방법을 포함한 더 넓은 PDF/A 및 PDF/UA 준수 그림은 PDF/A와 PDF/UA 프리플라이트 안내에서 다룹니다. 인보이스가 접근성까지 갖춰야 한다면, PDF/A-3a와 태그드 PDF가 의존하는 구조 트리는 태그드 PDF 접근성 글의 주제입니다. 여기서 설명한 확장 스키마 처리는 이 블로그 곳곳에 문서화된 Factur-X, ZUGFeRD, XRechnung 프로파일 지원과 함께 PDF Library for Delphi Delphi PDF Library의 일부로 제공됩니다