ADR-0057: 레이크 데이터셋 정본과 운영 소비 경로, 승격·폐기 절차¶
- 상태: 제안됨
- 날짜: 2026-09-28
- 보류 범위: 이 ADR은 제안이다. 승인되지 않았고 구현도 완료되지 않았다. 2026-09-28에 작업 범위를 축소해, 기존 경로 통합·기존 데이터 이관·소비자 전환에 이 결정을 적용하는 일은 보류했다 (현황 대장 §7). 지금 적용하는 것은 §1~§4를 신규 수집 데이터의 저장 계약(설계 초안 §9)에 참고하는 데까지다. 예외가 하나 있다. 5분봉 애드혹 raw의 표준 raw 복사는 이미 실행됐다(추가만, 소비자 없음 — 대장 §3.1).
- 2026-10-05 추기: 맥락이 인용한 v1 분석엔진 소비자(
collect/intraday.py·pit.py·fin.py·flowhist.py·dgwide.py·tau_sidecar.py)는 #1140(ALPHA-1173)으로 소스째 제거됐다. 아래 "분석엔진" 서술은 작성 시점(v1) 기준이다.
맥락¶
분석엔진이 읽는 레이크 표면은 정본 존(raw·canonical·feature·operations_archive) 밖에도 있다.
analysis/backfill/{name}.parquet 10세트, draft/curated/source=dataguide/…, draft/canonical/*
Iceberg, 애드혹 raw(raw/kr_intraday/…·raw/fmp_5min_us/…), 다른 버킷의 Glue 5분봉 표
(market_data_kr.edge_intraday_5m)다. 2026-09-28 dev 실측과 코드 조사에서 드러난 사실은 다음과 같다
(수치와 경로는 현황 대장에 있다).
- 같은 값이 이름만 다른 두 곳에 있다.
fx_usdkrw·us_market백필은canonical/market_data/fx_daily·index_daily와 시가·종가가 전부 같다. 그런데change_pct는 정의가 다르다(일중 대 전일 대비). 이름이 비슷하다는 이유로 입력을 바꾸면 분석 의미가 조용히 바뀐다. - 정본이라 부르는 표의 생산자가 레포에 없다. Glue 5분봉 표는 레포 밖 적재가 08-05에 멈췄다. 그 표에만 있는 로컬 이력 54만 행은 canonical에 없다. fx·지수·금리 canonical도 레포 밖 일회성 적재였다. 둘 다 뷰는 성공하지만 공백은 보이지 않는다.
- 한 데이터셋에 writer가 둘이다. 5분봉 canonical은 롤업·백필 스크립트가 소유권 가드를 지켜 쓴다. 반면
collect/intraday.py는 같은part-0을 가드 없이 덮는다. - 버킷·경로 정본이 흩어져 있다. data-pipeline은
lake/storage.py빌더와canonical/tables.py선언을 쓴다. 분석엔진은 배포 경계 때문에 이를 import하지 못해 경로 문자열을 베끼고, 일부는 dev 버킷을 하드코딩한다 (pit.py·fin.py·flowhist.py·dgwide.py·tau_sidecar.py).
기존 결정과의 관계: - ADR-0028은 분석이 feature 산출물(canonical/feature 존 + Event Store)만 읽는다고 정했다. 임시 존 직독은 그 계약 밖에 있는데, 이를 드러내는 목록이 없다. - ADR-0043의 Dataset Contract가 신선도 판정 축이다. 새 축을 만들지 않는다. - ADR-0048은 설명 영속의 S3 폴백을 폐기했다. 시장 데이터 읽기 폴백(S3→로컬, Iceberg→canonical)은 그 결정의 대상이 아니다.
결정¶
1. 존의 경계¶
| 존 | 담는 것 | 규칙 |
|---|---|---|
raw/source=…/dataset=… |
공급자 응답 원본 | 실행 단위로 불변이다. 수신일(ingest_date)은 실제 수신일이고 복사일로 바꾸지 않는다 |
canonical/… |
결정론적으로 정규화한 관측값. 날짜 파티션 Parquet 시장 데이터셋은 현재 상태다. canonical/tables.py에 선언된 Iceberg 표는 추가 전용 이력이다(현재값은 latest_view, 시점 조회는 as_of_sql) |
데이터셋·파티션마다 writer를 하나로 정한다. raw에서 재생성할 수 있어야 한다. 두 형식의 계약은 서로 바꾸지 않는다. draft Iceberg 표를 승격할 때도 추가 전용 이력 계약을 유지한다 |
feature/… |
비결정적 모델 추론 결과(LLM 태깅 등) | 결정적 집계(예: 구성종목 수급 가중합)는 여기 두지 않는다 |
operations_archive/… |
실행 증거(manifest·실행별 불변 artifact·수집/품질 로그) | 과거 시점 재현과 재처리의 계보다 |
| RDB·Glue/Iceberg 표 | 조회용 복제 | 정본 존에서 파생한다. 복제의 생산자가 레포에 없으면 정본으로 부르지 않는다 |
analysis/backfill/·draft/·애드혹 raw |
임시 존 | 운영 소비를 허용하되 현황 대장에 올라 있어야 하며, 정본으로 부르지 않는다 |
결정적 파생물은 먼저 조회 뷰로 제공한다. 영속이 필요하면 canonical 존의 별도 파생 데이터셋으로 등록한다. 이 경우 자기 writer를 두고, 입력 artifact를 manifest로 참조한다.
2. writer 소유권과 소비 계약¶
- 한 (데이터셋, 파티션)의 소유 writer는 하나다. 한 파티션에 파일이 여럿이어도 된다. 다만 행이 서로소임을
코드가 보장해야 한다(
rollup.writer_owns·is_foreign_5m_key·백필의 티커 서로소 규칙이 그 형태다). 보장이 주석뿐이면 writer가 둘인 것으로 본다. - 백필과 이관은 소유 규칙이 허락한 파티션에 추가 전용 파일로만 쓴다. 다른 writer의 파일을 덮지 않는다. 같은 (키, 날짜)를 이미 누가 가졌으면 이관 원천의 값이 달라도 옮기지 않는다. 그 차이는 데이터 품질 결정으로 따로 올린다.
- 분석 소비자는
CausalLake의 논리 뷰 이름을 계약으로 삼는다. 저장 전환은 뷰 이름과 열 의미를 유지한 채 입력만 바꾼다. 열 의미가 달라지면(예:change_pct정의) 호환 뷰가 옛 의미를 재현한다. 재현하지 못하면 전환하지 않는다.
3. 버킷·경로 정의의 정본¶
- 환경별 버킷은 terraform이 정해 env(
ALPHAMALE_LAKE_BUCKET등)로 주입한다. 신규 코드는 dev 버킷을 하드코딩하지 않는다. 기존 하드코딩은 현황 대장의 부채 목록에 둔다. - 레이크 경로 문자열의 정본은
data_pipeline/lake/storage.py빌더다. Iceberg 표 선언의 정본은canonical/tables.py다. - 분석엔진은 배포 경계상 이를 import하지 않는다. 베낀 경로는 원본과 같다는 것을 테스트가 지킨다.
기존
SECTOR_ROLLUP_VENDOR복제 규약과 같다. 새 범용 레지스트리나 서비스는 만들지 않는다.
4. 최신 조회와 과거 시점 조회¶
- 날짜 파티션 Parquet canonical과 RDB upsert는 현재 상태다(Iceberg canonical 표는 §1대로 이력을 보존하고
as_of_sql로 시점 조회한다). 그 밖의 과거 시점 T의 재현은 실행별 불변 artifact·manifest와available_at(또는 수신 시각) ≤ T 조건으로 한다. 날짜 파티션만으로 시점 재현을 보장했다고 판정하지 않는다. - 이관은 원본의 관측·공개·가용·수신 시각을 보존한다. 원천에 가용 시각이 없어 유도해야 하면
(예:
ts + 5분) 유도 규칙이 정본 writer와 같을 때만 옮기고, 다르면 멈춘다. - 시장 데이터 읽기 폴백은 허용한다. 단 폴백이 일어났다는 사실과 사유를 커버리지 보고(
exists·unbound·backfill_notes)에 남긴다. 조용히 0행이 되는 폴백은 결함이다. 설명 영속 폴백은 ADR-0048대로 폐기 상태다.
5. 임시 존의 승격 조건¶
임시 존의 세트는 다음을 모두 갖춘 데이터셋 단위로만 정본 존으로 승격한다. 폴더 이동으로 승격하지 않는다. 1. 레포 안의 생산자와 갱신 담당. 상시 생산이거나, 일회성이면 그 사실을 명시한다. 2. ADR-0043 Dataset Contract(cadence·기준일·허용 지연·담당). 3. 행 키·심볼·단위·시각 의미를 문서화하고, 기존 소비자가 쓰는 의미와 대조한 결과. 4. Iceberg는 카탈로그·메타데이터 참조를 옮기고, 실제 조회로 새 데이터를 읽는지 확인한 결과.
DataGuide처럼 공급 계약·갱신 담당이 정해지지 않은 원천은 조건 1을 못 채우므로 승격하지 않는다.
6. 소비 전환 검증·롤백, 구 경로 폐기¶
- 검증: 바이트 복사는 체크섬으로 대조한다. 재정제는 논리 키별 값·단위·기간·시각·중복·누락을 대조하고, 같은 입력에서 신구 소비 결과를 비교한다. 경로가 바뀌어 과거 기간이 사라지거나 결측 수가 0이 되면 회귀로 본다.
- 부분 실패: 이관 도구는 원천 객체·목적 객체·해시·상태를 남긴다. 원천 전량이 착지 또는 사유 있는 건너뜀으로 설명되지 않으면 성공이 아니다.
- 롤백: 구 입력은 폐기 전까지 남긴다. 롤백은 reader 설정(뷰 입력)을 되돌리는 것으로 한다. 이관 중 새로 들어온 데이터의 처리 기준점과 재처리 방법은 현황 대장에 기록한다.
- 폐기: 구 경로의 삭제는 소비 전환과 다른 변경이다. 활성 writer 0, 활성 reader 0(코드 참조와 런타임 env 모두), 보존 기간 충족, 담당자 승인을 모두 채운 뒤 별도로 한다.
- 상태는 코드 완료 / 데이터 이관 완료 / 운영 소비 전환 완료 / 구 경로 폐기 넷을 따로 보고한다.
대안¶
- 폴더를 한 번에 표준 경로로 옮긴다. 읽기 경로 수는 줄지만 writer·키·시각 의미가 검증되지 않은 채
"정본" 이름만 얻는다. 이름만 옮겨도 소비 의미는 바뀔 수 있다. 실측한
change_pct정의 차이와 Glue 표에만 있는 이력이 그 예다. 기각한다. - 임시 존 직독을 즉시 금지한다(ADR-0028 문자 그대로). 계약은 선명해진다. 그러나 대체 생산자가 없는
세트(
pit_daily·fin_annual·flow_daily·layers_daily)가 곧바로 0행이 된다. 이는 폴백 없는 조용한 공백을 만든다. 현황 대장의 예외 목록으로 관리하는 쪽을 택한다. - 데이터셋 레지스트리·경로 서비스를 새로 만든다. 이미
storage.py·tables.py·Dataset Contract가 같은 일을 한다. 세 번째 정본을 만들면 드리프트 면이 늘어난다. 기각한다. - ADR-0048을 모든 폴백 금지로 확장한다. 5분봉 Iceberg→canonical 폴백은 신선도 판정(
iceberg_covers)과 함께 있다. 이를 없애면 정본이 멈췄을 때 분석이 과거만 보거나 멈춘다. 이 폴백은 설명 영속 폴백과 실패 양식이 다르다. 기각한다.
결과¶
- 전환은 데이터셋 단위 PR로 진행한다. 각 PR은 생산자·소비자·이관 도구·검증·문서를 함께 갖춘다. 첫 적용 대상과 현재 상태는 현황 대장에 둔다.
- 분석엔진에 남는 임시 존 소비(백필 세트·DataGuide·draft Iceberg)는 예외 목록으로 관리한다. 목록에서 빠지는 조건은 위 승격 조건이다.
- 이관 도구는 기존 스크립트를 확장한다(
migrate_fmp_5min_raw.py·backfill_intraday_5m.py— 후자의 이관 확장은 보류 브랜치에 있다). 새 프레임워크를 만들지 않는 대신, 각 도구가 dry-run·범위 제한·멱등·재개·대장 기록을 스스로 져야 한다. - 구 경로 삭제는 이 ADR로 승인되지 않는다. 폐기 조건을 채운 경로마다 별도 승인이 필요하다.
- 이 ADR은 기존 ADR의 조항을 대체하지 않는다. ADR-0028의 분석 입력 계약을 그대로 두고 예외 관리만 더한다. ADR-0043·0048은 적용 범위를 명시해 재사용한다.