Factur-X 또는 ZUGFeRD 송장은 하나의 파일 이름 안에 두 개의 문서가 있는 형태입니다. 외부 문서는 보존 판독기(archival reader)가 향후 10년 동안 수용해야 하는 PDF/A-3 컨테이너입니다. 내부 문서는 구매자의 회계 시스템이 EN 16931을 기준으로 파싱(parse)해야 하는 XML 송장입니다. 깨진 송장이 프로덕션 환경으로 배포되는 이유는 첫 번째 문서를 올바르게 만들면 두 번째 문서는 저절로 해결될 것이라고 믿는 착각 때문입니다. 실제로는 그렇지 않습니다. 파일이 완벽한 PDF/A-3이면서도 어떤 세무 당국도 받아들이지 않을 XML을 지닐 수 있으며, 교과서적인 EN 16931 XML을 담고 있으면서도 보존성 검증에 실패하는 컨테이너 안에 있을 수도 있습니다. 이 두 계층은 서로에 대해 아무것도 모르는 두 개의 각기 다른 도구에 의해 검증되며, 제대로 된 파이프라인이라면 두 가지 모두를 충족해야 합니다
두 가지 검증기, 두 가지 서로 다른 질문
veraPDF는 PDF/A의 참조 구현(reference implementation)입니다. 이를 송장에 대고 돌리면 한 가지 질문에 대답합니다. 이 파일이 규칙을 준수하는(conformant) PDF/A-3 파일입니까? 이 도구는 ISO 19005-3이 관여하는 사항들을 검사합니다. 모든 글꼴이 내장(embedded)되어 있는지, OutputIntent가 존재하는지, XMP 메타데이터가 올바른 파트와 호환성 수준을 선언하고 있는지 등입니다. 전자 송장의 경우, XML이 /AFRelationship과 문서 카탈로그의 /AF 배열 항목을 갖춘 내장 파일(embedded file) 형태로 실려 있기 때문에 PDF/A-3가 요구하는 연관 파일(associated file) 배관도 검사합니다. veraPDF는 송장의 총액이 제대로 합산되는지 여부에 대해서는 아무 말도 하지 않는데, 이는 도구의 소관이 아니기 때문입니다
Mustang은 Mustangproject에서 만든 오픈소스 검증기입니다. 이 도구는 직교(orthogonal)하는 질문을 던집니다. 내장된 XML이 유효한 송장인가? 이 도구는 선언된 프로파일의 스키마에 대해 XML을 실행한 다음, EN 16931 비즈니스 규칙과 그 위에 겹쳐진 국가별 규칙 세트(XRechnung의 CIUS 등)를 적용합니다. 총액 조건에 따라 판매자의 VAT 식별자가 존재하는지, 허용 금액(allowance)과 청구 금액(charge)이 문서의 총액과 일치하는지, XML에 있는 프로파일 URN이 파일이 주장하는 바와 일치하는지를 검사합니다. Mustang은 자신을 둘러싼 PDF가 글꼴을 내장하고 있는지 여부에는 관심이 없는데, 그것은 veraPDF가 할 일이기 때문입니다
두 도구 중 어느 것도 다른 도구의 상위 집합(superset)이 아닙니다. veraPDF는 말이 안 되는 XML을 감싼 구조적으로 완벽한 컨테이너를 통과시킵니다. Mustang은 OutputIntent가 누락된 컨테이너에 싸여 있는 완벽한 XML을 통과시킵니다. 각각은 상대방이 보지 못하는 바로 그 부류의 결함만을 잡아냅니다. 진지한 검증 하네스(validation harness)가 두 가지 도구를 모두 실행하고 양쪽이 모두 동의할 때만 파일을 배포 가능한(shippable) 것으로 간주하는 이유가 바로 이것입니다
검증 매트릭스
라이브러리가 이 두 가지 관문을 모두 통과하는 파일을 생성한다는 것을 증명하기 위해, 검증 하네스는 매트릭스를 구축합니다. 유럽의 파이프라인이 실무에서 마주치는 범위를 포괄하는 6개의 송장 프로파일이 있습니다. 바로 Factur-X EN 16931, Factur-X BASIC, Factur-X EXTENDED France B2B 변형, XRechnung 3.0, ZUGFeRD 1.0 COMFORT, 그리고 ZUGFeRD 2.0 BASIC입니다. 레벨 B와 레벨 U의 요구 사항이 유니코드 매핑(Unicode mapping)에서 엇갈려 어느 하나를 통과한 파일이 다른 하나에서는 실패할 수 있기 때문에, 각 프로파일은 두 가지 PDF/A 하위 호환성 수준(sub-conformance levels)인 3b와 3u에 맞춰 생성됩니다. 6개 프로파일 곱하기 2개 수준으로 총 12개의 파일이 만들어지며, 이들 모두는 GUI 샘플과 함께 배포되는 동일한 코드 경로를 통해 헤드리스(headless) 방식으로 빌드됩니다. 즉, 테스트 대상이 되는 결과물(artifacts)은 테스트를 위해 수작업으로 조정된 것이 아닙니다
생성기(generator)가 이 12개 파일을 모두 작성하고, 스크립트가 각 파일을 두 검증기에 입력합니다. 첫 번째 전체 실행에서 veraPDF는 12개를 모두 통과시켰습니다. 연관 파일이 등록되고, XMP 호환성이 선언되고, 출력 의도(output intents)가 자리를 잡는 등 컨테이너 배관은 전반적으로 올바르게 되어 있었습니다. Mustang은 8개를 통과시켰습니다. 4개의 송장은 구조적으로 유효한 PDF/A-3 파일이면서도 비즈니스 규칙 검증기가 거부하는 XML을 담고 있었습니다. 이것은 바로 도구 두 개를 사용하는 접근법이 수면 위로 드러내기 위해 존재하는 분할(split) 지점입니다. 하네스가 veraPDF 하나만 신뢰했다면 이 4개의 파일은 완성된 것처럼 보였을 것입니다
간극을 메운 두 가지 수정 사항
Mustang에서 실패한 4건은 크게 두 가지 원인에서 비롯되었으며, 여러분이 이 프로파일들을 직접 생성하기 전에 각 수정 사항의 세부 내용을 알아둘 가치가 있습니다
첫 번째는 Factur-X EXTENDED France B2B 프로파일이었습니다. 원본 생성기는 내부 라벨을 호환성 수준(conformance level)으로 전달하고 내부 URN을 가이드라인으로 전달했으며, Mustang은 잘못된 호환성 값 오류와 지원되지 않는 프로파일 유형 오류를 연달아 내며 파일을 거부했습니다. 그 이유는 XMP의 fx:ConformanceLevel 필드가 자체 프로파일 명명법을 위해 마련된 자유 텍스트 슬롯(free-text slot)이 아니기 때문입니다. Factur-X는 여기에 정확히 5가지 표준 값을 정의하고 있습니다. MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED입니다. XMP 메타데이터의 관점에서 프랑스 전용 B2B 송장은 여전히 EXTENDED 프로파일 문서입니다. 송장의 프랑스적인 특징은 6번째 호환성 값을 새로 만들어내서 표현하는 것이 아닙니다. 국가 코드 FR, 그리고 EN 16931을 준수하는 CIUS임을 표시하는 urn:cen.eu:en16931:2017#conformant# 접두사가 포함된 XML 내의 가이드라인 식별자를 통해 표현됩니다. 국가 코드 FR과 함께 올바른 가이드라인 URN을 갖춘 표준 EXTENDED 값을 전달하자 파일이 규칙을 준수하게 되었습니다
라이브러리 API 측면에서는 이것이 AddFacturXAssociatedFileFromString 호출 시 호환성(conformance), 국가(country), 가이드라인(guideline)을 알맞게 정렬하는 것에 해당합니다. 호환성 수준 인수는 표준 토큰(token)을 실어 나르고, 국가 코드 인수는 FR을 운반하며, 가이드라인 URN은 여러분이 전달하는 XML 바이트 안에 존재합니다
var
FileID: Integer;
begin
PDF.SetPDFAMode(5); // PDF/A-3b
PDF.NewDocument;
// ... 사람이 읽을 수 있는 송장 페이지 그리기 ...
// ExtendedXML은 다음과 같은 형태의 EN 16931 가이드라인 URN을 지닙니다
// urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
FileID := PDF.AddFacturXAssociatedFileFromString(
ExtendedXML,
'EXTENDED', // 내부 라벨이 아닌, 표준 fx:ConformanceLevel
'factur-x.xml',
'Factur-X EXTENDED invoice',
'Alternative', // /AFRelationship
'1.0',
'FR'); // 프랑스 B2B는 호환성 수준이 아닌 국가 코드로 표시됩니다
if FileID = 0 then
raise Exception.Create('Factur-X attachment rejected');
PDF.SaveToFile('02_Factur-X-EXTENDED-FR_PDFA-3b.pdf');
end;
두 번째 원인은 ZUGFeRD 1.0 COMFORT 프로파일이었고 메타데이터와는 아무런 관련이 없었습니다. ZUGFeRD 1.0은 :1p0 XSD를 대상으로 검증되는데, 이는 산문체의 요약문이 시사하는 것보다 카디널리티(cardinality)에 대해 훨씬 더 엄격합니다. XSD는 헤더 정산 합산(header settlement summation)인 ram:SpecifiedTradeSettlementMonetarySummation이 ram:ChargeTotalAmount와 ram:AllowanceTotalAmount를 각각 정확히 한 번씩 포함할 것을 요구합니다. 생성된 XML은 두 가지를 모두 생략했고, 그래서 Mustang은 이 요소들이 정확히 한 번 나타나야 한다고 보고했습니다. 스키마가 minOccurs가 1이라고 지정할 때 이 요소들은 선택 사항(optional)이 아닙니다. 이 두 요소를 XSD 순서에 맞게 ram:LineTotalAmount 바로 뒤에 내보내면서(청구나 허용 금액이 없을 때는 값 0.00을 부여하여) 스키마를 충족시켰습니다. 0은 존재하는 요소이며, 요소의 부재는 스키마 위반입니다. 이 두 가지 수정 사항을 적용하자 매트릭스는 Mustang에서 12개 중 12개를 통과했고 veraPDF에서도 12개 중 12개 통과를 유지했습니다
무효에서 유효로 판정을 뒤집는 XRechnung 필드
XRechnung은 그 자체로 주목할 가치가 있는데, 왜냐하면 독일 CIUS는 기본 EN 16931 세트에는 없는 비즈니스 규칙들을 추가하며, 이들은 언뜻 보기에 문서에 아무런 문제가 없는 것처럼 보이는 방식으로 실패를 일으키기 때문입니다. 이 중 두 가지는 전자 주소(electronic addresses)와 관련이 있습니다. BT-34는 판매자의 전자 주소이고 BT-49는 구매자의 전자 주소로서, 독일 공공 부문 포털(public-sector portal)이 송장을 전달하고 확인하기 위해 사용하는 라우팅 엔드포인트(routing endpoints)입니다. 기본 EN 16931 모델은 이를 선택 사항으로 취급하지만 XRechnung은 그렇지 않습니다. 둘 중 하나라도 생략하면 송장이 형식에 잘 맞고(well-formed) 스키마상 유효하더라도 거부당하게 됩니다
세 번째는 판매자의 연락처 전화번호가 존재할 것을 요구하는 BR-DE-6 규칙입니다. 이것은 개발자가 데이터라기보다는 보여주기식 요소라고 느껴서 빠뜨리기 쉬운 종류의 필드입니다. 게다가 이 요소가 없으면 누락된 지점이 명백히 드러나는 대신, 판매자 연락처 그룹을 가리키는 검증 실패가 발생합니다. Mustang 상에서 XRechnung 파일을 무효에서 유효 상태로 옮겨주는 것은 바로 BT-34, BT-49, 그리고 판매자 전화번호를 제공하는 것입니다. 그리고 이 세 가지 모두 XML 안에 존재하기 때문에 이들을 제공한다고 해서 veraPDF가 보는 내용이 바뀌지는 않습니다
라이브러리 출력을 검증기에 연결하기
하네스 이면에 있는 아키텍처 관점은 모든 비즈니스 시스템에 일반적으로 적용됩니다. PDF 라이브러리는 규칙에 맞는 컨테이너를 작성하고 XML을 내장합니다. 이 라이브러리는 EN 16931 비즈니스 규칙 당국(authority)이 되려고 시도하지 않으며, 시도해서도 안 됩니다. 라이브러리의 ValidateFacturXInvoice는 카탈로그의 /AF 배열, 내장된 파일의 이름 트리, XMP의 DocumentFileName, 프로파일, 가이드라인, /AFRelationship이 모두 일치하는지 컨테이너의 일관성을 검사하지만, 세금 코드를 검증하거나 금액을 대조하지는 않습니다. 노동 분업을 올바르게 하는 방법은 하네스가 XML을 Mustang에 건네주는 것과 똑같이, 비즈니스 시스템이 XML을 추출하여 전용 송장 검증기에 넘겨주는 것입니다
파일을 다시 읽어보면 실제로 무엇이 쓰여 있는지 알 수 있습니다. DetectFacturXInvoice는 송장이 인식되었는지 여부를 보고하고, GetFacturXInvoiceInfo는 태그별로 메타데이터 필드를 읽어 들입니다. 태그 1은 내장된 파일 이름, 태그 2는 XMP DocumentFileName, 태그 5는 호환성 수준, 태그 6은 가이드라인 식별자, 태그 7은 /AFRelationship입니다. 다시 읽어온 호환성 수준이 내부 라벨이 아닌 표준 토큰(token)인지 확인하는 것이 파일이 빌드 환경을 떠나기 전에 EXTENDED 프로파일의 실수를 잡아내는 가장 비용이 적게 드는 방법입니다
function ExtractAndInspect(const PdfPath: string): AnsiString;
var
Profile, Guideline: WideString;
begin
Result := '';
PDF.LoadFromFile(PdfPath);
if PDF.DetectFacturXInvoice = 1 then
begin
Profile := PDF.GetFacturXInvoiceInfo(5); // fx:ConformanceLevel
Guideline := PDF.GetFacturXInvoiceInfo(6); // XML 가이드라인 ID
Writeln('Profile: ', Profile);
Writeln('Guideline: ', Guideline);
// 원시 XML을 전용 EN 16931 / Mustang 검증기에 넘깁니다.
Result := PDF.ExtractFacturXXMLToString;
end;
end;
ExtractFacturXXMLToString은 원시 XML 바이트를 파일에 쓰거나 검증기 프로세스로 스트리밍할 준비가 된 AnsiString 형태로 반환합니다. 테스트 하네스에서 이 대상(target)은 명령줄 jar를 통해 호출되는 Mustang이며, veraPDF도 동일한 파일에 대해 동일한 패스(pass)에서 실행됩니다. 연결 고리는 작습니다. 콘솔 생성기(console generator)인 EInvoiceValidation.dpr이 샘플의 공유 송장 모델을 사용하여 12개의 파일을 작성하고, 스크립트 run-validation.ps1이 출력 디렉토리에 대해 두 검증기를 모두 구동하고 통과 및 실패 테이블을 출력합니다. 2단계로 이루어진 이러한 구조, 즉 라이브러리를 통해 생성하고 외부 검증기를 통해 검증하는 것은 지속적 통합(continuous-integration) 작업이 송장 생성과 관련된 모든 변경 사항에 대해 실행되어야만 하는 방식입니다. 왜냐하면 파일이 두 계층 모두를 만족시키는지 아는 유일한 방법은 두 도구 모두에 묻는 것뿐이기 때문입니다
파이프라인에서 서명 전에 컨테이너를 증명(certify)해야 하는 경우, 이 작업의 프리플라이트(preflight) 측면은 Delphi에서의 PDF/A 및 PDF/UA 프리플라이트 안내서에서 다루며, 보다 광범위한 증명-후-서명(certify-then-sign) 흐름은 호환성 및 서명 작업대(compliance and signing workbench)에 설명되어 있습니다. 이 두 기능은 모두 이곳에서 사용된 PDF/A, 연관 파일, 메타데이터 API와 함께 Delphi 및 C++Builder용 Delphi PDF Library의 일부로 제공되는 것과 동일한 생성 경로 위에서 구축됩니다