BuildSpec Contract — KPubData Builder¶
1. 목적¶
BuildSpec은 Builder가 실행하는 모든 빌드의 단일 입력 계약입니다. Builder는 BuildSpec을 기준으로 source 실행, export, output 디렉터리 결정, publish 요청, manifest 기록을 수행합니다.
참고: Bronze/Silver/Gold는 Builder 내부의 Medallion pipeline stage이며 orchestrator가 관리합니다. 현재 BuildSpec에는 이를 직접 제어하는 사용자 노출 필드(예:
pipeline.stages)를 추가하지 않습니다.
2. 최소 필수 구조¶
다음 구조가 최소 요구사항입니다.
dataset_id: weather-village-forecast
title: "동네예보 데이터셋"
description: "기상청 동네예보 서비스에서 수집한 기상 예보 데이터"
sources:
- provider: datago
dataset: village_fcst
params:
base_date: "20250401"
nx: 55
ny: 127
exports:
- kind: markdown
output_path: artifacts/weather_report.md
3. 필드 분류¶
3.1 Required fields¶
| 필드 | 타입 | 설명 |
|---|---|---|
dataset_id |
string | 빌드 대상 데이터셋의 전역 식별자 |
title |
string | 사람이 읽는 데이터셋 제목 |
description |
string | 빌드 목적과 데이터 설명 |
sources |
array | 1개 이상의 입력 소스 정의 |
exports |
array | 1개 이상의 출력 대상 정의 |
3.2 Optional fields¶
| 필드 | 타입 | 설명 |
|---|---|---|
publish |
boolean | 빌드 후 게시까지 수행할지 여부 (기본값: false) |
metadata |
object | 산출물에 실을 임의 메타데이터 (JSON 호환 값) |
splits |
object | 데이터셋 분할 정의 |
pii |
object | PII 검출 정책 |
license |
string | 데이터셋 라이선스 또는 이용허락 범위 |
quality |
object | Silver 통계 기반 품질 임계 정책 |
composition |
object | 두 source를 join해 하나의 결합 Gold dataset을 추가로 만드는 계약 (#506) |
4. 필드 상세¶
4.1 dataset_id¶
- 타입:
string - 예시:
weather-village-forecast - 의미: 빌드 전체를 식별하는 dataset ID입니다.
4.2 title¶
- 타입:
string - 예시:
"2025년 동네예보 데이터셋" - 의미: 사람이 읽는 데이터셋 제목입니다.
4.3 description¶
- 타입:
string - 예시:
"기상청 동네예보 서비스에서 수집한 기상 예보 및 실제 관측 데이터" - 의미: 빌드 목적과 데이터 내용을 설명합니다.
4.4 sources (배열)¶
kind 필드로 세 종류의 source를 표현합니다(#498). kind를 생략하면 항상
public_api로 해석되므로 기존 BuildSpec은 수정 없이 그대로 동작합니다.
| kind | 의미 |
|---|---|
public_api (기본) |
기존 kpubdata provider/dataset 참조 |
file |
POST /uploads로 사전 업로드한 파일(CSV/JSON/JSONL/Parquet) 참조 |
url |
안전한 GET(Auth=None) HTTP(S) 소스 |
alias/schema는 세 kind 공통 필드입니다. 그 외 필드는 kind별로 서로
배타적이며, 다른 kind의 필드가 섞이면 BuildSpec 로드 시 즉시 거부됩니다.
kind: public_api (기본값)¶
sources:
- provider: datago
dataset: village_fcst
params:
base_date: "20250401"
nx: 55
ny: 127
alias: forecast
schema:
required: [base_date, nx, ny]
dtypes:
nx: int64
ny: int64
casts:
nx: int64
ny: int64
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
kind |
string | 아니오 | 생략 시 public_api |
provider |
string | 예 | kpubdata provider 이름 |
dataset |
string | 예 | provider 내부 dataset 이름 |
params |
object | 아니오 | source 호출 파라미터 (JSON 호환 값) |
alias |
string | 아니오 | 조립 단계에서 사용할 사용자 정의 소스 이름 |
schema |
object | 아니오 | Silver 정규화 및 required-column/dtype 검증 계약 |
kind: file (#498)¶
POST /uploads가 발급한 upload_id를 참조합니다. 파일시스템 경로를 직접
쓸 수 없습니다 — upload_id는 서버가 발급한 불투명한 식별자(upl_<hex32>)이며,
업로드 자체는 principal의 owner_id로 격리됩니다(다른 사용자의 upload_id를
참조할 수 없음). format/encoding은 업로드 시점에 검증된 값과 정확히
일치해야 합니다.
sources:
- kind: file
upload_id: upl_0123456789abcdef0123456789abcdef
format: csv
encoding: utf-8
alias: uploaded_trades
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
kind |
string | 예 | file |
upload_id |
string | 예 | POST /uploads 응답의 upload_id |
format |
string | 예 | csv | json | jsonl | parquet (업로드 시 검증된 값과 일치해야 함) |
encoding |
string | 아니오 | 텍스트 포맷(csv/json/jsonl) 디코딩 인코딩. 기본값 utf-8. parquet은 무시됨 |
alias |
string | 아니오 | 조립 단계에서 사용할 사용자 정의 소스 이름 |
schema |
object | 아니오 | Silver 정규화 및 required-column/dtype 검증 계약 |
지원 포맷은 CSV/JSON(top-level array of objects)/JSONL/Parquet입니다. Excel/ZIP은 범위 밖입니다.
kind: url (#498 P0)¶
GET, Auth=None인 안전한 HTTP(S) 소스만 P0 범위입니다. Bearer credential 연동은 #492 이후 P1 확장입니다.
sources:
- kind: url
endpoint: https://example.org/data.json
method: GET
alias: external_feed
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
kind |
string | 예 | url |
endpoint |
string | 예 | fetch할 절대 URL. https만 허용하며 userinfo(user:pass@host)는 금지 |
method |
string | 아니오 | 기본값 GET. P0는 GET만 허용 |
format |
string | 아니오 | json | jsonl | csv. 생략 시 응답 Content-Type으로 추론(기본 json) |
alias |
string | 아니오 | 조립 단계에서 사용할 사용자 정의 소스 이름 |
schema |
object | 아니오 | Silver 정규화 및 required-column/dtype 검증 계약 |
SSRF 방어(#498): https 외 scheme(http/file/ftp 등) 거부, userinfo
포함 URL 거부, hostname을 DNS로 직접 resolve해 loopback/private/link-local/
reserved 등 비공인(non-global) 주소로의 fetch 거부(하나라도 비공인이면 전체
거부), 실제 TCP 연결은 검증된 IP에 직접 연결(DNS rebinding 방지), redirect마다
동일 검증 반복(최대 5회), connect/read timeout과 응답 크기 상한 적용. 임의
header나 POST/PUT/PATCH는 계약에 필드 자체가 없어 표현할 수 없습니다.
다중 사용자 배포에서는 쓸 수 없습니다(#685). OIDC_ISSUER 나 ENFORCE_OWNERSHIP 이
설정된 배포(ADR 0012 2026-09-30 개정)에서는 url 소스가 있는 spec 을 POST /preview,
POST /build, POST /builds 가 요청을 하나도 보내기 전에 403 url_source_forbidden 으로
거부하고, 응답의 sources 가 문제 소스를 index·alias·path 로 짚습니다. 단일 사용자
배포에서는 위 규칙 그대로 동작합니다.
provenance/manifest에는 query string이 제거된 endpoint만 남습니다.
schema는 다음 선택 필드를 지원합니다.
| 필드 | 타입 | 설명 |
|---|---|---|
required |
array |
반드시 존재해야 하는 컬럼 목록 |
dtypes |
object |
컬럼별 기대 dtype |
casts |
object |
정규화 단계에서 적용할 컬럼별 dtype 캐스팅 |
rename |
object |
원 필드명 → canonical 컬럼명 매핑 (#611) |
derived |
array | 기존 컬럼에서 새 컬럼을 만드는 규칙 (#611) |
제거된 normalization_mode는 허용하지 않습니다. 정규화는 schema.casts로 선언합니다.
적용 순서는 rename → casts → derived입니다. 따라서 required/dtypes/casts/
derived의 컬럼명은 모두 rename 이후의 이름을 가리킵니다. rename이 원본에 없는
컬럼을 가리키면 TabularError로 실패합니다 — 상류 스키마 변경을 조용히 넘기지 않습니다.
rename은 이름만 바꾸고 컬럼을 버리지 않습니다. 배포용 스크립트(scripts/pipeline/
transform.py)의 column_mapping은 매핑되지 않은 컬럼을 드롭하지만, Silver는 Bronze의
행과 열을 보존하는 계층이므로 여기서는 드롭하지 않습니다.
casts는 _NAMED_DTYPES(bool/date/datetime/float/int/str 계열)에 더해 named formatted
cast를 받습니다.
| cast | 동작 |
|---|---|
int_comma |
천단위 구분자와 주변 공백을 제거한 뒤 Int64로 캐스팅 ("120,000" → 120000) |
int_comma가 없으면 금액 컬럼에 int를 선언했을 때 모든 값이 null이 되어 #188의
data-loss 가드가 빌드를 실패시킵니다.
derived의 각 규칙은 name / kind / columns를 갖습니다. 자유형 표현식은 받지
않습니다(RangeRule/CompareColumnsRule과 같은 typed rule 관례).
| kind | columns | 결과 |
|---|---|---|
date_parts |
정확히 3개 (year, month, day) | Date 컬럼 |
join_key |
1개 이상 | \|로 이어붙인 문자열 복합키 |
join_key는 composition의 단일 컬럼 equi-join으로 다중 키 조인을 표현할 때 씁니다.
schema:
rename:
sggCd: district_code
dealAmount: deal_amount
casts:
deal_amount: int_comma
derived:
- name: deal_date
kind: date_parts
columns: [dealYear, dealMonth, dealDay]
- name: join_key
kind: join_key
columns: [district_code, year_month]
rename과 derived는 canonical BuildSpec snapshot에 실리므로 spec digest에 반영됩니다
— 변환 규칙을 바꾸면 digest가 바뀝니다.
sources[].gold — 게시본의 컬럼과 행 (#659)¶
Silver 는 Bronze 의 모든 컬럼과 행을 보존하고, 품질은 거기서 잰다(#611). 게시되는 것은 Gold 이므로 컬럼 선택과 행 필터는 Gold 에 둔다(ADR 0018 선택지 C).
sources:
- provider: datago
dataset: apt_trade
gold:
select: [district_code, apartment_name, deal_amount_10k_krw, deal_date]
filters:
- column: deal_amount_10k_krw
op: gt
value: 0
| 필드 | 설명 |
|---|---|
select |
Gold 에 남길 컬럼, 이 순서대로. 생략하면 전부 |
filters[].column |
비교할 컬럼. select 가 버리는 컬럼도 쓸 수 있다(필터가 먼저) |
filters[].op |
eq ne gt ge lt le in(리스트) not_null(값 없음) |
filters[].value |
비교할 값. 식(expression)이 아니라 값이다 — 평가하지 않는다 |
pii_columns |
kpubdata spec 의 license.pii_columns 에 더해 이 BuildSpec 이 PII 로 선언하는 Silver 컬럼(#689). Silver 에 없는 이름이면 소스가 실패한다 |
publish_unmasked |
선언된 PII 컬럼 중 마스킹하지 않고 게시할 컬럼(#689). 각각 manifest 경고로 남는다 |
- null 은 어떤 비교도 통과하지 않는다(
not_null과 null 이 아닌 값에 대한ne제외). - Silver 에 없는 컬럼이나 비교할 수 없는 타입이면 그 소스가 실패한다 — 스펙과 다른 표를 게시하지 않는다.
- manifest 의
gold_selection에 Silver 행 수(input_rows)·Gold 행 수(output_rows)·dropped_rows와 규칙이 남는다.row_counts는 Silver 기준 그대로다. 웨어하우스 스냅샷의 행 수와 데이터셋 카드(컬럼·표본 행)는 Gold 를 따른다. composition과 함께 쓸 수 없다 — 합성은 소스별 Gold 대신 하나의 합성 Gold 를 만든다.- canonical snapshot 에 실리므로 digest 에 반영된다. 선언하지 않은 spec 의 digest 는 그대로다.
- 선언된 PII 컬럼은 Gold 에서 기본으로 마스킹된다(#689). 컬럼의 dtype 은 유지된다(#902):
텍스트 컬럼은 값이 있던 칸이
[masked]가 되고(null 은 null), 텍스트가 아닌 컬럼(숫자, 날짜, 리스트)은 전부 null 이 된다. 그래서 Gold schema 는 Silver 와 같다. manifestpii_masking의 각masked항목이 어느 쪽인지(masked_as:token/null) 적는다. - kpubdata 가 선언했지만 이 소스에 없는 필드(표기가 달라진 경우 등)는 건너뛰되 manifest
pii_masking.declared_absent에 kpubdata 표기 그대로 남는다(#902).
4.5 exports (배열)¶
각 export 대상은 다음 필드를 가집니다.
exports:
- kind: markdown
output_path: artifacts/weather_report.md
- kind: jsonl
output_path: artifacts/data.jsonl
options:
indent: 2
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
kind |
string | 예 | exporter 레지스트리 키 (markdown, jsonl, parquet, csv, huggingface, kaggle 등) |
output_path |
string | 예 | output_dir 기준 상대 출력 경로 |
options |
object | 아니오 | exporter별 선택 옵션 (JSON 호환 값) |
Studio → Builder 필드명 매핑 (#264)¶
KPubData Studio에서는 ExportTarget.format이라는 필드명을 사용하지만, Builder에서는 kind를 사용합니다. Studio에서 Builder로 BuildSpec을 전송할 때, Studio의 specMapping.ts:toBuilderSpec() 함수가 자동으로 필드명을 변환합니다.
| Studio (format) | Builder (kind) | 비고 |
|---|---|---|
"markdown" |
"markdown" |
|
"jsonl" |
"jsonl" |
|
"parquet" |
"parquet" |
|
"csv" |
"csv" |
|
"huggingface" |
"huggingface" |
|
"kaggle" |
"kaggle" |
참고: 값 자체는 동일하며, 단지 필드명만
format→kind로 변환됩니다. Builder API를 직접 호출하는 경우(CLI 등)에는 항상kind를 사용해야 합니다.
4.6 publish (optional)¶
publish: true
빌드 후 자동으로 게시(publish) 단계까지 실행할지 여부입니다. 기본값은 false입니다.
계획(planned)/미구현: 현재
publish: true로 설정해도 게시 로직이 실행되지 않습니다. 게시 기능은 향후 릴리스에서 활성화될 예정입니다.
4.7 metadata (optional)¶
metadata:
author: "Sisyphus-Junior"
version: "1.0.0"
tags: [weather, forecast]
coverage:
years: [2024, 2025]
임의 메타데이터입니다. 키는 문자열이고 값은 null, 문자열, 숫자, 불리언, 배열, 객체로 구성된 표준 JSON 호환 값이어야 합니다. NaN과 Infinity 및 순환 참조는 허용하지 않습니다.
현재 구현에서 산출물에 반영되는 항목은 제한적입니다. version은 dataset card의
버전 표기로, license는 최상위 license 필드 미지정 시 레거시 폴백으로 사용됩니다.
그 외 임의 메타데이터는 검증·스펙 저장 목적으로만 보관되며 산출물, exporter,
manifest로 전달되지 않습니다.
4.8 splits (optional)¶
splits:
mode: ratio
ratios:
train: 0.8
val: 0.1
test: 0.1
seed: 42
데이터셋을 명명된 분할로 나누는 방법을 정의합니다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
mode |
string | 예 | 분할 방식 (ratio: 비율 기반, key: 컬럼 값 기반) |
ratios |
object | 아니오 | ratio 모드에서 분할 이름 → 비율 매핑 (합이 1.0이어야 함) |
key |
string | 아니오 | key 모드에서 분할 기준이 되는 컬럼 이름 |
seed |
integer | 아니오 | ratio 모드의 결정적 셔플 시드 (기본값: 0) |
ratio 모드는 비율의 합이 1.0이어야 하며, key 모드는 비어 있지 않은 key가
필요합니다. 이 선언은 Gold 패키지의 분할 생성에 사용됩니다.
4.9 pii (optional)¶
pii:
mode: warn
allow_columns: [contact_hint]
| 필드 | 타입 | 설명 |
|---|---|---|
mode |
string | block(기본), warn, allow 중 하나 |
allow_columns |
array |
PII 스캔에서 제외할 컬럼 목록 |
스캔은 Gold 를 만들기 전에 Silver 를 본다. sources[].gold 의 PII 선언과는 이렇게
맞물린다(#902).
- 선언되어 Gold 에서 마스킹되는(또는
gold.select가 버리는) 컬럼은 이미 처리된 것으로 보고 스캔 결과에서 뺀다. 선언·마스킹된 전화번호 컬럼은allow_columns없이도mode: block을 통과한다. allow_columns는 "이 컬럼의 평문을 게시해도 된다"는 수용이다. 스캔 게이트뿐 아니라 웨어하우스 프로필과 export 도 그렇게 읽는다. 선언된 컬럼의 마스킹을 풀지는 않는다.gold.publish_unmasked는 선언된 컬럼을 평문으로 게시하겠다는 선택이다. 스캔 게이트를 면제하지 않는다 —mode: block이면 그 컬럼을allow_columns에도 적어야 한다.- 즉 어느 쪽도 다른 쪽을 함의하지 않는다. 선언된 컬럼을 평문으로 내보내려면 둘 다 필요하다.
publish: true와 pii.mode: allow의 조합은 허용하지 않습니다.
4.10 license (optional)¶
license: CC-BY-4.0
SPDX 식별자 또는 자유 텍스트 라이선스 선언입니다. publish: true이면 반드시
지정해야 합니다.
4.10a refresh_cadence (optional)¶
테이블을 얼마나 자주 갱신할 것으로 기대하는지 — ISO 8601 기간의 주·일·시간(P1D, PT6H,
P1W, P1DT12H)만 받는다. 월·연은 길이가 달라 받지 않는다(#781, 소유자 결정 D7).
refresh_cadence: P1D
GET /datasets 의 status_axes.health 가 이것으로 정해진다: 마지막 성공한 갱신이 한 주기보다
오래됐으면 stale, 아니면 healthy. 선언이 없거나 성공한 갱신이 없으면 unknown — 실제
실행 간격에서 추측하지 않는다. canonical snapshot 에 실리므로 digest 에 반영되고, 선언하지 않은
spec 의 digest 는 그대로다.
4.11 quality (optional)¶
Silver 통계·테이블에 대한 품질 규칙 선언입니다. 각 규칙 위반은 quality.evaluator가
QualityCheckResult(PASS 포함)로 구조화해 manifest(quality_results)와
GET /builds/{run_id}/quality, GET /datasets/{dataset_id}/quality/history에
반영합니다(#486). Preview(POST /preview)와 Build는 동일한 evaluator를 공유하므로
같은 데이터/규칙에는 항상 같은 판정을 냅니다.
기존 syntax (#446, 하위 호환)¶
quality:
max_duplicate_rate: 0.01
max_null_ratio:
temperature: 0.05
min_rows: 100
| 필드 | 타입 | 설명 |
|---|---|---|
max_duplicate_rate |
number | 허용할 최대 중복 행 비율. 초과 시 위반(초과가 아니면 위반 아님 — 경계값은 통과). |
max_null_ratio |
object |
컬럼별 허용 최대 null 비율 |
min_rows |
integer | 요구하는 최소 행 수. 미만이면 위반. |
기존 syntax의 위반은 기본적으로 WARN입니다 — Build는 계속 진행하고 결과만 기록합니다(#446 시절 semantics 그대로 유지).
명시적 WARN/FAIL severity (#486)¶
각 규칙에 대응하는 *_severity 필드로 명시적 심각도를 선언할 수 있습니다. 값은
"warn"(기본) 또는 "fail"이며, max_null_ratio는 컬럼별 override 맵을 씁니다.
quality:
max_duplicate_rate: 0.01
max_duplicate_rate_severity: fail # 명시적 FAIL
max_null_ratio:
temperature: 0.05
max_null_ratio_severity:
temperature: warn # 명시적 WARN(기본과 동일하지만 선언 가능)
min_rows: 100
min_rows_severity: fail
FAIL로 판정된 rule이 하나라도 있으면 해당 source는 Gold 진입 전에 실패 처리됩니다. WARN은 계속 진행합니다.
range (#486)¶
숫자 컬럼의 최소/최대 범위를 typed rule로 선언합니다. 자유형 Python/eval은
허용하지 않습니다. min/max는 포함(inclusive) 경계이며 최소 하나는 있어야
합니다. null 값은 범위 위반으로 계산하지 않습니다(missing/null 여부는
max_null_ratio가 담당 — 역할 분리).
quality:
range:
- column: price
min: 0
max: 1000000
severity: fail
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
column |
string | 예 | 대상 컬럼명 |
min |
number | 아니오 | 허용 최소값(포함) |
max |
number | 아니오 | 허용 최대값(포함) |
severity |
string | 아니오 | warn(기본) | fail |
평가 결과의 threshold는 {"min": ..., "max": ...} 구조로 원래 경계를
보존합니다. 컬럼 dtype 때문에 숫자 범위를 평가할 수 없으면 규칙을 생략하지 않고
선언된 severity의 WARN/FAIL 결과와 안전한 detail을 기록합니다.
compare_columns (#486)¶
두 컬럼 간 비교를 제한된 operator만으로 선언합니다. 자유형 expression/eval은 허용하지 않습니다. 두 컬럼 모두 null이 아닌 행만 평가하며, 비교 불가능한 행을 자동으로 통과 처리하지 않습니다.
quality:
compare_columns:
- left: sale_price
operator: gte
right: list_price
severity: warn
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
left |
string | 예 | 왼쪽 컬럼명 |
operator |
string | 예 | eq|ne|gt|gte|lt|lte 중 하나 |
right |
string | 예 | 오른쪽 컬럼명 |
severity |
string | 아니오 | warn(기본) | fail |
평가 결과의 threshold는 {"operator": ..., "right_column": ...} 구조로
연산자와 오른쪽 컬럼을 보존합니다. dtype이 호환되지 않는 경우에도 선언된 severity의
WARN/FAIL 결과를 남기며, WARN은 Build를 계속하고 FAIL은 Gold 진입을 막습니다.
유효하지 않은 operator는 BuildSpec 로드 단계에서 즉시 거부됩니다.
Schema 계약과의 관계¶
sources[].schema.required/dtypes(#437)도 같은 QualityCheckResult 모델로
구조화됩니다(category: "schema", rule: "required_column" | "dtype"). required
컬럼이 없어 dtype을 검사할 수 없는 경우 dtype PASS를 만들지 않습니다 — "required
FAIL + dtype PASS" 같은 모순을 피합니다. Schema 위반은 severity 설정과 무관하게
항상 hard failure입니다(#189의 기존 게이트 그대로).
4.12 composition (optional)¶
두 source의 검증된 Silver 테이블을 join해 하나의 결합 Gold dataset을
추가로 만드는 계약입니다(#506). composition 없는 기존 multi-source
BuildSpec은 그대로 동작합니다 — source별 독립 Gold가 회귀 없이 유지되고,
composition은 부가 산출물로 gold/{composition.name}/에 추가됩니다.
초기 범위는 두 source, 단일 equi-join으로 제한합니다. 자유형 SQL transformation과 3개 이상의 join graph는 지원하지 않습니다.
sources:
- provider: datago
dataset: apt_trade
alias: sales
- provider: datago
dataset: region_meta
alias: region
composition:
name: sales_with_region
join:
left: sales
right: region
left_key: region_id
right_key: id
type: inner
on_duplicate_key: warn
복합 key와 cardinality 선언(#698):
composition:
name: trade_with_population
join:
left: trade
right: population
keys:
- left: region_id
right: region_id
- left: month
right: month
cardinality: many_to_one
on_null_key: warn
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
string | 예 | 결합 결과 Gold dataset 이름. 다른 source의 output key(alias 또는 provider.dataset)와 겹칠 수 없습니다. |
join |
object | 예 | 아래 join 계약 |
join.left |
string | 예 | 왼쪽 source의 alias (sources[].alias 참조) |
join.right |
string | 예 | 오른쪽 source의 alias |
join.keys |
list | left_key/right_key와 택일 |
복합 join key. {left, right} 컬럼 쌍의 목록이며, 모든 쌍이 같을 때 행이 매칭됩니다(#698). |
join.left_key |
string | keys와 택일 |
왼쪽 테이블의 join key 컬럼명 (단일 쌍 축약형) |
join.right_key |
string | keys와 택일 |
오른쪽 테이블의 join key 컬럼명 (단일 쌍 축약형) |
join.type |
string | 아니오 | inner(기본) | left |
join.on_duplicate_key |
string | 아니오 | warn(기본) | fail |
join.cardinality |
string | 아니오 | one_to_one | one_to_many | many_to_one | many_to_many. 생략하면 선언 없음(검증하지 않음) (#698) |
join.on_null_key |
string | 아니오 | warn(기본) | fail. key 컬럼 중 하나라도 null인 행의 처리 (#698) |
keys와 left_key/right_key는 정확히 하나만 써야 합니다. 둘 다 쓰거나
둘 다 없으면 스펙 로드가 실패합니다. left_key/right_key는 keys에 쌍이 하나인
경우의 축약형이며, 기존 스펙은 그대로 동작합니다.
alias 필수/중복 규칙: composition.join.left/right가 참조하는 두
source는 반드시 비어 있지 않은 alias를 선언해야 하며, composition을
쓰는 BuildSpec에서는 선언된 모든 sources[].alias 값이 서로 달라야
합니다(빈 alias는 이 유일성 검사 대상이 아닙니다). 이 규칙은 composition이
없는 BuildSpec에는 적용되지 않습니다.
join key 검증 시점: alias 참조·type/on_duplicate_key 어휘 같은
구조적 문제는 validate_spec(구조 검증)이 스펙 파싱 직후 잡습니다. 하지만
join key 컬럼의 존재 여부와 dtype 호환성은 두 source가 각각 Silver
단계를 통과해야만 알 수 있으므로, 여기서는 검증할 수 없고 빌드 파이프라인의
런타임 게이트(orchestrator, Silver 완료 직후)에서 확인해 실패시킵니다. dtype은
완전히 동일해야 join이 허용됩니다 — 자동 캐스팅은 하지 않으며, 타입을
맞추려면 sources[].schema.casts(§4.4, #437)로 Silver 단계에서 명시적으로
정규화하세요.
교집합 key 규칙(#698): cardinality와 중복 key는 양쪽에 모두 나타나는
key(교집합)만으로 판정합니다. 한쪽에만 있는 key는 몇 번 반복되든 결과 행을
곱할 수 없기 때문입니다. 예를 들어 왼쪽 key가 A, A, 오른쪽이 B, B이면 양쪽
모두 non-unique이지만 교집합이 없으므로 위반도 경고도 아닙니다. 복합 key는
컬럼 튜플 단위로 판정합니다 — 각 컬럼만 보면 중복이어도 쌍이 유일하면 유일한
key입니다.
cardinality 검증: 교집합 key마다 양쪽 행 수를 세어, 어떤 key든 왼쪽에서
반복되면 왼쪽이 "many", 오른쪽에서 반복되면 오른쪽이 "many"인
observed_cardinality를 얻습니다(왼쪽→오른쪽으로 읽습니다: many_to_one은
왼쪽은 반복 가능, 오른쪽은 유일). 선언된 cardinality가 관측값을 허용하지
않으면 composition이 실패하고, 오류 메시지에 선언값·관측값과 가장 많은 행을
곱하는 key 하나의 좌우 행 수가 담깁니다. "many"는 반복을 허용할 뿐 요구하지
않습니다 — many_to_many는 모든 관측값을, one_to_one은 one_to_one만
허용합니다.
duplicate-key row explosion guard: 교집합 key 중 하나라도 양쪽 모두에서
반복되면(many-to-many) 결과 행이 곱셈으로 폭증하므로 duplicate_key_warning이
켜집니다. 기본값 on_duplicate_key: warn은 결과를 만들고 manifest에 경고와
함께 정확한 행 수/distinct key 수를 남기며, fail은 Gold 진입 전에
composition을 실패 처리합니다. #698 이전에는 양쪽이 각각 non-unique이기만
하면(교집합이 없어도) 경고했습니다.
null key: key 컬럼 중 하나라도 null인 행은 어떤 행과도 매칭되지 않으므로
inner join에서 조용히 빠집니다. 이 행 수를 left_null_key_rows/
right_null_key_rows로 manifest에 기록하고 경고 로그를 남기며(on_null_key:
warn), on_null_key: fail이면 composition을 실패시킵니다.
참조된 source가 실패한 경우: composition.join이 참조하는 source 중
하나라도 Bronze/Silver를 통과하지 못하면 join을 시도하지 않고
skipped로 기록합니다(다른 source의 독립 Gold는 영향받지 않습니다).
provenance: manifest의 composition 필드(CompositionProvenance)에
join 조건(source/key/type)과 좌우 원본 행 수·distinct key 수·결과 행 수가
그대로 기록되어 추적 가능합니다. #698부터는 keys, 선언된 cardinality,
observed_cardinality, left_unmatched_ratio/right_unmatched_ratio(상대편에
매칭되는 key가 없는 행 — null key 행 포함 — 의 비율, 빈 쪽은 0.0),
expansion_ratio(결과 행 수 / 왼쪽 행 수, 왼쪽이 비면 null),
left_null_key_rows/right_null_key_rows도 기록합니다. POST /build 응답에도 outcomes(소스별)와
별도로 composition(결합 결과) 키가 노출됩니다.
5. 검증 규칙¶
Builder는 최소한 다음 규칙을 검증해야 합니다.
dataset_id는 비어 있지 않은 문자열이어야 합니다.title은 비어 있지 않은 문자열이어야 합니다.description은 비어 있지 않은 문자열이어야 합니다.sources는 1개 이상 정의되어야 합니다.- 각
sources[].provider는 비어 있지 않아야 합니다. - 각
sources[].dataset은 비어 있지 않아야 합니다. exports는 1개 이상 정의되어야 합니다.- 각
exports[].kind는 비어 있지 않아야 합니다. - 각
exports[].output_path는 비어 있지 않아야 합니다. - 지원하지 않는 필드 조합은 검증 단계에서 실패해야 합니다.
6. 버전 호환성¶
현재 BuildSpec 계약은 단일 버전(schema_version 필드 없음)으로 운영됩니다.
호환성 원칙:
- minor 문서 확장은 가능한 한 backward compatible하게 추가합니다.
- breaking change는 신규 계약 버전으로만 도입합니다.
- Studio는 Builder가 지원하는 버전만 전송해야 하며 임의 해석을 추가하면 안 됩니다.
7. 계약 원칙 요약¶
- BuildSpec은 Builder가 소유합니다.
- BuildSpec은 실행 계획이지 UI 상태 저장 포맷이 아닙니다.
- exports/publish는 source 정의를 대체하지 않습니다.
- manifest는 BuildSpec의 결과 기록물이지 입력이 아닙니다.
- 검증된 실행 입력은 run workspace의
buildspec.yaml에 canonical YAML로 저장됩니다. 이 snapshot은 결정적 직렬화와sha256:<hex>digest의 기준이며, source pipeline이 부분 실패해도 남습니다. 검증 실패 입력은 snapshot으로 기록하지 않습니다. metadata,sources[].params,exports[].options안의 명시적 credential 키 값은 snapshot에서<redacted>로 대체됩니다. 따라서 inline credential이 있던 snapshot은 감사·비교용이며, 재실행 시 credential을 환경 또는 서비스 설정에서 다시 공급해야 합니다.spec_digest는 저장된 redaction 후 canonicalbuildspec.yamlbytes의 SHA-256입니다. 따라서 credential 값만 다르고 나머지 spec이 같으면 두 snapshot과 digest도 같을 수 있으며, 이는 secret을 digest로 식별하거나 노출하지 않기 위한 의도된 보안 정책입니다.
8. 관련 문서¶
| 문서 | 설명 |
|---|---|
| ARCHITECTURE.md | BuildSpec 중심 설계 |
| API_CONTRACT.md | BuildSpec을 받는 서비스 계약 |
| BOUNDARY.md | Builder-Studio 경계 |
| ALGORITHM.md | 전체 빌드 알고리즘 명세 |