규칙을 준수하는 전자 송장은 측면에 XML 파일이 스테이플러로 찍힌 PDF가 아닙니다. 그것은 사람이 읽을 수 있는 페이지로서의 송장, 그리고 파일 내부에 연관 파일(associated file)로서 저장된 기계 판독 가능한(machine-readable) Cross Industry Invoice XML로서의 송장, 이렇게 두 번에 걸쳐 송장을 싣고 다니는 단일 PDF/A-3 문서입니다. 이 두 가지 표현 방식은 동일한 송장을 설명합니다. 이러한 이중적 특성(dual nature)은 현재 프랑스와 독일의 Factur-X, 독일어권 시장 전반의 ZUGFeRD, 독일 공공 부문 청구용 XRechnung 등 유럽의 의무 사항이 요구하는 포맷 제품군 전체의 핵심입니다. 이 기사에서는 Delphi에서 PDFlibPas가 이러한 하이브리드 송장을 어떻게 조립하는지, 표준이 어디에서 잘못될 여지를 남겨두는지, 그리고 카탈로그의 한 프로파일이 완전히 별개의 XML 빌더를 필요로 하는 이유에 대해 살펴봅니다
하이브리드 송장이란 실제로 무엇인가
눈에 보이는 페이지와 내장된 XML은 서로 다른 독자를 대상으로 합니다. 결재를 승인하는 사무원은 렌더링된 페이지를 봅니다. 반면 외상매입금(accounts-payable) 시스템은 XML을 섭취하고(ingest), 총액과 세금 명세를 구조화된 필드로 읽어 들인 다음, 사람이 아무것도 입력하지 않아도 해당 항목을 장부에 기입합니다. 이 XML의 의미론적 내용(semantic content)은 송장 데이터 모델을 정의하는 유럽 표준인 EN 16931의 지배를 받습니다. 즉 어떤 필드가 존재하고, 그 필드들이 무엇을 의미하며, 어떤 것이 필수적인지를 정의합니다. EN 16931은 의미론적 모델이지 파일 형식이 아닙니다. Factur-X, ZUGFeRD 2.x, 그리고 XRechnung은 모두 이 모델을 UN/CEFACT Cross Industry Invoice 문서로 구현하는데, 이것이 바로 전송 과정(on the wire)에서 EN 16931 필드를 실어 나르는 구문(syntax)입니다
문서가 보존 가능(archivable)하면서도 스스로를 설명(self-describing)할 수 있으려면, 컨테이너는 ISO 19005-3에 의해 정의된 PDF/A-3이어야 합니다. PDF/A-3는 임의의 내장 파일(embedded files)을 허용하는 호환성 수준(conformance level)으로, 이것이야말로 송장 XML에 정확히 요구되는 사항입니다. PDF/A-2는 그 자체로 PDF/A가 아닌 파일의 내장을 금지하므로 Factur-X 송장은 PDF/A-2가 될 수 없습니다. 따라서 PDF/A-3의 선택은 선호의 문제가 아니라, 보존용 문서에 PDF가 아닌 데이터를 내장하고자 하는 목적에서 직접적으로 비롯된 필수 요구 사항입니다
관계성(relationship)이 Alternative인 이유
바이트를 내장하는 것은 쉬운 부분입니다. ISO 32000 §7.11.4는 원시 XML과 그 매개변수를 담고 있는 객체인 내장 파일 스트림을 정의합니다. 파일을 유효한 연관 파일(associated file)로 만드는 부분은 §14.13으로, 여기에 연관 파일이라는 개념과 /AFRelationship 키가 추가됩니다. 이 키는 내장된 데이터가 그것이 첨부된 콘텐츠와 어떤 관계를 맺고 있는지를 명시하며, Factur-X가 요구하는 값은 Alternative입니다
이 선택이 중요한 이유는 다른 값들이 문서에 대해 거짓된 주장을 펴게 되기 때문입니다. Source는 XML이 시각적 콘텐츠를 생성해 낸 재료, 즉 페이지가 파생되어 나온 원본(master)임을 의미하게 됩니다. Supplement는 XML이 페이지가 보여주는 것 이상으로 정보를 추가하는 것, 즉 렌더링에 포함되지 않은 부가 요소(extra)임을 뜻합니다. 둘 다 Factur-X 송장이 의미하는 바가 아닙니다. XML과 페이지는 하나의 송장에 대한 동등한 두 가지 표현으로, 동일한 법적 내용을 두 가지 형태로 전달할 뿐입니다. Alternative는 시각적 콘텐츠를 동등하게 대체하는 표현이라는 사실을 정확히 말해주는 값입니다. Factur-X 파일에서 다른 관계성을 읽어 들이는 검증기는 당연하게도 이를 거부할 텐데, 관계성이라는 것은 이 첨부 파일의 용도가 무엇인지에 대한 기계 판독 가능한(machine-readable) 주장이기 때문입니다
프로파일 카탈로그
PDFlibPas와 함께 제공되는 전자 송장(E-Invoice) 샘플은 InvoiceModel.pas에 레코드 배열로 정의된 6가지 프로파일에 걸쳐 동일한 생성 경로를 구동합니다. 각 프로파일은 작성기(writer)에 필요한 값들을 담고 있습니다. 표시 이름, 내장된 파일 이름, 호환성 수준, /AFRelationship, 버전, 선택적 국가 코드, 그리고 XML이 자신의 문서 컨텍스트 내에서 알리는 GuidelineID URN이 포함됩니다
이 6가지는 Factur-X EN16931, Factur-X BASIC, 프랑스용 Factur-X EXTENDED, XRechnung 3.0, ZUGFeRD 1.0 COMFORT, 그리고 ZUGFeRD 2.0 BASIC입니다. GuidelineID는 수신자에게 정확히 어떤 프로파일을 예상해야 하는지 알려주는 필드이며, 그 값은 구체적으로 정해져 있습니다. Factur-X EN16931은 urn:cen.eu:en16931:2017을 알립니다. XRechnung 3.0은 urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0을 알립니다. ZUGFeRD 2.0 BASIC은 urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic을 알립니다. 내장된 파일 이름 역시 계약의 일부입니다. Factur-X 프로파일은 factur-x.xml을, XRechnung은 xrechnung.xml을 내장하며, ZUGFeRD 프로파일은 ZUGFeRD-invoice.xml 또는 zugferd-invoice.xml을 내장합니다. 수신자는 첨부 파일 이름들을 훑어보고 송장을 찾아내기 때문에 파일 이름은 장식적인 요소가 아닙니다
카탈로그의 한 가지 세부 사항은 주의 깊게 읽어볼 가치가 있습니다. 대부분의 프로파일은 Alternative 관계성을 사용하지만, 샘플의 XRechnung 3.0 항목은 Source를 사용합니다. 이 두 형식은 서로 다른 검증기와 관례를 따르며, 샘플은 단일 값을 하드코딩하는 대신 카탈로그에서 각 프로파일의 관계성을 설정합니다. 이것이 바로 상수가 아니라 프로파일별 필드가 존재하는 이유입니다
ZUGFeRD 1.0의 함정
모든 프로파일이 선택적 필드를 얼마나 채워 넣느냐의 사소한 차이만 있을 뿐, 본질적으로는 동일한 EN 16931 Cross Industry Invoice라고 가정하고 싶은 유혹에 빠지기 쉽습니다. 6개 중 5개는 이 가정이 들어맞습니다. 하지만 ZUGFeRD 1.0 COMFORT는 그렇지 않으며, 그 이유는 외관상이 아니라 구조적인 문제 때문입니다
현대의 프로파일들은 루트 요소(root element)가 rsm:CrossIndustryInvoice인 네임스페이스 버전 :100의 UN/CEFACT Cross Industry Invoice를 내보냅니다. ZUGFeRD 1.0은 이 스키마보다 앞서 나왔습니다. 이는 네임스페이스 버전 :1p0인 2014년의 CrossIndustryDocument이며, 루트 요소는 rsm:CrossIndustryDocument입니다. 네임스페이스 URN이 다르고 루트 요소가 다를 뿐만 아니라, 요소 트리 전체가 다릅니다. :1p0 스키마는 데이터를 ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery, ApplicableSupplyChainTradeSettlement 아래에 그룹화하는 반면, :100은 ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery, ApplicableHeaderTradeSettlement를 사용합니다. 이 명명법은 오해를 불러일으킬 만큼 비슷하면서도 코드를 망가뜨릴 만큼 충분히 다릅니다
프로파일 이름의 COMFORT라는 단어는 데이터가 얼마나 풍부한지, 즉 전체 품목 라인, 세금 명세, 지불 조건 등을 갖춘 자동화 등급(automation-grade)의 프로파일임을 설명할 뿐, 어떤 스키마가 이를 실어 나르는지를 나타내는 것은 아닙니다. 따라서 :100 문서를 가져다가 단순히 이름만 ZUGFeRD 1.0으로 다시 붙일 수는 없습니다. 샘플은 각 프로파일 레코드에 있는 플래그(flag)와 두 개의 분리된 빌더 함수를 사용하여 이 문제를 처리하며, XML이 생성되기 전에 올바른 함수를 선택합니다
function BuildInvoiceXMLText(const AProfile: TeInvoiceProfile;
const Data: TInvoiceData): string;
begin
// XMLFamily = 1은 레거시 ZUGFeRD 1.0 :1p0 스키마를 의미하며,
// 그 외의 모든 프로파일은 최신 UN/CEFACT :100 Cross Industry Invoice입니다.
if AProfile.XMLFamily = 1 then
Result := BuildZUGFeRD1Text(AProfile, Data)
else
Result := BuildCII100Text(AProfile, Data);
end;
이러한 분리는 단순히 구현을 깔끔하게 하려는 것이 아닙니다. :100 트리를 ZUGFeRD 1.0 수신자에게 넘겨주면 문서가 루트 요소에서부터 스키마 검증에 실패하게 되므로, 두 제품군은 자신이 무엇을 쓰고 있는지 아는 코드에 의해 각각 별도로 구축되어야만 합니다
PDF/A-3 수준 선택하기
PDF/A-3는 세 가지 호환성 수준(conformance levels)을 가지며, PDFlibPas는 SetPDFAMode를 통해 이를 선택합니다. Mode 5는 PDF/A-3b로, 신뢰할 수 있는 시각적 재현을 보장하는 수준입니다. Mode 6은 PDF/A-3a로, 레벨 a의 태그 지정 구조(tagged-structure) 및 접근성 요구 사항을 추가합니다. Mode 7은 PDF/A-3u로, 모든 텍스트가 유니코드(Unicode)에 매핑될 것을 요구합니다. 모드를 활성화하면 렌더링된 색상이 기기에 종속되지 않고 명확히 정의되도록 PDF/A가 요구하는 색상 특성(color characterization)인 라이브러리의 내장 sRGB 출력 의도(output intent)도 함께 내장됩니다
대부분의 송장 워크플로는 3b에서 실행되며, 이는 원본에 충실한 시각적 페이지와 내장된 XML에 충분한 수준입니다. 내장된 프로파일 대신 명시적인 ICC 프로파일이 필요하다면 모드가 설정된 후 LoadOutputIntentProfile이 이를 바꿔치기합니다. 샘플은 이런 방식으로 저장소의 sRGB 프로파일을 로드하며 파일에 도달할 수 없을 때는 내장 의도로 대비(fall back)하므로 출력 의도는 항상 존재하게 됩니다
PDF := TPDFlib.Create;
try
// Mode 5 = PDF/A-3b, 6 = PDF/A-3a, 7 = PDF/A-3u.
if PDF.SetPDFAMode(5) <> 1 then
raise Exception.Create('PDF/A-3 모드를 활성화할 수 없습니다');
// 선택 사항: 내장된 sRGB 의도를 명시적 ICC 프로파일로 교체합니다.
if PDF.LoadOutputIntentProfile(ICCFile, 'DeviceRGB') <> 1 then
{ SetPDFAMode가 내장한 기본 sRGB 의도로 대비(fall back)합니다 };
finally
// ... 문서 구성을 계속합니다
end;
하이브리드 송장 구축하기
컨테이너가 구성되고 나면 나머지는 순서대로 진행되는 세 단계입니다. PDF/A-3 모드를 설정하고, 사람이 읽을 수 있는 페이지를 그린 다음, XML을 연관 파일로 첨부합니다. 시각적 페이지는 평범한 콘텐츠입니다. 기억할 만한 제약 조건 하나는 PDF/A가 내장되지 않은 표준 14 글꼴(Standard 14 fonts)을 금지하므로, 페이지가 내장 글꼴을 참조하는 대신 실제 글꼴 활자면(font face)을 내장해야 한다는 점입니다
첨부는 단일 호출로 이루어집니다. AddFacturXAssociatedFileFromString은 원시 UTF-8 XML 바이트와 프로파일 메타데이터를 취하여 내장된 파일 스트림을 쓰고, PDF/A-3가 요구하는 카탈로그 /AF 배열에 이를 등록하며, /AFRelationship을 적용하고, 문서를 Factur-X, ZUGFeRD 또는 XRechnung으로 식별하는 XMP 전자 송장 메타데이터를 생성합니다. 또한 XML의 가이드라인 ID가 여러분이 요청한 호환성 수준과 일치하는지도 점검하므로, 여러분이 만든 XML과 여러분이 지명한 프로파일 간의 불일치가 조용히 배포되는 대신 발견되게 됩니다
// 1. PDF/A-3 모드와 출력 의도(output intent)가 이미 설정되어 있습니다.
// 2. 시각적 페이지를 그립니다 (실제 TrueType 글꼴을 내장합니다).
DrawInvoicePage(PDF, AProfile, Data);
// 3. 프로파일에 맞는 XML을 빌드하고
// /AFRelationship = Alternative를 주어 연관 파일로 첨부합니다.
InvoiceXML := BuildInvoiceXML(AProfile, Data); // UTF-8 바이트의 AnsiString
FileID := PDF.AddFacturXAssociatedFileFromString(
InvoiceXML,
AProfile.ConformanceLevel, // 예: 'EN16931'
AProfile.FileName, // 'factur-x.xml'
AProfile.Description,
AProfile.Relationship, // 'Alternative'
AProfile.Version, // '1.0'
AProfile.CountryCode); // '' 또는 'DE' 또는 'FR'
if FileID <= 0 then
raise Exception.Create('송장 XML을 첨부할 수 없습니다');
PDF.SaveToFile(TargetFile);
데이터 경로에서 주의해야 할 한 가지 미묘한 부분은 인코딩(encoding)입니다. 내장된 XML은 encoding="UTF-8"을 선언하고 이 메서드는 해당 바이트들을 AnsiString으로 취하므로, ASCII가 아닌 판매자나 구매자 이름은 반드시 원시 UTF-8 옥텟(octets) 형태로 호출에 도달해야 합니다. 시스템 ANSI 코드 페이지를 통한 단순 캐스트(cast)는 이러한 문자들을 손상시키고, 스스로의 선언과 일치하지 않는 XML을 가진 송장을 조용히 생성하게 됩니다. 샘플은 바이트를 넘겨주기 전에 명시적으로 UTF-8로 인코딩하는데, 이것이 바로 유니코드 string으로부터 바이트 지향(byte-oriented) PDF API에 안전하게 데이터를 공급하는 방법입니다
인식된 전자 송장 프로파일이 아닌 XML을 첨부하는 경우, 제네릭(generic) 대응물인 AddPDFA3AssociatedFileFromString을 사용합니다. 이는 파일 이름, MIME 타입, 설명, 관계성, 바이트를 인수로 취하며, 송장 관련 메타데이터나 가이드라인 점검이 일절 없는 평범한 PDF/A-3 연관 파일을 씁니다. 보충 데이터용으로는 이를 사용하고, 송장용으로는 Factur-X 메서드를 사용해야 프로파일 메타데이터와 가이드라인 매치가 여러분을 대신하여 작성됩니다
문서가 생산되고 나면 다음 질문은 PDF/A 및 접근성 검증을 통과하는지, 그리고 규정 준수를 위반하지 않고 서명할 수 있는지 여부입니다. 이는 Delphi에서의 PDF/A 및 PDF/UA 프리플라이트 안내서와 호환성 및 서명 작업대(compliance and signing workbench)에서 다룹니다. 이 모든 기능은 여기서 사용된 PDF/A, 태그 지정 및 문서 속성 API와 함께 Delphi 및 C++Builder용 PDFlibPas Delphi PDF Library의 일부로 제공됩니다