제2장은 대기 CO2 직접 포집(DAC) 설비가 자기 자신에 대해 만들어 내는 레코드를 다루었다. 시설이 무엇인지, 분 단위로 얼마를 포집했는지, 흡착제가 어떻게 노화하고 있는지가 그 내용이다. 그러나 이 레코드들이 기술하는 것은 총포집량이다. 어느 것도 단독으로는 CO2 1톤이 대기에서 영구히 빠져나갔음을 보여 주지 못한다. 그 주장은 더 하류에서 성립한다. 포집한 CO2가 저장 사업자에게 넘어가고, 독립 검증기관이 증거를 검토하고, 등록부가 탄소제거 크레딧을 발행하며, 누군가 그 크레딧을 자기 배출량에 대해 폐기(상쇄 사용)할 때 비로소 완결된다.
이 장은 그 하류 이야기를 담는 세 레코드, 즉 저장 이벤트, 검증 보고서, 탄소제거 크레딧을 다룬다. 세 레코드는 모두 WIA 대기 CO2 직접 포집 표준(WIA-ENE-050)의 PHASE-2-DATA §5–§7에 정의되어 있다. 이어서 이들이 어떻게 연결되어 제3자가 폐기된 크레딧에서 개별 계량기 판독값까지 거슬러 올라갈 수 있는 증거 사슬을 이루는지 살펴본다. 또한 표준이 API용으로 정의한 응답·오류 봉투(PHASE-2-DATA §8)와 모든 스키마에 적용되는 검증·버전 관리 규칙(§9–§10)도 함께 다룬다.
표준은 각 레코드를 별도의 JSON Schema(draft 2020-12)로 정의한다. 아직 제대로 정의되지 않은 것은 레코드 사이의 연결이다. 저장 이벤트에는 그 CO2가 어느 포집 데이터에서 왔는지 가리키는 필드가 없고, 검증 보고서에는 어떤 저장 이벤트를 다루었는지 나열하는 필드가 없으며, 크레딧 스키마에는 그 크레딧을 뒷받침하는 검증 보고서 필드가 없다. 발행 API 요청에는 그 값이 들어가는데도 그렇다. 이 장의 상당 부분은 1.0 버전과의 적합성을 깨지 않으면서 이 빈틈을 메우는 데 할애하며, 명세에 없는 필드는 이 책의 권고로 표시한다.
모든 예시는 제2장에서 소개한 가상 시설 dac-example-reykjanes-01과 가상의 저장소, 가상의 검증기관을 사용한다. 수치는 내부적으로 일관되도록 고른 예시값이며, 실제 설비의 운영 데이터가 아니다.
저장 이벤트는 포집한 CO2를 저장 경로로 한 번 인도한 사실을 기록한다. 지중 저장 또는 광물화 현장의 주입 배치 하나일 수도 있고, 활용 공정으로의 이송 한 건일 수도 있다. 사슬에서 CO2를 어떻게 포집했는지가 아니라 어디로 갔는지를 다루는 첫 레코드이며, 제거 주장의 자연스러운 기준점이다. 순제거량에 산입되는 것은 포집량이 아니라 저장량이기 때문이다(제1장 §1.4).
필수 필드는 eventId, facilityId, timestamp, amount의 네 개뿐이다. 이벤트 ID는 ^storage-[a-z0-9-]+$를 따라야 하며, 제2장에서 본 시설 ID 패턴 ^dac-[a-z0-9-]+$와 같은 소문자·하이픈 방식이다. amount는 CO2 톤 단위의 수치로 최솟값은 0이고, timestamp는 RFC 3339 date-time이다.
스키마는 타임스탬프가 어느 시점을 가리키는지 말하지 않는다. 21시간 동안 이어지는 주입에는 시작 시각, 종료 시각, 저장 사업자가 수량을 확인한 시각이 따로 있고, 이 시각들은 서로 다른 날짜나 심지어 서로 다른 보고 기간에 걸칠 수 있다. 이 책은 timestamp를 UTC 기준 주입 종료 시각으로 정하고 시작 시각은 따로 명시할 것을 권고한다(아래 예시 참조). 그러면 이벤트가 어느 기간에 산입되는지가 분명한 규칙을 따르게 되고, 검증기관이 이를 재현할 수 있다.
저장 이벤트의 시설 ID에는 시설 레코드와 달리 패턴 제약이 없다. 그래도 검증기는 시설 ID 패턴을 적용해야 하고, 더 중요하게는 그 ID가 실제 등록된 시설 레코드를 가리키는지 확인해야 한다. 후자는 JSON Schema로는 검사할 수 없다. 이 문제는 §3.6에서 다시 다룬다.
선택 블록들은 인도의 세부 내용을 기술한다.
storageMethod는 mineralization, geological-storage, ocean-storage, utilization 중 하나다. 시설 레코드는 hybrid도 허용하지만, 개별 이벤트는 반드시 하나의 구체적인 경로를 지정해야 한다.storageSite는 siteId, name, 그리고 위도·경도·depth(미터)로 이루어진 location을 담는다. 사이트 ID에는 패턴이 없다. 저장 사업자 자체 사이트 대장에 대한 외래 키로 취급하고, 값이 바뀌지 않도록 유지해야 한다.injectionData는 주입 온도(°C), 압력(bar), 유량(t/h), 지속 시간(h)을 담는다. 네 값 모두 범위 제약이 없는 수치다.co2Quality는 백분율(0–100)로 표시한 purity와 contaminants 목록을 담는다. 목록의 각 항목은 compound(화합물 이름)와 ppm 단위의 concentration을 가진다.permanence와 verification은 §3.1.3과 §3.2에서 다룬다.제2장에서 본 단위 문제 두 가지가 여기서 다시 나타난다. 첫째, 순도는 백분율인데 불순물은 ppm이며, 스키마는 그 ppm이 부피(몰분율) 기준인지 질량 기준인지 밝히지 않는다. CO2 흐름의 가스 사양은 보통 몰 기준으로 표시하지만, 레코드가 이를 명시해야 한다. 예를 들어 확장 필드에 basis를 둘 수 있다. 둘째, 두 척도는 서로 맞아야 한다. 순도가 99.5 %라면 나머지 0.5 %, 즉 5,000 ppm이 다른 성분의 몫이다. 불순물 목록의 합이 이 잔여분에 크게 못 미친다면 목록이 불완전한 것이며, 검증기관이 발견하도록 내버려 두지 말고 그 사실을 선언해야 한다. 명세의 저장 이벤트 예시는 순도를 98.5 %로 적으면서 불순물은 합계 1,500 ppm만 나열하는데, 이는 잔여분의 10분의 1에 불과하다.
permanence 블록에는 guaranteedYears(최솟값 1000인 정수), monitoringPlan(문자열), insurancePolicy(문자열)의 세 필드가 있다. 스키마가 과학적 주장에 가장 가까이 다가가는 곳이 여기이므로 주의해서 읽어야 한다.
minimum: 1000 제약 때문에 영구성 선언이 1,000년 미만인 저장 이벤트는 검증을 통과하지 못한다. 이는 기반 문서가 지중 저장과 광물화에 부여한 등급과는 맞지만, 활용(utilization)과는 맞지 않는다. 활용은 유효한 storageMethod 값이고, PHASE-3는 이를 사례별로 분류한다. 한편 크레딧 스키마는 permanence.years의 최솟값을 100으로 둔다. 두 하한은 서로 일관되지 않는다. 활용 이벤트에 대해 구현자가 정직하게 택할 수 있는 길은 두 가지다. 선택 필드인 guaranteedYears를 생략하고 모니터링 계획에 내구성 등급을 기술하거나, 활용 이벤트를 permanence 블록 대상에서 제외하는 프로파일을 선언하는 것이다. 경로가 뒷받침하지 못하는데도 검증을 통과하려고 1000을 적어 넣으면 증거 사슬에 거짓 진술을 심는 셈이 된다.
더 일반적으로 말하면 guaranteedYears는 측정값이 아니라 선언된 내구성 등급이다. 그 값을 믿을 수 있게 만드는 것은 뒤에 있는 모니터링 계획이다. 예컨대 PHASE-3 §3.1은 지중 저장의 주입 후 모니터링 기간을 정한다. 스키마는 이 계획을 자유 텍스트로 저장하지만, 이 책은 버전 번호가 붙은 문서 ID나 콘텐츠 해시가 딸린 URL 같은 참조를 저장할 것을 권고한다. 그래야 주입 당시 유효했던 계획을 수십 년 뒤에도 찾아볼 수 있다. insurancePolicy도 마찬가지다.
저장 이벤트 안의 verification 하위 블록(검증기관 ID, 날짜, 적용 표준, 인증서 URL)은 저장 사업자 측 검증기관이 저장 행위 자체를 확인한 사실을 기록한다. 이는 DAC 시설의 보고 기간 전체를 다루는 §3.2의 검증 보고서와는 별개다. 명세의 예시는 standard에 ISO 27916을 적어 두었다. 이 표준은 석유회수증진(EOR)과 연계된 CO2 저장을 다루므로 DAC 광물화 인도에는 맞지 않는다. 저장 검증기관이 실제로 적용한 표준을 적어야 한다.
다음 이벤트는 가상 시설의 광물화 주입 한 건을 기록한 것이다. ext 아래의 필드는 이 책이 권고하는 확장 필드이고, 나머지는 1.0 버전 필드다.
{
"eventId": "storage-example-reykjanes-01-20260314-001",
"facilityId": "dac-example-reykjanes-01",
"timestamp": "2026-03-14T21:00:00Z",
"amount": 12.6,
"storageMethod": "mineralization",
"storageSite": {
"siteId": "site-example-basalt-01",
"name": "Example Basalt Injection Site 1",
"location": { "latitude": 63.9, "longitude": -22.4, "depth": 800 }
},
"injectionData": {
"temperature": 30,
"pressure": 25,
"rate": 0.6,
"duration": 21
},
"co2Quality": {
"purity": 99.5,
"contaminants": [
{ "compound": "N2", "concentration": 3200 },
{ "compound": "O2", "concentration": 900 },
{ "compound": "H2O", "concentration": 600 },
{ "compound": "Ar", "concentration": 300 }
]
},
"permanence": {
"guaranteedYears": 1000,
"monitoringPlan": "mp-example-basalt-01 v2",
"insurancePolicy": "POL-EXAMPLE-0001"
},
"verification": {
"verifierId": "storage-verifier-example",
"verificationDate": "2026-03-16",
"standard": "site operator's storage monitoring procedure (example)"
},
"ext": {
"injectionStart": "2026-03-14T00:00:00Z",
"meterReadingIds": ["mtr-example-ct01-20260314"],
"contaminantBasis": "mol/mol"
}
}
JSON Schema로 표현할 수 없는 일관성 검사가 여러 가지 적용된다.
timestamp보다 앞서야 하고, 저장 검증 날짜는 그와 같거나 뒤여야 한다.검증 보고서는 독립된 제3자가 어떤 기간에 대해 증거가 뒷받침하는 톤수를 밝히는 레코드다. 필수 필드는 reportId(패턴 ^verify-[a-z0-9-]+$), facilityId, period(필수 하위 필드 startDate와 endDate를 가진 객체, 둘 다 형식 date), verifiedAmount(0 이상, 톤 단위)다.
스키마는 verifiedAmount를 "검증된 CO2 포집 톤수"라고 설명한다. 문자 그대로 읽으면 총포집량이다. 그러나 크레딧은 순제거량이 뒷받침해야 하며, 제1장에서 보았듯이 둘의 차이는 클 수 있다. 수치만 보아서는 어느 쪽인지 알 수 없다. 이 책은 크레딧 산정에 쓰이는 보고서라면 verifiedAmount에 항상 순제거량, 즉 에너지·내재 배출·손실과 보수적 불확도 공제까지 뺀 값을 담고, 총량과 각 공제 항목은 별도의 확장 필드로 기록할 것을 권고한다. 불확도 처리는 제6장에서 자세히 다룬다.
uncertainty 필드는 백분율로 설명된 수치다. 어느 신뢰수준에서의 값인지, 총량 대비인지 순량 대비인지는 밝히지 않는다. 기반 문서는 저장 검증에 95 % 초과 신뢰도를, 포집 측정에 ±5 % 불확도를 요구한다. 이 목표와 비교할 수 있도록 보고서는 신뢰수준을 명시해야 한다. 예를 들면 ext.uncertaintyConfidence: 0.95와 같다.
두 날짜는 시간대가 없는 달력 날짜이고, 스키마는 endDate가 포함 경계인지 밝히지 않는다. 그 결과 연속된 두 분기 보고서가 하루 겹치거나 하루 비게 될 수 있고, 경계일 늦게 기록된 저장 이벤트는 두 번 산입되거나 아예 빠질 수 있다. 이 책은 두 날짜를 모두 UTC 달력 날짜로, 양 끝을 모두 포함하는 것으로 읽고, 각 저장 이벤트는 이 규칙에 따라 timestamp(주입 종료 시각)로 기간을 배정할 것을 권고한다. 그러면 연속된 보고서는 next.startDate = previous.endDate + 1일을 만족해야 한다.
verifier 객체는 organization(기관), accreditation(인정), 선임 auditor(심사원)를 기록한다. standards는 문자열 목록이고 methodology는 자유 텍스트다. 온실가스 주장에서 흔히 참조하는 표준은 프로젝트 수준 정량화의 ISO 14064-2, 검증의 ISO 14064-3이며, 검증기관 요구사항은 ISO 14065가 정한다. 명세의 스키마 설명은 예시 값으로 ISO 27916을 제시한다. §3.1.3에서 설명했듯이 이 표준은 석유회수증진과 연계된 저장을 다루므로, 실제로 그 표준을 적용한 경우가 아니라면 DAC 제거량 검증의 근거로 적어서는 안 된다.
발견사항은 category, 자유 텍스트 description, severity를 가진 객체의 목록이다. 결론은 status가 아니라 별도의 conclusion 필드이며, 값은 verified, verified-with-conditions, not-verified 중 하나다. 스키마는 심각도와 결론을 연결하지 않으므로, 표 3-1의 규칙은 검증기 구현을 위한 이 책의 권고다.
| 범주 | 의미 | 심각도 사용 | conclusion에 대한 권고 효과 |
|---|---|---|---|
conformance | 요구사항을 확인했고 충족함 | 보통 없음 | 없음 |
non-conformance | 요구사항을 충족하지 못함 | minor / major / critical | minor: 최대 verified-with-conditions. major: 검증기관이 영향을 정량화해 공제했을 때만 verified-with-conditions, 아니면 not-verified. critical: not-verified |
observation | 위반은 아니지만 기록해 둘 사항 | 선택 | 없음 |
recommendation | 개선 제안 | 선택 | 없음 |
verified-with-conditions로 결론 난 보고서는 그 조건을 기계가 읽을 수 있는 곳에 적어야 한다. 스키마에는 이를 위한 필드가 없으므로, 범주가 non-conformance인 발견사항에 확장 필드로 종결 기한을 붙이는 방식이 실용적이다. nextVerificationDue는 다음 검증 날짜를 알려 주므로 등록부가 검증이 끊긴 시설을 표시하는 데 쓸 수 있다. reportUrl은 사람이 읽는 보고서를 가리킨다. JSON은 그 문서의 요약에 불과하므로 URL에는 콘텐츠 해시를 함께 붙여야 한다.
{
"reportId": "verify-example-reykjanes-01-2026",
"facilityId": "dac-example-reykjanes-01",
"period": { "startDate": "2026-01-01", "endDate": "2026-12-31" },
"verifiedAmount": 780,
"uncertainty": 4.0,
"verifier": {
"organization": "Example Verification Body Ltd.",
"accreditation": "ISO 14065 (example accreditation reference)",
"auditor": "restricted"
},
"standards": ["ISO 14064-2", "ISO 14064-3"],
"methodology": "example-dac-methodology v1",
"findings": [
{ "category": "non-conformance", "severity": "minor",
"description": "유량계 CT01의 교정 인증서가 기간 종료 전에 만료됨. 재교정했으며 보정은 불필요." },
{ "category": "observation",
"description": "에너지 레코드의 전력망 배출계수 출처에 버전이 없음." }
],
"conclusion": "verified-with-conditions",
"verificationDate": "2027-02-18",
"nextVerificationDue": "2028-02-18",
"ext": {
"grossStored": 1000.0,
"deductions": { "energy": 150, "embodied": 50, "losses": 10, "uncertainty": 10 },
"uncertaintyConfidence": 0.95,
"storageEventIds": ["storage-example-reykjanes-01-20260314-001", "..."],
"reportSha256": "..."
}
}
산술은 레코드만으로 확인할 수 있다. 1,000 − 150 − 50 − 10 − 10 = 780이다. 이 보고서를 받은 등록부는 확장 필드로부터 verifiedAmount를 다시 계산해 값이 다르면 보고서를 거부할 수 있다. 나열된 저장 이벤트의 수량을 합해 grossStored와 같은지도 확인할 수 있다.
크레딧 스키마는 등록부가 발행한 단위를 기술한다. 필수 필드는 creditId, facilityId, amount(톤, 0 이상), vintage(2020 이상의 정수, 제거가 일어난 연도로 설명됨)다. 선택 필드는 등록부 식별자, 소유, 내구성, 공동편익, 가격, 온체인 표현을 다룬다.
| 필드 | 유형과 제약 | 노트 |
|---|---|---|
creditId | 문자열, 패턴 없음 | PHASE-2-DATA에서 패턴이 없는 유일한 레코드 ID다. 일관성을 위해 프로파일에서 ^credit-[a-z0-9-]+$를 적용할 것을 권고한다. |
serialNumber | 문자열 | 등록부의 일련번호. 등록부는 흔히 톤 단위로 일련번호를 매기고 묶음을 범위로 표현하는데, 문자열 하나로는 단일 번호인지 범위인지 알 수 없다. §3.4.3 참조. |
amount | 수치 ≥ 0 | 소수도 스키마상 유효하지만 등록부는 정수 단위로 발행한다. 발행 시 정수인지 검사한다. |
vintage | 정수 ≥ 2020 | 제거 연도. DAC에서는 포집 날짜가 아니라 저장 이벤트 타임스탬프에서 가져온다. 해가 바뀌는 기간은 빈티지 두 개를 만든다. 상한이 없으므로 미래 연도는 거부한다. |
registry | 열거형: Puro.earth, Verra, Gold Standard, ACR, Other | 값에 점과 공백이 들어 있어 식별자라기보다 표시용 이름이다. Other를 쓸 때는 확장 필드에 등록부 이름을 기록한다. |
methodology | 문자열 | 크레딧 방법론의 이름과 버전. 검증 보고서의 methodology와 일치해야 한다. |
issuanceDate | date | 근거 보고서의 verificationDate보다 앞설 수 없다. |
status | 열거형: issued, transferred, retired, cancelled | 수명주기 상태. 전이 규칙은 §3.4.4에 있다. |
owner | 문자열 | 현재 보유자. 이전 시 바뀌고 폐기 시 고정된다. 기관일 수도, 개인일 수도 있다. |
permanence.years | 정수 ≥ 100 | 크레딧이 주장하는 내구성. 저장 이벤트의 하한 1000과 일관되지 않는다(§3.1.3). 크레딧 값은 근거 저장 이벤트들의 최솟값을 넘지 않아야 한다. |
permanence.guarantee | 문자열 | 내구성 주장을 어떻게 보장하는지 적는 자유 텍스트. 스키마가 형식을 정하지 않으므로 증거가 아니라 설명으로 다룬다. |
cobenefits | 문자열 배열 | 자유 텍스트. 수량 계산에는 쓰지 않는다. |
price | 객체: amount, currency(^[A-Z]{3}$) | 패턴은 대문자 세 글자면 무엇이든 받는다. ISO 4217 코드 목록과 대조한다. 가격은 상업 데이터이므로 파트너 또는 비공개 접근 등급에 둔다(제7장). |
blockchain | 객체: network, contractAddress, tokenId, transactionHash | 선택적인 온체인 미러. 권위는 등록부 레코드에 있다. 체인이 언제 가치를 더하는지는 제6장에서 다룬다. |
가장 중요한 누락은 증거에 대한 참조다. 발행 엔드포인트 POST /credits/issue(PHASE-4 §2)는 요청에 verificationReportId를 받지만, 크레딧 레코드 자체에는 그 값을 둘 곳이 없다. 일단 발행되고 나면 크레딧은 시설 외에는 아무것과도 연결되지 않으므로, 감사인은 어느 검증이 그 크레딧을 뒷받침하는지 알 수 없다. §3.4의 권고는 크레딧 레코드에 보고서 ID를 저장하는 것이다.
PHASE-2-DATA는 일곱 가지 데이터 범주를 나열한다. 다섯 범주(시설, 운영, 흡착제, 저장, 검증)에는 고유한 레코드 스키마가 있고, 크레딧에도 스키마가 하나 더 있다. 일곱 번째인 에너지 데이터는 별도 레코드가 아니라 운영 레코드의 energy 블록과 시설 레코드 안에 실려 있다. 그림 3-1은 레코드 사이의 관계를 보여 준다. |로 그린 선은 1.0 버전에 존재하는 참조이고, :로 그린 선은 이 책이 확장으로 추가할 것을 권고하는 참조다.
+-------------------------+
| Facility |
| dac-... |
+-------------------------+
^ ^ ^ ^
facilityId | | | | facilityId
| | | |
+-----------------------+ | +-----------------------+
| Capture record | | | Sorbent material |
| (operational + energy)| | | sorbent-... |
+-----------------------+ | +-----------------------+
^ | facilityId
: meterReadingIds |
: |
+-----------------------+----+
| Storage event |
| storage-... |
+-----------------------+
^
: storageEventIds facilityId (모든 레코드)
:
+-----------------------+
| Verification report |
| verify-... |
+-----------------------+
^
: verificationReportId (발행 API 요청에만 존재)
:
+-----------------------+
| Carbon removal credit |
| credit-... |
+-----------------------+
| 1.0 버전에 정의된 참조 : 권고 확장
facilityId를 가지며, 크레딧에서 포집까지 거슬러 올라가는 증거 연결은 확장이다.연결의 신뢰도는 그 연결이 쓰는 식별자의 신뢰도를 넘지 못한다. 표 3-3은 식별자 규칙을 모은 것이다. ID는 레코드를 만드는 시스템이 한 번만 발급해야 하며, 재사용해서는 안 되고, 사이트 이름처럼 바뀔 수 있는 데이터에서 파생해서도 안 된다.
| 레코드 | ID 필드 | 1.0 버전 패턴 | 발급 주체 |
|---|---|---|---|
| 시설 | facilityId | ^dac-[a-z0-9-]+$ | 운영사, 시설당 한 번 |
| 포집 레코드 | 없음(facilityId + timestamp가 키) | — | 엣지 수집기 |
| 흡착제 | sorbentId | ^sorbent-[a-z0-9-]+$ | 운영사 또는 공급사 |
| 저장 이벤트 | eventId | ^storage-[a-z0-9-]+$ | 합의에 따라 운영사 또는 저장 파트너 |
| 검증 보고서 | reportId | ^verify-[a-z0-9-]+$ | 검증기관 |
| 탄소제거 크레딧 | creditId | 없음(이 책은 ^credit-[a-z0-9-]+$ 권고) | 발행 플랫폼. serialNumber는 등록부가 따로 발급 |
표에서 두 가지를 읽을 수 있다. 첫째, 포집 레코드에는 고유 ID가 없다. 따라서 저장 이벤트는 포집 레코드를 시간 범위로만 가리킬 수 있거나, 여기서 권고하듯이 인수인계 계량기 판독값의 ID로 가리켜야 한다. 실제로 설비 경계를 넘은 양이 바로 그 판독값이다. 둘째, 패턴은 소문자·숫자·하이픈만 허용하므로, 대문자가 흔히 섞이는 등록부 일련번호 같은 외부 식별자는 WIA ID로 그대로 쓸 수 없고 별도 필드에 저장해야 한다.
권고한 참조를 갖추면 크레딧에서 출발해 증거를 거꾸로 따라갈 수 있다.
{
"creditId": "credit-example-reykjanes-01-2026-a",
"serialNumber": "EXREG-2026-000001..EXREG-2026-000780",
"facilityId": "dac-example-reykjanes-01",
"amount": 780,
"vintage": 2026,
"registry": "Other",
"methodology": "example-dac-methodology v1",
"issuanceDate": "2027-03-10",
"status": "issued",
"owner": "org-example-operator",
"permanence": { "years": 1000 },
"ext": {
"registryName": "Example Registry",
"verificationReportId": "verify-example-reykjanes-01-2026",
"serialRange": { "prefix": "EXREG-2026-", "first": 1, "last": 780 }
}
}
ext.verificationReportId로 검증 보고서를 가리키고, 수량 780은 보고서의 verifiedAmount와 같다.ext.storageEventIds에 자신이 다룬 저장 이벤트를 나열한다. 이들의 수량 합은 grossStored(예시에서 1,000 t)와 같고, 모든 이벤트 타임스탬프는 §3.2.2의 양끝 포함 규칙에 따라 보고 기간 안에 든다.ext.meterReadingIds로 인수인계 계량기 판독값을 가리킨다. 그 적산값은 계량기 불확도 ±2 % 안에서 이벤트 수량과 일치한다.dac-example-reykjanes-01에 속한다. 포집 레코드에는 에너지 공제를 계산한 근거인 에너지 데이터가 들어 있다.각 단계는 기계적이다. ID를 해석하고, 수량을 비교하고, 시각을 비교한다. 실무에서 "검증 가능하다"는 말은 바로 이것을 뜻한다.
이중 계산은 같은 제거량이 둘 이상의 주장을 뒷받침할 때 일어난다. 데이터 관점에서 이는 참조 무결성 위반이며, 발행 플랫폼이 매 발행 전에 검사하는 몇 가지 불변식으로 막을 수 있다.
amount 합은 그 보고서의 verifiedAmount를 넘을 수 없다. 한 보고서에 대해 여러 차례 나누어 발행할 수는 있지만 총량을 넘을 수는 없다.verified 또는 verified-with-conditions인 보고서만 크레딧의 근거가 될 수 있다.// 발행 전 검사 예시(JavaScript). 위반 사항 목록을 반환한다.
function checkIssuance(request, db) {
const errors = [];
const report = db.reports.get(request.verificationReportId);
if (!report) return ["unknown verification report"];
if (report.facilityId !== request.facilityId) errors.push("facility mismatch");
if (!["verified", "verified-with-conditions"].includes(report.conclusion))
errors.push("report not verified");
const alreadyIssued = db.credits
.filter(c => c.ext?.verificationReportId === report.reportId
&& c.status !== "cancelled")
.reduce((sum, c) => sum + c.amount, 0);
if (alreadyIssued + request.amount > report.verifiedAmount)
errors.push("issuance would exceed verifiedAmount");
for (const id of report.ext?.storageEventIds ?? []) {
const other = db.reports.findByStorageEvent(id)
.filter(r => r.reportId !== report.reportId && !r.withdrawn);
if (other.length > 0) errors.push(`storage event ${id} claimed twice`);
}
if (!Number.isInteger(request.amount)) errors.push("amount must be whole tonnes");
return errors;
}
ext 필드는 이 책의 권고다.취소된 크레딧은 누적 합계에서 제외되므로, 취소된 묶음은 다시 발행할 수 있다. 보유자를 잘못 적어 발행한 경우 같은 행정적 취소라면 이것이 옳다. 그러나 저장된 CO2가 나중에 누출된 것으로 드러나 취소하는 역전(reversal)의 경우에는 옳지 않다. 그때는 보고서의 실효 verifiedAmount도 함께 줄여야 한다. 표준에는 이를 위한 필드가 없다. 등록부가 역전을 어떻게 처리하는지는 제6장에서 다룬다.
크레딧의 status 열거형은 수명주기를 암시하지만, 명세는 허용되는 전이를 밝히지 않는다. 다음 규칙은 이 책의 권고이며 일반적인 등록부 관행과 맞는다.
| 이전 상태 | 다음 상태 | 계기 | 함께 바뀌는 것 |
|---|---|---|---|
| (없음) | issued | 등록부 발행 | serialNumber, issuanceDate, owner 설정 |
issued / transferred | transferred | 보유자 변경 | owner 갱신. 이전 내역은 추가 전용 이력으로 보존 |
issued / transferred | retired | 보유자가 크레딧을 주장에 사용 | 종결 상태. 폐기 수혜자와 목적 기록. 온체인 미러가 있으면 그것도 사용 불가로 표시해야 함 |
issued / transferred | cancelled | 행정 오류 또는 역전 | 종결 상태. 사유 기록. 역전이면 근거 보고서의 실효 수량 감축 |
retired / cancelled | 모든 상태 | — | 허용하지 않음 |
상태의 진실원은 등록부다. 운영사 대시보드, 기업 탄소회계 시스템, 체인 위의 토큰 등 다른 모든 사본은 미러이며 등록부를 따라야 한다. 폐기는 다음 정기 동기화를 기다리지 말고 이벤트로 전파해야 한다. 위험은 이미 폐기된 크레딧을 여전히 사용 가능하다고 보여 주는 미러에 있다. 낡은 사본을 믿는 상대에게서 구매자가 같은 크레딧을 한 번 더 살 수 있기 때문이다. 제6장에서 보겠지만, 명세의 스마트 계약 예시는 retired 플래그만 세울 뿐 토큰을 소각하거나 잠그지 않는다. 바로 이런 불일치의 예다.
PHASE-2-DATA §8은 API가 레코드를 어떻게 감싸는지 정의한다. 엔드포인트 자체는 제4장에서 다루고, 여기서는 봉투를 다룬다. 봉투도 데이터 형식의 일부이기 때문이다.
성공 응답은 네 멤버를 가진다. success(true), data(레코드 또는 목록), metadata(timestamp, version, requestId), 그리고 목록일 때의 pagination(page, limit, total)이다. 오류 응답은 success를 false로 두고 data 대신 code, message, 형식이 자유로운 details를 가진 error 객체를 담는다. 명세의 유일한 오류 코드 예시는 INVALID_FACILITY_ID이며, details에 해당 필드와 거부된 값을 적는다.
이 봉투에는 세 가지 약점이 있다. 본문에 HTTP 상태 코드가 없고, 오류 코드 목록이 표준 어디에도 정리되어 있지 않으며, 한 응답이 한 번에 실패 하나만 보고할 수 있다.
README와 PHASE-1·PHASE-2의 참조 표준 목록은 IETF RFC 9457 Problem Details for HTTP APIs를 인용한다. 이 RFC는 application/problem+json 미디어 타입과 표준 멤버 type(문제 유형을 식별하는 URI), title, status, detail, instance를 정의하고 확장 멤버를 허용한다. 결국 표준은 오류 형식 하나를 인용하면서 다른 형식을 규정하고 있는 셈이다.
| WIA §8.2 멤버 | RFC 9457 대응 | 설명 |
|---|---|---|
success: false | HTTP 상태 4xx/5xx와 미디어 타입 | RFC 9457에서는 불필요하다. 클라이언트는 상태 코드로 분기한다 |
error.code | type(URI)과 확장 멤버 code | URI가 해당 코드의 문서로 연결될 수 있다 |
error.message | title(유형별 고정)과 detail(이번 발생) | RFC 9457은 일반 설명과 개별 설명을 구분한다 |
error.details | 확장 멤버, 예: errors | 배열을 쓰면 여러 검증 실패를 한 번에 보고할 수 있다 |
metadata.requestId | instance 또는 확장 멤버 | 지원·감사 로그를 위해 요청 ID를 유지한다 |
metadata.version | 확장 멤버 | 폐기 예고 기간 중에 유용하다(§3.6) |
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://example.com/wia/direct-air-capture/problems/validation",
"title": "레코드 검증 실패",
"status": 422,
"detail": "저장 이벤트의 필드 2개가 유효하지 않습니다.",
"instance": "urn:uuid:5b1c2f6e-0d8a-4c61-9f1e-2a7d3c9b8e10",
"code": "VALIDATION_FAILED",
"version": "1.0.0",
"errors": [
{ "pointer": "/facilityId", "code": "INVALID_FACILITY_ID",
"detail": "^dac-[a-z0-9-]+$ 패턴과 일치해야 합니다" },
{ "pointer": "/co2Quality/purity", "code": "OUT_OF_RANGE",
"detail": "0에서 100 사이여야 합니다" }
]
}
type URI는 자리표시용 도메인을 쓴다.이 책은 오류 형식으로 RFC 9457을 권고하며, 이유는 네 가지다. 표준이 이미 이를 인용하므로 채택하면 의존성을 늘리는 것이 아니라 불일치를 해소하게 된다. IETF에 등록된 형식이라 API 게이트웨이, HTTP 클라이언트 라이브러리, 모니터링 도구가 이미 인식한다. 확장 메커니즘 덕분에 INVALID_FACILITY_ID 코드를 포함해 WIA 객체의 모든 정보를 잃지 않고 옮길 수 있다. 그리고 문제 유형마다 type URI를 두면 명세에 없는 오류 코드 카탈로그를 공개할 자연스러운 자리가 생긴다. 성공 봉투는 명세대로 두어도 된다. 클라이언트는 상태 코드와 미디어 타입으로 파서를 고르므로 두 형식을 섞어도 충돌하지 않는다. 피해야 할 것은 한 엔드포인트에서 오류 형식 두 가지를 모두 지원하는 일이다. 그러면 클라이언트가 오류 처리 경로를 두 개 갖추어야 한다. 기존 클라이언트 때문에 §8.2 형식을 유지해야 하는 배포라면 프로파일 매니페스트에 이를 선언하고, 콘텐츠 협상으로 RFC 9457을 제공해야 한다.
불일치 하나가 더 있다. POST /storage/events는 "status": "pending-verification"을 돌려주지만, 저장 이벤트 스키마에는 상태 필드가 없다. 클라이언트가 방금 통보받은 상태를 레코드에서 다시 읽을 방법이 없는 것이다. 이 책은 저장 이벤트에 pending-verification, verified, rejected 같은 값을 갖는 상태 확장 필드를 추가하고, 그 이벤트가 긍정적 결론의 보고서에 포함될 때 값을 갱신할 것을 권고한다.
PHASE-2-DATA §9는 여섯 가지 검증 규칙을 나열한다. 표 3-6에서 보듯 각 규칙은 JSON Schema 키워드에 대응하며, 예시는 이 장의 레코드에서 가져왔다.
| 규칙 | JSON Schema 키워드 | 이 장의 예 |
|---|---|---|
| 필수 필드 | required | vintage가 없는 크레딧은 거부된다 |
| 유형 검사 | type | vintage는 정수여야 하며 문자열 "2026"은 실패한다 |
| 형식 | format | period.startDate는 완전한 날짜, reportUrl은 URI여야 한다 |
| 범위 | minimum, maximum | purity 0–100, guaranteedYears ≥ 1000 |
| 열거형 | enum | conclusion, status, registry, storageMethod |
| 패턴 | pattern | eventId, reportId, price.currency |
이 규칙을 검증기로 구현할 때 실무상 주의할 점이 세 가지 있다. 첫째, JSON Schema 2020-12에서 format 키워드는 기본적으로 주석(annotation)이다. 형식 단언을 켜야만 검사되며, 예컨대 Ajv에서는 ajv-formats 플러그인을 추가해야 한다. 그러지 않으면 잘못된 날짜가 조용히 통과한다. 둘째, 어느 스키마도 additionalProperties: false를 두지 않는다. 이 장의 확장이 가능한 것도 그 덕분이지만, verfiedAmount처럼 철자가 틀린 필드도 받아들여지고 정작 진짜 필드는 빠진 상태가 된다. 배포 프로파일은 스키마를 닫고 확장은 이름 붙은 멤버(이 장에서는 ext) 아래에서만 허용하거나, 알 수 없는 최상위 필드를 보고하는 별도 검사를 돌려야 한다. 셋째, §3.1.4와 §3.4.3의 검사는 모두 JSON Schema 바깥에 있다. 유량 × 시간, 순도와 불순물, 검증량 대비 발행 합계, 저장 이벤트의 배타성이 그렇다. 적합한 검증기는 먼저 스키마 검증을 하고 이어서 레코드 간 검사를 두 번째로 수행한다. 이중 계산을 막는 것은 두 번째 단계뿐이다.
스키마는 시맨틱 버저닝을 따른다(PHASE-2-DATA §10.1). MAJOR는 구조를 깨는 변경, MINOR는 하위 호환 추가, PATCH는 수정과 명확화다. 폐기 예정 필드에는 "deprecated": true를 표시한다. 이 키워드는 JSON Schema 메타데이터 어휘에 속하므로 표준 도구가 이를 드러낼 수 있다. 다만 주석이므로 검증기는 폐기 예정 필드를 거부하지 않고 보고만 한다. 레코드의 의미는 버전에 따라 달라지므로, 모든 레코드는 어느 스키마 버전을 기준으로 작성되었는지를 봉투의 metadata.version이나 $schema 또는 $id URI의 버전으로 밝혀야 한다.
명세는 같은 절차에 대해 서로 다른 기간을 여럿 제시한다. 구현자는 사용자에게 무엇을 약속하기 전에 이 사실을 알아 두어야 한다.
| 출처 | 내용 |
|---|---|
| PHASE-1·PHASE-2 공통 문서 §6 | 메이저 버전은 최소 90일 병행하고, 이후 폐기 예정 버전은 12개월 일몰 기간에 들어간다 |
| 부록 F(각 PHASE 문서) | 호환을 깨는 변경은 최소 90일 병행이 필요하다 |
| 부록 H(각 PHASE 문서) | 이전 메이저 버전을 최소 180일 병행 유지한 뒤 12개월 일몰 기간을 둔다 |
| PHASE-2-DATA §10.2 | 폐기 예정 필드는 제거 전 6개월의 예고 기간을 둔다 |
| 부록 E(PHASE-2-DATA) | 필드 이름 변경이나 제거는 마이너 버전 갱신을 유발해야 한다고 적었지만, §10.1과 부록 H는 호환을 깨는 변경을 메이저로 정의한다 |
90일과 180일은 정면으로 충돌한다. 6개월 필드 규칙과 버전 병행 규칙은 서로 다른 대상(필드 제거와 버전 전체의 병행 유지)을 다룬다고 읽을 수도 있으며, 어느 쪽으로 읽든 12개월 일몰은 병행 기간 뒤에 온다. 마이너 갱신에 관한 부록 E의 문장은 MAJOR의 정의와 모순된다. 워킹그룹이 정오표를 내기 전까지 이 책은 보수적으로 읽을 것을 권고한다. 이름 변경과 제거는 모두 메이저 변경으로 취급하고, 이전 메이저 버전은 두 병행 기간을 모두 충족하는 최소 180일 동안 유지하며, 폐기 예정 필드는 최소 6개월 전에 예고하고, 실제로 적용한 기간을 구현체의 프로파일 매니페스트에 밝힌다(제8장). 각 경우에 가장 긴 기간을 약속한 배포는 어느 판본의 규칙도 어기지 않는다.
이 장의 레코드에는 버전 관리의 차원이 하나 더 있다. 검증 보고서와 크레딧은 작성 기준이 된 스키마가 폐기된 뒤에도 수십 년 동안 유효한 증거로 남으며, 검증 기록은 영구 보존 대상이다(FOUNDATION §5.1.3). 스키마가 바뀌었다고 옛 레코드를 다시 써서는 안 된다. 옛 레코드는 스스로 선언한 스키마 버전으로 읽고, 이전(migration)이 필요하면 원본을 가리키는 새 파생 레코드를 만든다.
제4장에서는 이 레코드들을 실어 나르는 인터페이스, 즉 발견·인증과 REST, GraphQL, WebSocket 엔드포인트를 다룬다.