ADR 0006: 컬럼 메타 계약 — 저장 타입, 의미, 표시를 나눈다¶
상태¶
채택됨(Accepted) — 2026-09-29
요약 (English summary)¶
A column's metadata carried three different things under overlapping names: the type a value is cast to, what the value means, and how it should be shown. This ADR separates them in KPubData. Storage type stays
type(the cast result). Meaning is a newsemantic_kind(code, measure, date, period, text, flag), andcoderequires the source string to be kept. Display reuses the fields KPubData already has —FieldDescriptor.titleandFieldConstraints.format— instead of addinglabelor a secondformat. Asemantic_kindthat contradicts the storage type is a spec validation error. KPubData owns only the declared layer; the precedence between user notes, KPubData, catalogue and Builder estimates is Builder's contract (kpubdata-builder#813).
문제¶
2026-09-29 기준, 컬럼 하나의 메타가 세 군데에 흩어져 있고 경계가 없다 (#644).
| 위치 | 필드 |
|---|---|
spec FieldSpec (core/spec.py) |
name, type, source_name, unit, transform, description |
공개 FieldDescriptor (core/models.py) |
name, title, type, description, nullable, raw, constraints |
공개 FieldConstraints |
max_length, min_value, max_value, pattern, allowed_values, format |
type 하나가 "무엇으로 캐스팅하나" 와 "이 값이 무엇인가" 를 함께 떠맡았다. 그래서
- 우편번호·지번 같은 코드가 숫자처럼 생겼다는 이유로
integer로 선언돼 앞자리 0 을 잃었다 (#613 — 번들 spec 에서 16개 필드). - Builder 가 컬럼이 식별자인지 판정할 근거가 없다 (kpubdata-builder#702).
- 표시명·형식을 새로 넣으려 하면 기존
title·format과 같은 의미가 두 곳에서 운영된다.
결정¶
1. 세 층¶
| 층 | 질문 | KPubData 의 자리 |
|---|---|---|
| 저장 타입 | 값을 무엇으로 캐스팅하나 | FieldSpec.type → FieldDescriptor.type (그대로) |
| 의미 | 이 값은 무엇인가 | FieldSpec.semantic_kind → FieldDescriptor.semantic_kind (신규) |
| 표시 | 사람에게 어떻게 보이나 | FieldSpec.title → FieldDescriptor.title, FieldSpec.format → FieldConstraints.format, unit |
저장 타입은 바꾸지 않는다: string, integer, number, boolean — 캐스팅 결과다.
2. 의미 — semantic_kind¶
| 값 | 뜻 | 허용 저장 타입 |
|---|---|---|
code |
식별자·분류 코드. 산술의 대상이 아니다 (우편번호, 지번, 역코드, 시각코드) | string 만 |
measure |
양. 더하거나 평균낼 수 있다 (금액, 면적, 인원) | integer, number |
date |
날짜 | string (ISO YYYY-MM-DD, date_yyyymmdd 변환 후) |
period |
기간 단위 (연, 연월) | string (YYYY, YYYY-MM) |
text |
자유 텍스트, 이름 | string |
flag |
참·거짓 | boolean |
code는 원천 문자열 보존을 요구한다. 숫자로 캐스팅하지 않고, 숫자 변환transform(to_int,to_float,strip_comma)도 허용하지 않는다. #613 의 해법(코드 컬럼을string으로)이 이 규칙의 첫 적용이다.- 선언이 없으면 모른다. KPubData 는 추정하지 않는다. 추정은 Builder 층의 일이고, 추정임을 표시해야 한다.
- 목록에 없는 값은 spec 검증 오류다. 소비자(Builder·Studio)는 모르는 값을 만나면 원문 그대로 표시한다 — 새 값이 추가돼도 깨지지 않게.
3. 표시 — 기존 필드를 재사용한다¶
- 표시명은
FieldDescriptor.title.label을 따로 만들지 않는다. - 표시 형식은
FieldConstraints.format(예:"YYYY-MM","url"). 두 번째format을 만들지 않는다. description은 지금처럼 설명문.
4. 단위¶
unit은 원천 문서로 확인된 값만 적는다. 모르면 비운다.unit은measure에만 붙는다. 다른 의미에 붙으면 검증 오류.- 배율·통화(
천원= 1,000 × 원)를 구조로 나누는 일은 이 ADR 범위가 아니다. 지금은 원문 단위 문자열 그대로 둔다. 나눌 때 별도 결정.
5. 충돌은 검증 오류¶
spec 로딩(core/spec.py)과 scripts/validate_spec.py 가 다음을 거부한다.
semantic_kind가 위 표의 허용 저장 타입과 맞지 않는다 (code+integer등).code에 숫자 변환transform이 있다.unit이measure가 아닌 필드에 있다.semantic_kind가 목록에 없는 값이다.
선언이 없는 필드는 검사하지 않는다 — 기존 spec 이 그대로 통과한다. 선언이 없는 코드 컬럼의 안전망은 #613 의 캐스팅 거부(0으로 시작하는 값은 숫자로 캐스팅하지 않음)다.
6. 우선순위는 Builder 의 계약¶
표시 메타가 여러 곳에서 올 때의 우선순위 — 사용자 주석 > KPubData 명세 > 카탈로그 > Builder 추정 — 는 Builder 계약(kpubdata-builder#813)에 둔다. KPubData 는 명세 층만 책임진다. 어느 층도 저장 타입·라이선스·PII 판정을 덮어쓰지 않는다.
그래서 무엇을 만드는가¶
이 ADR 은 계약이다. 구현은 #651 로 나눈다.
FieldSpec에semantic_kind,title,format추가,specs/schema.jsonenum 추가FieldDescriptor.semantic_kind추가,get_schema(#643)가 세 층을 채운다- 5절의 검증 규칙과, 각 규칙이 실패하는 것을 보이는 테스트
- 번들 spec 의 코드 컬럼(#613 의 16개)에
semantic_kind: code선언
규칙은 구현 이슈에서 게이트(검증 오류 + 실패 시연 테스트)와 함께 들어온다. 이 문서만으로 강제되는 것은 없다.
기각한 대안¶
code저장 타입 신설. 저장 타입과 의미를 다시 한 축에 섞는다.string으로 저장하고 의미를 따로 적으면 같은 효과에 builder 타입 매핑 변경이 없다.label·display_format신설.title·format이 이미 공개 모델에 있다. 같은 의미가 두 곳에 살면 어느 쪽이 맞는지 소비자가 매번 골라야 한다.- KPubData 가 의미를 추정. 값 모양(숫자, 0으로 시작)으로 추정하면 틀릴 때 조용히 틀린다. 추정은 추정임을 표시할 수 있는 Builder 층에 둔다.
관련¶
-
644 (이 ADR), #613 (코드 컬럼 앞자리 0), #643 (
get_schema)¶ - kpubdata-builder#813 (Builder 컬럼 메타 계약), kpubdata-builder#702 (식별자 판정)