ADR-0044: 학습 데이터셋의 물리 배치 — 공유 스키마에서 워크스페이스 스키마로
- 상태: 제안 (Proposed)
- 날짜: 2026-08-08
- 연관: #3212(생성이 admin 전용) · #3071(바인딩 관리자 API) · #3074(쓰기 게이트 확대) · ADR-0027(버전 스냅샷 고정) · #2798 PR-14(워크스페이스 프로비저닝)
배경
학습 데이터셋 테이블은 모든 워크스페이스가 공유하는 iceberg.datasets 에 생성된다
(routers/training_dataset.py 의 trino_catalog="iceberg", trino_schema="datasets" 하드코딩).
반면 다른 워크스페이스 자산은 전부 자기 물리 스키마를 쓴다:
| 자산 | 물리 위치 |
|---|---|
| 데이터마트 | iceberg.ws_<slug> (사용자가 지정, 게이트가 검증) |
| 파이프라인 산출물 | iceberg.ws_<slug> / hive.ws_<slug>_staging |
| 학습 데이터셋 | iceberg.datasets (공유) |
워크스페이스를 만들면 프로비저너(#2798 PR-14)가 물리 스키마 생성·레지스트리 등록· write 바인딩·그룹 DataGrant 를 자기 스키마에 대해 자동으로 해준다. 데이터셋만 그 대상 밖이라 다음이 따라온다.
이 배치가 만든 결과 (실측)
- 바인딩을 수동으로 발급해야 한다. 공유 네임스페이스라 프로비저닝 대상이 아니고, 그래서 #2798 PR-15 로 write 게이트를 enforce 한 순간 전 워크스페이스의 데이터셋 생성이 조용히 403 이 됐다(당시 데이터셋 0건이라 observe 분포에도 안 잡힘).
- 격리가 API 계층에만 의존한다. 물리적으로는 한 스키마에 섞여 있고, 분리는 PG
training_datasets.workspace_id+scope_query가 전담한다. 이 계층에서 이번 분기에 구멍이 둘 나왔다 — tenantless 키(#3040), 게이트 미적용 쓰기 경로(#3074). - Trino ACL 에
datasets규칙이 하나도 없다. ACL 은 DataGrant 에서 생성되는데 (trino_rules_generator), 공유 스키마에는 그룹 grant 를 줄 주체가 없다. prod 실측:schemas/tables규칙 37건 중datasets언급 0건.
결정
데이터셋 테이블을 워크스페이스 자기 스키마(iceberg.ws_<slug>)에 생성한다.
-
좌표는 하드코딩이 아니라
Workspace.physical_schema에서 온다.★ NULL 이면 fail-closed 로 차단한다. 이 컬럼은 nullable 이고 (
db/models/workspace.py:110), 레거시·미프로비저닝 워크스페이스에서 NULL 일 수 있다. 지금 prod 활성 5개가 모두 채워져 있는 것은 소급 프로비저닝(#2798 PR-14)의 결과이지 불변식이 아니다. NULL 일 때slug로ws_<slug>를 재계산하거나datasets로 폴백하면 격리가 깨진다 — 프로비저닝을 먼저 요구하고, 그 전 데이터셋 생성은 명시적으로 거부한다(리뷰 반영). -
새 워크스페이스는 프로비저닝만으로 데이터셋을 쓸 수 있다. 바인딩·그룹 grant· ACL 규칙이 이미 그 스키마에 대해 발급되기 때문이다.
-
격리가 물리 분리로 승격된다. API 계층 스코프는 그대로 두되, 그 계층이 뚫려도 다른 워크스페이스의 스키마가 보이지 않는다.
무엇을 고치고 무엇을 못 고치는가 (2026-08-08 실측으로 정정)
DDL(CREATE)과 DML(INSERT)이 서로 다르게 갈린다. 초안은 이 둘을 뭉뚱그려
"이 ADR 은 #3212 를 고치지 않는다"고 적었는데, INSERT 에 대해서는 틀렸다.
| 동작 | 지금(iceberg.datasets) | 이관 후(iceberg.ws_<slug>) |
|---|---|---|
CREATE TABLE | ❌ 테이블 OWNERSHIP 필요 — 테넌트 그룹은 owner: false | ❌ 동일 (OWNERSHIP 은 자기 스키마에도 없다) |
INSERT(적재) | ❌ 규칙 없음 → analyst 기본 read-only | ✅ 허용 — 테넌트 그룹이 자기 스키마에 INSERT 보유 |
prod ACL 실측:
{"group": "/tenants/finance-invest", "privileges": ["SELECT","INSERT","DELETE","UPDATE"],
"catalog": "iceberg", "schema": "ws_finance_invest"} ← 자기 스키마엔 INSERT 있음
datasets 스키마 관련 규칙: 0건 ← 공유 스키마엔 아무것도 없음
따라서 이 ADR 은 격리 개선일 뿐 아니라 적재가 동작하기 위한 기능 요건이다.
CREATE 만 서비스 신원 대리 실행으로 남고(#3212 수정), 적재는 사용자 신원 그대로
Trino ACL 의 판정을 받는다 — 즉 이관 후에는 엔진 계층이 데이터셋 쓰기의 절반(적재)
을 다시 보게 된다.
★ 이 사실은 ACL 활성화(2026-08-08, #3117) 이후에야 드러났다. 그 전에는 엔진이 판정하지 않아 analyst 적재가 통과했고, 활성화와 함께 적재가 조용히 막혔다.
결합점 (이 값을 가정한 곳)
| 위치 | 무엇 | 조치 |
|---|---|---|
routers/training_dataset.py | trino_schema="datasets" 하드코딩 + check_write_target("iceberg","datasets") | 워크스페이스 physical_schema 로 |
db/models/dataset.py | 컬럼 기본값 "datasets" | 기본값 제거(명시 필수) |
config.grant_enforced_schemas | "iceberg.datasets" — DataGrant 시행 대상 스키마 목록 | ws 스키마들을 포괄하는 형태로. 누락 시 데이터셋이 시행 범위 밖으로 나간다 |
pipelines/iceberg_maintenance | ADR-0027 가드가 iceberg.datasets.* 를 스냅샷 만료에서 제외 | 판정 기준을 스키마명이 아니라 테이블 접두어(ds_) 또는 카탈로그 레지스트리로. 누락 시 버전이 고정한 스냅샷이 만료돼 과거 버전 조회가 영구 파손 |
services/access/marketplace_policy.py · read_policy.py | iceberg.datasets 를 데이터셋 판별에 사용 | 같은 판정 기준으로 통일 |
★ 위 5개 중 grant_enforced_schemas 와 유지보수 가드는 누락 시 조용히 나빠진다
(권한 시행 이탈 / 데이터 영구 손실). 나머지는 실패가 즉시 드러난다.
이관
기존 데이터셋 3건(전부 finance-invest)을 옮긴다. Iceberg 테이블 이동은
ALTER TABLE ... RENAME TO 로 카탈로그 내 스키마 간 이동이 가능한지 먼저 실측하고,
불가하면 CTAS + 스냅샷 재고정 절차를 쓴다.
★ 버전이 고정한 스냅샷 ID 는 테이블에 종속이다(ADR-0027). 이관 방식이 스냅샷을 보존하지 못하면 기존 버전 조회가 깨진다 — 이관 설계에서 가장 먼저 확정할 지점이며, 필요하면 버전 행의 snapshot_id 재매핑까지 포함한다.
단계
각 단계가 그 자체로 정합하게 쪼갠다 (중간 상태가 "우회책이 영구화된 모습"이 되지 않게).
-
선행(별건): 데이터셋 DDL 을
ddl_identity()로 — #3212 해소. 관례 정합이라 이 ADR 과 독립적으로 옳다. -
좌표를
physical_schema에서 읽도록 + 신규 데이터셋만 ws 스키마에 생성. 기존 행은 자기trino_schema값을 그대로 쓰므로 양쪽이 공존한다. -
결합점 5개를 "스키마명" 대신 데이터셋 판별식 기준으로 전환.
★ 공존 기간(2~4단계)의 판별 규칙을 하나로 정의한다 (리뷰 반영). 이 기간엔 기존
iceberg.datasets와 신규iceberg.<physical_schema>가 동시에 존재하는데,grant_enforced_schemas는catalog.schemaCSV 정확 비교라 한쪽만 넣으면 다른 쪽의 DataGrant 시행이 통째로 누락된다. 판별식은 레지스트리(training_datasets행의 좌표) 또는ds_접두어 중 하나로 단일화하고 전 경로에 같은 것을 쓴다.physical_schema가 NULL 이거나 레지스트리에 없으면 fail-closed.회귀 테스트로 고정할 것: 기존 공유 데이터셋 · 신규 ws 데이터셋 · 바인딩 없는 ws · NULL/미등록
physical_schema. -
기존 3건 이관 + 버전 스냅샷 정합 확인.
-
datasets네임스페이스 정리(바인딩 회수·레지스트리 처리).
★ 공존 기간에는 양쪽 모두 유지보수 제외 대상으로 유지한다 (리뷰 반영). 기존
iceberg.datasets.* 와 신규 physical_schema 의 데이터셋 테이블 둘 다 스냅샷 만료에서
빼야 한다. 레지스트리 누락으로 한쪽이 보호에서 빠지면 그 스냅샷이 만료되고,
training_dataset_versions.iceberg_snapshot_id 를 쓰는 과거 버전 조회가 영구 실패한다.
5단계(기존 보호 조건·datasets 네임스페이스 제거) 완료 조건:
전 버전 행에 대해 (a) 대상 테이블 실재 (b) 그 테이블에 해당 snapshot_id 실재 —
두 가지를 전수 검증하고, CTAS 로 이관한 경우 snapshot 재매핑까지 끝난 뒤에만 제거한다.
이관해도 남는 것 — 명시적으로 수용한다
HF·업계 비교 조사(2026-08-08)에서 이번에 겪은 문제들을 필연적 대가와 배치 선택의 결과로 갈랐다. 이 ADR 이 고치는 것은 후자뿐이며, 전자는 "데이터셋 생성이 DDL 이다"라는 선택에 붙어 오는 것이라 이관해도 남는다.
| 남는 것 | 근거 |
|---|---|
| 사용자 신원으로는 CREATE TABLE 이 영원히 불가 | Trino FileBasedAccessControl 은 테이블 OWNERSHIP 을 요구하는데 테넌트 그룹 규칙은 owner: false + _map_privilege("ALL") 에 OWNERSHIP 없음. ws 스키마로 옮겨도 동일 |
↳ 엔진 계층이 CREATE 는 못 본다 | 위 때문에 DDL 은 서비스 대리 실행(#3212 수정)이라 Trino ACL 의 판별력이 0 이다. 단 적재(INSERT)는 이관 후 사용자 신원으로 판정된다 — 쓰기 전체가 아니라 생성만 API 계층 단독 책임이다 |
즉 이 ADR 이후에도 데이터셋 생성의 인가는 API 계층이 단독 책임이다(적재는 엔진이 함께 본다). 그 계층의 구멍은 이번 분기에 둘 나왔고(#3040 tenantless 키, #3074 미적용 경로) 둘 다 고쳤지만, 구조적으로 "엔진이 뒤를 받쳐 주지 않는다"는 사실은 유지된다. 이를 바꾸려면 테넌트 그룹에 스키마 OWNERSHIP 을 주는 별도 결정이 필요하며 그건 이 ADR 의 범위 밖이다.
참고 — HuggingFace 와의 대조
같은 문제를 파일 중심으로 푼 사례. 저장 3계층은 동일하고, 사본을 카탈로그에 등록하는지가 갈린다.
| 축 | GenD | HF Hub |
|---|---|---|
| 원본 | S3 datasets/<id>/{blobs,raw}/ | git repo + 포인터(LFS→Xet), 실물 S3 |
| 쿼리용 사본 | Iceberg warehouse/datasets.db/…/data/*.parquet | refs/convert/parquet 브랜치(원본 불가침) |
| 사본 등록 | 한다 (HMS + Trino 카탈로그) | 안 한다 — 경로 규약만 |
| DDL 발행 | 데이터셋마다 CREATE SCHEMA+CREATE TABLE | 없음 — 새 디렉터리일 뿐 |
| 버전 고정 | Iceberg 스냅샷 ID | git commit SHA |
| 행 필터·컬럼 마스크 | 있음 | 없음 |
파일 중심 전면 재설계는 채택하지 않는다. SQL 조회·행 단위 PII 삭제·거버넌스 판정이 GenD 의 값어치이고, 그것이 등록 계층의 존재 이유다. 다만 위 대조는 "등록하면 권한이 결합된다"는 대가가 구조적임을 보여 준다 — 그래서 이 ADR 은 결합을 없애는 대신 프로비저닝이 이미 발급하는 권한 위로 옮겨 결합을 자동으로 충족시킨다.
★ 업계 관행도 이 방향이다: Databricks Unity Catalog 는 비부모 객체(테이블)당 grant 를
1,000개로 제한해 사실상 "테이블마다 개별 권한"을 금지하고 스키마/카탈로그 상속을 강제한다.
datasets 공유 스키마는 그 상속이 성립하지 않는 배치다.
대안
- A. 현행 유지 + 바인딩 수동 발급 — 지금 상태. 격리가 API 계층에만 의존하고, 새 워크스페이스마다 수동 조작이 남는다.
- B. 프로비저너가
datasets바인딩을 자동 발급 — 마찰은 줄지만 공유 자원에 default-open 이 되고, 프로비저너의 소유권 확정 분기(owner_workspace_id is None)가 공유 네임스페이스에 임의 소유자를 박는다. 무엇보다 ACL 규칙이 없어 "발급됐는데 안 되는" 상태를 양산한다. - C(채택). ws 스키마 이관 — 비용이 가장 크지만 위 문제들이 원인 수준에서 사라진다.
확인이 필요한 것
- Iceberg 스키마 간
RENAME의 스냅샷 보존 — 업스트림 문서상ALTER TABLE a.t RENAME TO b.t2는 카탈로그가 지원하면 스키마 간 이동이 가능하고 메타데이터 포인터만 바꾸므로 스냅샷 이력이 보존된다(리뷰 조사). 다만 우리 환경(Hive Metastore + Trino iceberg 커넥터)에서 실제로 되는지는 미확인 — 4단계 착수 전 스테이징에서 실측하고, 안 되면 CTAS + snapshot 재매핑으로 전환한다.
해소된 질문 (2026-08-08 실측)
테넌트 그룹이 자기 스키마에서 CREATE TABLE 을 하려면 OWNERSHIP 이 필요한가→ 필요하다. 그리고 현재 없다._map_privilege("ALL")은 SELECT/INSERT/DELETE/UPDATE 만 주고, 스키마 규칙은owner: false다. prod 통제 실측(그룹 동기화 대기 후): finance-invest analyst(바인딩 有) 502 · crm-demo analyst(바인딩 발급 후) 502 · admin 201. 따라서 사용자 신원 DDL 은 이 ADR 범위에서 되살리지 않는다 — 서비스 대리 실행을 유지하고, 위 "이관해도 남는 것"으로 수용한다.