ADR-006 — Ontology Layered Packaging (L1 표준어휘 / L2 공통 / L3 산업팩 / L4 워크스페이스)
| 항목 | 값 |
|---|---|
| Status | Accepted (2026-05-23 — PR 발행 + #993 보완 코멘트 합의) |
| Date | 2026-05-23 |
| Decider | GenD 코어팀 |
| Related Epic | #993 (Ontology Layer — RFP SFR-011 / ECR-020), 부모 #989, prereq #1018 (Workspace Isolation) |
| Related ADR | ADR-001 (DB 모델 디렉토리 분리, #999), ADR-002 (Alembic, #1000) |
본 ADR 의 두 의사결정:
- 의사결정 #1 — Layered Packaging 채택: 4-Layer (L1/L2/L3/L4) 로 온톨로지를 분리 (§결정 본문). 시장 표준 정렬 + 산업별 납품 + 고객 확장 동시 충족.
- 의사결정 #2 — Workspace 격리는 별도 Epic: 본 ADR 흡수 대신 cross-cutting prerequisite Epic #1018 로 분리 (분석 보고서
/tmp/workspace_decision.md— 26+ 모델 영향, 흡수 시 #994/#990/#991 drift 확정). 본 ADR 의workspace_idFK 와 L4 활성화는 #1018 M1 (workspaces테이블 + 4 FK) 완료에 의존.
컨텍스트
한국가스공사 RFP SFR-011 (도메인 온톨로지) / ECR-020 (지식 그래프·하이브리드 RAG) 충족을 위해 #993 에서 LinkML + SHACL + ArangoDB 기반 온톨로지 레이어 도입을 제안했다. 그러나 본문은 가스공사 LNG 단일 도메인을 가정해 설계됐고, 다른 산업 (금융·의료·제조·공공) 납품 시나리오에서 "기업마다 도메인 모델을 재구축할 것인지, 미리 여러 산업 팩을 넣어두고 고를 것인지, 고객 고유 확장은 어디에 둘 것인지" 라는 운영 모델이 미정의 상태였다.
#993 보완 코멘트 (2026-05-23) 에서 4-Layer Packaging 도입이 결정됐고, 본 ADR 은 그 결정의 단일 진실의 원천 (Single Source of Truth) 으로 정착시킨다.
시장 표준 (Palantir Foundry, Collibra IDM, Unity Catalog, Snowflake Industry Solutions) 은 모두 layered packaging 을 채택했다. 단일 모델 (산업 팩 미도입) 도, 워크스페이스 단독 도입도 정답이 아니다.
결정
GenD 온톨로지를 4 개 레이어로 분리해 패키징한다. L1·L2 는 무조건 탑재, L3 는 산업별 선택 설치, L4 는 워크스페이스에서 고객사 admin 이 직접 추가.
레이어 정의
| Layer | 패키징 단위 | 변경 권한 | 활성화 시점 | 버전 관리 |
|---|---|---|---|---|
| L1 | GenD 코드 내장 | GenD 코어팀만 | 무조건 탑재 | GenD release |
| L2 | GenD 코드 내장 | GenD 코어팀만 | 무조건 탑재 | GenD release |
| L3 | Git repo + LinkML YAML (서명) | GenD 산업팀 | Helm gend.ontology.enabledPacks: [...] (canonical) 또는 POST /api/v1/ontology/packs/{name}/install | semver, immutable snapshot |
| L4 | DB row (PG + ArangoDB) | 고객사 admin (require_admin + workspace owner) | 워크스페이스 활성화 시 | ontology_version snapshot |
산업 팩 카탈로그 (L3 후보)
| Pack ID | 기반 표준 | 우선순위 | 비고 |
|---|---|---|---|
gend-onto-lng | ISO 15926 + OSDU + 가스공사 도메인 | M2 (가스공사 RFP) | 본 Epic 첫 검증 |
gend-onto-finance | FIBO (EDM Council) | P3 | 글로벌 은행 90% |
gend-onto-healthcare | HL7 FHIR R5 + SNOMED CT | P3 | 의료 international |
gend-onto-manufacturing | ISA-95 / ISO 62264 + MIMOSA | P3 | 제조 OT |
gend-onto-public | DCAT-AP + 공공데이터포털 메타데이터 | P3 | KR 공공 |
gend-onto-power | IEC 61850 + CIM (IEC 61970) | 보류 | 전력 산업 |
각 팩은 별도 Git repo (genonai/gend-onto-<industry>) 로 분리, semver + signed release. 설치는 Helm value (gend.ontology.enabledPacks — canonical key) 또는 런타임 admin API (POST /api/v1/ontology/packs/{name}/install).
REST API 형태: install/uninstall 모두 install 서브리소스로 일관:
POST /api/v1/ontology/packs/{name}/install— 설치 (생성)DELETE /api/v1/ontology/packs/{name}/install— 해제 (설치 상태 토글, 팩 메타데이터는 유지)DELETE /api/v1/ontology/packs/{name}— 팩 메타데이터 자체 삭제 (cascade 영향 평가 후, admin only)
데이터 모델
ADR-001 디렉토리 패턴에 따라 apps/api/src/gend_api/db/models/ontology.py 신규 모듈에 정의. Class, Property, Relation, OntologyClassGrant 공통 컬럼:
layer: Mapped[str] # 'L1' | 'L2' | 'L3' | 'L4'
pack_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("ontology_packs.id"), nullable=True, index=True,
)
workspace_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("workspaces.id"), nullable=True, index=True,
)
# 제약 이름은 **테이블별로 prefix 를 분리** — Class/Property/Relation/
# ClassGrant 4 개 테이블이 같은 패턴을 공유하므로 PostgreSQL 의
# constraint 이름 충돌을 막기 위해 ``ck_ontology_{table}_layer_scope`` /
# ``uq_ontology_{table}_full_uri`` 형식으로 명명한다.
__table_args__ = (
CheckConstraint(
"(layer = 'L3' AND pack_id IS NOT NULL AND workspace_id IS NULL) OR "
"(layer = 'L4' AND workspace_id IS NOT NULL AND pack_id IS NULL) OR "
"(layer IN ('L1', 'L2') AND pack_id IS NULL AND workspace_id IS NULL)",
name="ck_ontology_classes_layer_scope", # 테이블별 prefix
),
UniqueConstraint("layer", "pack_id", "workspace_id", "name",
name="uq_ontology_classes_full_uri"), # 테이블별 prefix
)
PG UNIQUE NULL 갭 (의도된 한계, partial unique index 로 보완): PostgreSQL UNIQUE 는 NULL 을 서로 다른 값으로 취급하므로 위 uq_*_full_uri 만으로는 L1/L2 (pack_id/workspace_id 모두 NULL) 의 동명 Class 중복을 막지 못한다. 4 테이블 모두 다음 partial unique index 4종을 추가해 갭을 닫는다 (M1 Step 2 마이그레이션):
-- L1 동명 중복 차단
CREATE UNIQUE INDEX uq_ontology_classes_l1_name
ON ontology_classes (name) WHERE layer = 'L1';
-- L2 동명 중복 차단
CREATE UNIQUE INDEX uq_ontology_classes_l2_name
ON ontology_classes (name) WHERE layer = 'L2';
-- L3 동명 중복 차단 (pack 내)
CREATE UNIQUE INDEX uq_ontology_classes_l3_name
ON ontology_classes (pack_id, name) WHERE layer = 'L3';
-- L4 동명 중복 차단 (workspace 내)
CREATE UNIQUE INDEX uq_ontology_classes_l4_name
ON ontology_classes (workspace_id, name) WHERE layer = 'L4';
신규 테이블:
ontology_packs(id, slug, name, version, source_url, installed_at, status, signature)—slug는 URI 식별자 (e.g.gend-onto-lng),id는 FK 용 UUIDworkspaces(id, slug, name, keycloak_group, created_at)—slug는 URI 식별자 (e.g.samsung-card),id는 FK 용 UUID. 별도 Epic #1018 산출 (의사결정 #2)
slug 와 UUID FK 의 관계: pack_id / workspace_id 는 안정적 식별을 위해 UUID FK (불변); URI 의 {pack_or_ws} 자리에는 사람이 읽을 수 있는 slug (변경 가능하지만 unique) 가 들어간다. URI 해석 시 slug → id 단방향 lookup. slug 변경 시 historic URI 는 redirect 테이블로 보존 (M3 GA).
URI 풀네이밍
모든 Class/Property/Relation 은 풀 URI 로 식별된다. {pack_or_ws} 자리에는 slug (사람이 읽는 식별자) 가 들어가며, DB 의 pack_id/workspace_id UUID FK 와는 분리된 안정적 키.
gend://{layer}/{slug}/{ClassName}
# L1 표준어휘 — pack_id/workspace_id 모두 NULL.
# slug 자리에 reserved 고정값 'core' 사용 (별도 테이블 없음, 코드 상수).
gend://L1/core/Time
gend://L1/core/Organization
# L2 플랫폼 빌트인 — pack_id/workspace_id 모두 NULL.
# slug 자리에 reserved 고정값 'platform' 사용.
gend://L2/platform/Dataset
gend://L2/platform/Table
# L3 산업 팩 — slug = ontology_packs.slug
gend://L3/gend-onto-lng/Equipment
gend://L3/gend-onto-finance/Customer
# L4 워크스페이스 — slug = workspaces.slug
gend://L4/samsung-card/SamsungCardCustomer
gend://L4/gend-genon/InternalDashboard
L1/core/, L2/platform/ 는 reserved slug 이므로 ontology_packs.slug / workspaces.slug 에서 core / platform 이름은 사용 금지 (M2 마이그레이션에서 회귀 가드).
UI 표시 라벨은 [L3:finance] Customer / [L4:samsung-card] SamsungCardCustomer 형식으로 레이어를 명시 (#993 보완 §7 L3 ↔ L4 shadow 혼란 완화).
L4 ↔ L3 shadow 규칙
동일 name (예: Customer) 이 L3 산업 팩과 L4 워크스페이스에 동시 정의되면:
- L4 가 L3 를 shadow — 워크스페이스 내 조회 시 L4 정의가 우선.
- L3 정의는 부모로서 참조 가능 (
extends: gend://L3/gend-onto-finance/Customer@0.3.0) — version pin obligatory. - L4 가 L3 부모 version pin 없이
extends하면 SHACL 검증 실패 (deploy 게이트). - L3 팩 업그레이드 시 SHACL diff 자동 산출 → L4 확장 영향 평가 리포트 → breaking change 면 admin 승인 게이트.
pack_id / workspace_id 가 다르면 별 객체 (위 UniqueConstraint 의 4-tuple 키).
ABAC 우선순위 매트릭스
기존 project_context_security 의 fail-closed 패턴 + project_abac_enforcement 의 column_mask → row_filter 순서 패턴 위에, 레이어 차원 우선순위를 중첩한다:
1. L4 workspace grant deny ← 최우선 (고객사 정책)
2. L3 pack grant deny ← 산업 표준 정책
3. L2 built-in grant deny ← 플랫폼 정책
4. L1 standard vocab ← (정책 부착 안 함, 참조만)
5. 물리 DataGrant fall-through ← 기존 ABAC (resource_groups + DataGrant)
allow only if ALL applicable layers allow (fail-closed)
deny if ANY applicable layer denies
caller 컨텍스트에 workspace_id + groups (Keycloak group) 양쪽 주입. 평가 순서는 위→아래, 첫 deny 에서 즉시 중단.
L4 Shadow 와 ABAC 상호작용: L4 가 L3 Class 를 shadow 한 경우 — 즉 동일 name 의 L4 정의가 L3 정의를 덮어쓴 경우 — ABAC 평가는 L4 정의 기준으로 수행한다 (L3 grant 가 있어도 shadow 된 L3 정의는 caller 입장에서 존재하지 않음). 단 L4 가 L3 를 extends 한 경우 (부모-자식 관계, version pin obligatory) 는 양쪽 grant 모두 적용 (자식이 부모를 상속).
| 시나리오 | ABAC 평가 |
|---|---|
| L4 가 L3 Class 를 shadow (동명) | L4 grant 만 |
L4 가 L3 Class 를 extends (부모 참조) | L4 grant ∩ L3 grant (자식이 부모 권한 상속) |
| 동일 caller 가 L3/L4 모두에 멤버 | L4 우선 (위 #1 규칙) |
회귀 가드 (필수)
tests/api/test_ontology_layer_precedence.py— L4/L3/L2 동시 정책 부착 후 caller clearance 별 결과 단언tests/api/test_ontology_uri_naming.py— URI 충돌 시 layer 차원에서 격리 단언tests/api/test_ontology_shadow.py— L4 가 L3 extends version pin 없이 shadow 시 SHACL 실패tests/api/test_arango_collections.py(기존 가드 확장) —onto_*prefix 강제 + workspace 활성화 시ws_{workspace_id}_onto_*prefix 단언tests/api/test_ontology_pack_signature.py— 미서명 팩 install 거부
거부된 대안
(a) 산업 팩 미도입 단일 모델
- 거부 사유: GenD 를 다른 산업에 납품할 때마다 도메인 모델을 코드 분기 (
if industry == 'finance') 로 처리하게 됨 → 빌드/릴리스 분기 폭증, 산업별 SME 협업 비용 폭증. - 시장 검증: Palantir / Collibra / Snowflake 모두 산업 팩 분리. 단일 모델로 vertical SaaS 운영하는 사례 없음.
(b) 워크스페이스만 도입, 산업 팩 없음
- 거부 사유: 모든 고객사가 0 부터 모델링해야 함 → 도입 비용 폭증, 산업 표준 (FIBO/HL7) 활용 불가, 모델 품질 편차.
(c) 코드 분기로 multi-tenant
- 거부 사유: 빌드/릴리스 분기 폭증, CI/CD 표면적 N 배, 보안 회귀 위험 (한 tenant 코드 변경이 다른 tenant 영향).
(d) URI 에 layer 없이 pack_id / workspace_id 만 사용
- 거부 사유: URI 가독성 손실, 정책 평가 시 매번 join 필요. Layer 가 URI 1차 시민이어야 함 (Polymorphic identity 와 동일 사유).
영향
DB
- 신규 테이블 5건:
ontology_classes,ontology_properties,ontology_relations,ontology_class_grants,ontology_packs - 신규 외부 테이블 1건:
workspaces(의사결정 #2 결과 의존) - 모든 ontology 모델 공통 컬럼 3건 + CHECK 제약 1건
라우터
POST /api/v1/ontology/packs/{name}/install(admin) — L3 installDELETE /api/v1/ontology/packs/{name}/install(admin) — L3 설치 해제 (toggle, 메타데이터 유지)DELETE /api/v1/ontology/packs/{name}(admin) — 팩 메타데이터 자체 삭제 (cascade 영향 평가 후)GET /api/v1/ontology/classes—?layer=L4&workspace_id=...필터 obligatory (caller 컨텍스트로 자동)- 기존
_protected_routers패턴 준수
ABAC
services/ontology/abac_binding.py— 위 5단 우선순위 평가auth/dependencies.py의 caller context 에workspace_id추가 (의사결정 #2 의존)
RAG (#994)
- Hybrid RAG 의 Class 카드 retrieval 시 caller 의 active workspace + 활성 L3 팩 스코프 자동 필터
- L4 shadow 가 있으면 L3 결과 대체
마이그레이션
- Alembic (ADR-002) 도입 후 진행
- 기존 ArangoDB 18 collection 영향 없음 (schema 8 + glossary 2 + lineage 3 + document graph 5; PR #1016 의
_scan_repo_for_collection_names결과 기준) — 신규onto_*/ws_{id}_onto_*prefix 만 추가
메트릭
gend_ontology_pack_active_count{pack_id}(gauge)gend_ontology_workspace_count(gauge)gend_ontology_shadow_count{l4_class}(gauge) — L4 가 L3 를 shadow 한 건수gend_ontology_layer_grant_eval_total{layer,result}(counter)
마일스톤 매핑 (#993)
| 단계 | Packaging 범위 |
|---|---|
| M1 PoC (4w) | layer 컬럼만 도입, 모두 L2 로 시드 (single-tenant). L3/L4/pack/workspace 미도입 |
| M2 Beta (6w) | L3 도입 — gend-onto-lng 첫 산업 팩 분리, Helm enabledPacks. L4 도입은 workspace prereq 결과 의존 (의사결정 #2). ID 충돌 규칙 + shadow UX |
| M3 GA (6w) | L3 마켓플레이스 (2-3 산업 PoC), L3↔L4 마이그레이션 SHACL diff, 서명 검증, L4 워크스페이스 격리 GA |
관련
- Epic: #993 (본 ADR 소유), 부모 #989
- 보완 코멘트: https://github.com/genonai/DataX/issues/993#issuecomment-4524953429
- ADR: ADR-001 (#999), ADR-002 (#1000), ADR-003 (#1001), ADR-004 (#1002)
- 메모리:
project_context_security,project_abac_enforcement,project_data_isolation,feedback_arango_fallback_mock - 외부: Palantir Foundry Ontology (Object/Link/Action), Collibra Industry Data Models, Unity Catalog, FIBO, HL7 FHIR R5, ISO 15926, OSDU, DCAT-AP