온톨로지 개념 마무리
- 나의 목적
- Layer 0 — 세 층위 분리
- Layer 1 — 온톨로지란 무엇인가
- 표로 정리
- Layer 2 — RDF
- Layer 3 — RDFS / OWL (온톨로지를 RDF로 쓰기)
- Layer 4 — LPG (Labeled Property Graph)
- Layer 5 — Neo4j는 물리적으로 어떻게 도는가
- Layer 6 — Neptune
- Layer 7 — Graph RAG
- 골든셋 134문항 전수 분석 — 데이터 모델 선택의 근거
- 설계 원칙 — 개념 층 (형식 중립)
- 이 프로젝트에서의 논증 구조
- 다음 액션
- 자주 틀리는 것 정리
나의 목적
회사 스택은 Neo4j와 Neptune인 듯으로 보인다. Neptune은 RDF도 되고 LPG도 된다.
즉 “Neptune을 쓴다”고 해서 데이터 모델이 정해지는 게 아니다. Neptune 안에서 RDF/SPARQL로 갈 것인가, Property Graph/openCypher로 갈 것인가에 대한 결정을 근거 있게 하려면 RDF를 이해해야 한다.
Layer 0 — 세 층위 분리
| 층위 | 정체 | 예시 |
|---|---|---|
| ① 개념 모델 | “세상을 어떻게 나눌 것인가”의 명세. 코드가 아님 | 온톨로지 |
| ② 데이터 모델 | 그 명세를 기계가 저장할 수 있는 자료구조 형식 | RDF, LPG |
| ③ 저장 엔진 | 그 자료구조를 디스크에 실제로 굽는 소프트웨어 | Neo4j, Neptune, GraphDB |
Layer 1 — 온톨로지란 무엇인가
정의
온톨로지 = 어떤 도메인에 존재하는 개념(class), 개념 간 관계(property), 그리고 그것들이 지켜야 할 논리적 규칙(axiom)의 명시적 명세.
Gruber의 고전적 정의: “an explicit specification of a conceptualization”
Conceptualization (개념화)
: 우리가 머릿속으로 세상을 바라보는 방식이나 틀.
예) '인간' 아래에 '이름', '나이', '직업'이 있고,
인간은 '회사'에 '고용된다'는 관계를 머릿속으로 생각하는 것 자체.
Explicit Specification (명시적 명세)
: 머릿속에만 있는 모호한 생각을 컴퓨터가 알아먹을 수 있게
명확하고 꼼꼼하게 적어두었다는 뜻.
shared — 이게 실무에서 가장 중요하다
Gruber 정의에는 shared(공유된) 도 들어 있다. 실무에서 이 일을 하게된다면 이게 매우 중요하다고 생각한다.
shared = 여러 사람/시스템이 같은 개념을 같은 이름으로 부른다는 합의.
21개 파일에는 이런 표현들이 흩어져 있다:
"계약자" / "보험계약자" / "가입자" / "계약자님" / "policyholder"
이게 전부 하나의 개념이라는 걸 합의하고, 하나의 이름에 묶는 것이 온톨로지에서 매우 중요한 점 이다.
왜냐?:
- 그냥 LLM에게 그래프를 뽑게 하면 → 파일마다 다른 노드가 생긴다
- “계약자” 노드와 “가입자” 노드가 따로 생기면 → 그래프가 파편화되는 오류가 생긴다.
→ 이것이 나중에 “LLM에게 스키마를 강제로 주는” 이유. 스키마 = shared 합의의 코드화.
왜 필요한가 — 보험 도메인 예시
파일들에는 이런 내용이 흩어져 있다:
파일 A: "계약자는 보험료 납입 의무를 진다"
파일 B: "피보험자 사망 시 수익자에게 보험금 지급"
파일 C: "계약자와 피보험자가 다른 경우 피보험자의 서면동의 필요"
Vector RAG는 이걸 텍스트 덩어리의 유사도로만 다룬다. “계약자가 보험금을 받나요?”라고 물으면 ‘계약자’와 ‘보험금’이 같이 나오는 청크를 긁어온다. 그런데 계약자 ≠ 수익자라는 사실은 청크 안 어디에도 명시적으로 없다.
인간은 문서 3개를 읽고 머릿속에서 구조를 만들지만, 임베딩은 그 역할을 하지 못한다.
온톨로지는 그 구조를 문서 밖으로 꺼내서 명시적으로 쓴 것이다:
Class: 사람, 보험계약, 보험금, 보험료
Property: (보험계약) --계약자--> (사람)
(보험계약) --피보험자--> (사람)
(보험계약) --수익자--> (사람)
Axiom: 계약자 ≠ 수익자 (반드시 같지는 않음)
피보험자 ≠ 계약자 → 서면동의 필요
파일이 몇 개든, 문장이 어떻게 쓰였든, 도메인의 뼈대는 하나다. 설명하는 방식(문장, 언어, 파일 형식)은 수백 가지로 달라져도, 그 뒤에 숨은 진짜 규칙은 하나다.
온톨로지의 3요소
| 요소 | 뜻 | 보험 예 |
|---|---|---|
| Class (개념) | 사물의 종류 | 보험계약, 사람, 특약 |
| Property (관계/속성) | 사물 간 연결 또는 사물의 값 | 계약자(관계), 납입보험료(값) |
| Axiom (공리) | 논리적 규칙 | 종신보험 ⊑ 생명보험 (모든 종신보험은 생명보험이다) |
Axiom 종류들
종신보험 ⊑ 생명보험은 axiom 중에서도 딱 한 종류(subsumption, 상하위 관계)이고 실제로는 훨씬 다양하다.
- Subsumption (분류 관계)
종신보험 ⊑ 생명보험 (모든 종신보험은 생명보험의 일종이다)
class 계약구성요소(BaseModel):
구분: Literal["주계약", "특약", "부가서비스"]
- Disjointness (배타성) — “이 둘은 절대 같을 수 없다”
개인계약 ⊓ 단체계약 = ∅ (개인계약이면서 동시에 단체계약일 수 없다)
class 당사자(BaseModel):
구분: Literal["개인", "법인"] = "개인"
Literal["개인", "법인"] 자체가 배타성 공리. “한 당사자는 개인이거나 법인이거나 둘 중 하나이지, 둘 다일 수 없다”는 규칙이 타입 시스템으로 강제되어 있다.
- Cardinality (개수 제약) — “몇 개까지/몇 개 이상 있어야 하는가”
보험계약 은 정확히 1명의 계약자를 가져야 한다
보험계약 은 0명 이상의 수익자를 가질 수 있다
업무메뉴얼(Q26/60/80/110, 친권자 규칙)이 정확히 이 형태:
계약자가 미성년이고 다른 친권자 동의 있음 → 친권자 1명 필요
계약자가 미성년이고 동의 없음 → 친권자 2명 필요
“몇 명이 필요한가”라는 cardinality 공리이다. 지금 스키마에는 요구서류 관계 정도로만 표현되어 있고, 인원수 자체를 속성화하진 않았음.
- Property Restriction (속성 제약) — “이 관계가 성립하려면 이런 조건이 필요하다”
계약자 ≠ 수익자 → 타인을위한해지프로세스 적용
스키마에서 허용상태 관계가 이것:
{"name": "허용상태", "domain": "규정조건", "range": "계약상태", "properties": ["허용여부"]}
규정조건 --허용상태(허용여부:true/false)--> 계약상태
“이 규정조건이 이 계약상태에서 허용되는가”라는 조건부 공리를 관계+속성으로 표현한 것이다.
- Inverse (역관계) — “A가 B와 관계면, B도 A와 역방향 관계다”
피보험자(A, B) ⟺ 피보험대상계약(B, A)
“A가 B의 피보험자다”와 “B는 A를 피보험자로 하는 계약이다”는 자동으로 서로를 함의합니다.
LPG는 관계에 방향이 있지만 자동 역방향 생성이 없어서, 필요하면 쿼리에서 MATCH (a)-[:계약자이다]-(b) 처럼 방향 없이 매칭하거나, 역방향을 별도로 만들어야 한다.
- Transitivity (이행성) — “A→B, B→C면 A→C”
상위조직(A,B) ∧ 상위조직(B,C) → 상위조직(A,C)
CQ13(파급효과)이 이 성질을 요구하는 질문:**
특약삭제 → 바이탈리티 자동해지 → 보험료할인 종료
하지만 이건 OWL의 자동 이행성 추론이 아니라, 파급효과 관계를 명시적으로 하나씩 그래프에 심는 방식으로 처리하고 있다
(schema.py의 {"name": "파급효과", "domain": "업무유형", "range": "계약구성요소", "properties": ["효과유형"]}).
즉 “A→B→C를 자동으로 A→C 추론”하는 게 아니라, A→C를 사람/LLM이 직접 명시하는 쪽으로 설계되어 있습니다.
여기서 이 이행성. RDF는 자동으로 하고 LPG에선 Cypher의 가변길이 패턴(*0..)을 쓰면 도달 간으한 노드를 찾아주기때문에 가능하다. 그러나 한계점은 있다.
표로 정리
| Axiom 종류 | 뜻 | RDF/OWL 표현 |
|---|---|---|
| Subsumption | 상하위 계층 | rdfs:subClassOf |
| Disjointness | 배타적 분류 | owl:disjointWith |
| Cardinality | 개수 제약 | owl:minCardinality 등 |
| Property Restriction | 조건부 관계 | owl:someValuesFrom 등 |
| Inverse | 역관계 자동생성 | owl:inverseOf |
| Transitivity | 연쇄 추론 | owl:TransitiveProperty |
핵심은 이겁니다: OWL은 이 6가지를 선언만 하면 추론기가 자동으로 적용해줍니다. 당신의 LPG 스키마는 필요한 것만 골라서(주로 ②④), 나머지는 사람이 명시적으로 하나씩 채워 넣는 방식으로 대체하고 있습니다. 이게 지금까지 계속 나온 결론 — “OWL 추론의 자동성이 우리 도메인엔 필요 없었고, LPG로 충분했다”는 걸 axiom 종류별로 다시 확인한 셈입니다.
TBox / ABox
Class vs Instance 구분이 결정적이다:
보험계약= Class (개념, 틀)계약번호 2024-XXXX= Instance (실제 개별 사례)
온톨로지는 주로 Class 층을 다루고, 실제 데이터(Instance)는 그 틀(T-box)에 채워 넣는다.
| 층 | 이름 | 내용 |
|---|---|---|
| Class 층 | TBox (Terminological Box) | 개념 정의, 계층, 규칙 |
| Instance 층 | ABox (Assertional Box) | 실제 데이터 |
Layer 2 — RDF
RDF의 자료구조 트리플: RDF는 세상의 모든 지식을 3개짜리 문장으로 표현한다.
(Subject, Predicate, Object)
주어 서술어 목적어
:계약_2024_001 :계약자 :홍길동 .
:계약_2024_001 :피보험자 :김철수 .
:계약_2024_001 :보험료 "50000"^^xsd:integer .
이 트리플들이 모이면 자동으로 그래프가 된다. Subject와 Object가 노드, Predicate가 엣지이기 때문이다.
[계약_2024_001] --계약자--> [홍길동]
|
+--피보험자--> [김철수]
|
+--보험료--> ("50000")
RDF에서 “그래프”는 설계한 게 아니라, 트리플을 던져 넣으니 저절로 생긴 것이다.
turtle
:계약_001 :계약자 :홍길동 .
이 한 줄을 쓰면 → [계약_001]과 [홍길동]이라는 노드가 저절로 생기고,
계약자라는 엣지가 저절로 생긴다.
노드를 직접 만든 적이 없다. 트리플을 썼더니 노드가 생긴다.
LPG는 다르다. 노드를 직접 만든다.
cypher
CREATE (h:사람 {이름:"홍길동"}) // ← 노드를 직접 생성. 관계 없이도 존재
CREATE (c:보험계약 {번호:"2024-001"})
CREATE (c)-[:계약자]->(h) // ← 엣지를 직접 생성
| RDF | LPG | |
|---|---|---|
| 그래프의 원자 | 트리플(문장) — 그래프는 부산물 | Node와 Relationship — 처음부터 그래프 |
| 노드 생성 | 불가. 트리플을 쓰면 S/O가 노드가 됨 | CREATE (n) — 직접 생성 |
| 고립 노드 | 존재 불가. 트리플의 일부여야만 노드 | 존재 가능 |
| 속성 저장 | 별도 트리플 (:홍길동 :이름 "홍길동") |
노드 내부 ({이름:"홍길동"}) |
→ RDF는 “문장의 집합”이고, LPG는 “노드와 엣지의 집합”이다.
단, “스키마를 먼저 정의하지 않는다”는 건 둘의 차이가 아니다. LPG도 스키마리스다. 이건 그래프 DB 공통 특성이고, RDBMS와 대비되는 지점이다.
스키마리스 = “저장 엔진이 데이터를 받기 전에 구조를 강제하지 않는다”
URI
실제 RDF에서 모든 것은 URI로 식별된다.
@prefix aia: <http://aia.co.kr/ontology#> .
aia:계약_2024_001 aia:계약자 aia:홍길동 .
aia:계약자는 실제로 http://aia.co.kr/ontology#계약자의 축약형이다.
왜? 전 세계 어디서든 이 개념을 하나로 특정하기 위해서다. 회사 A의 “계약자”와 회사 B의 “계약자”가 같은 개념인지, 문자열만으로는 알 수 없다. URI가 다르면 다른 개념, 같으면 같은 개념이다.
이것이 RDF가 태어난 목적(Semantic Web = 웹 전체를 하나의 DB로)의 직접적 결과다.
주의:
http://로 시작하지만 웹주소가 아니다. 브라우저에 쳐도 안 열려도 된다. 전역 고유 이름표일 뿐이다.
Literal vs Resource
Object 자리에는 두 가지가 올 수 있다:
aia:계약_001 aia:계약자 aia:홍길동 . # Resource — 다른 노드로 연결됨
aia:계약_001 aia:보험료 "50000"^^xsd:int . # Literal — 값. 여기서 끝. 더 못 나감
Literal은 그래프의 잎(leaf)이다. Literal을 Subject 자리에 놓을 수 없다.
"50000" :단위 "원" . # ← RDF에서 불법
값에 추가 정보를 붙이고 싶으면, 그 값을 URI를 가진 독립 노드로 승격시켜야 한다.
# 불법
aia:계약_001 aia:보험료 "50000" .
"50000" aia:통화 "KRW" . # ← 안 됨
# 승격
aia:계약_001 aia:보험료 aia:보험료_001 .
aia:보험료_001
aia:금액 "50000"^^xsd:int ;
aia:통화 "KRW" .
이 제약 때문에 RDF는 모델링이 엄격해지고 데이터양이 늘어난다. 대신 모든 것이 S-P-O로 완벽하게 통일되기 때문에, 컴퓨터가 논리를 자동 추론하기에는 유리하다.
Turtle 문법 — 이것만 알면 된다
@prefix aia: <http://aia.co.kr/ontology#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
# 마침표 = 트리플 끝
aia:계약_001 rdf:type aia:보험계약 .
# 세미콜론 = 같은 Subject 반복
aia:계약_001
rdf:type aia:보험계약 ;
aia:계약자 aia:홍길동 ;
aia:피보험자 aia:김철수 ;
aia:보험료 "50000"^^xsd:integer .
# 쉼표 = 같은 Subject + 같은 Predicate 반복
aia:계약_001 aia:특약 aia:암특약, aia:재해특약 .
# 'a' 는 rdf:type 의 축약형 (자주 나옴)
aia:계약_001 a aia:보험계약 .
; , . 세 개만 구분하면 Turtle은 읽을 수 있다.
Layer 3 — RDFS / OWL (온톨로지를 RDF로 쓰기)
RDF는 “트리플을 쓸 수 있다”까지만 알려준다. 어떤 트리플을 써야 온톨로지가 되는가를 정한 것이 RDFS와 OWL이다.
RDFS — 최소한의 어휘
# 클래스 선언
aia:보험계약 a rdfs:Class .
aia:종신보험 a rdfs:Class .
# 클래스 계층 (상속)
aia:종신보험 rdfs:subClassOf aia:생명보험 .
aia:생명보험 rdfs:subClassOf aia:보험계약 .
# 프로퍼티 선언 + domain/range
aia:계약자 a rdf:Property ;
rdfs:domain aia:보험계약 ; # 이 관계의 출발점
rdfs:range aia:사람 . # 이 관계의 도착점
혼동될 수 있는 함정 — domain/range는 제약이 아니다
RDBMS 감각으로 보면 rdfs:range aia:사람은 “사람이 아니면 에러”로 읽힌다. 틀렸다.
RDF/OWL은 Open World Assumption (OWA) 위에서 돌아간다.
# 온톨로지 선언
aia:계약자 rdfs:range aia:사람 .
# 데이터 입력
aia:계약_001 aia:계약자 aia:법인_ABC .
# 결과: 에러 아님. 추론기가 이렇게 결론냄:
aia:법인_ABC a aia:사람 . # ← 자동 추론됨!
“위반”이 아니라 “그럼 법인ABC도 사람인가 보네”라고 결론낸다.
| Closed World (SQL) | Open World (RDF/OWL) | |
|---|---|---|
| 없는 데이터 | 거짓(false) | 모름(unknown) |
| 스키마 위반 | 에러 | 추론 재료 |
rdfs:range |
제약 | 추론 규칙 |
SHACL — OWL과 정반대 목적
그럼 진짜 검증(validation)은 어떻게 하나? → SHACL
중요: OWL과 SHACL은 대립하는 게 아니라 목적이 정반대다.
| OWL | SHACL | |
|---|---|---|
| 목적 | 이 데이터로부터 무엇을 추론할 수 있는가 | 이 데이터가 규격을 만족하는가 |
| 세계관 | Open World (모르면 unknown) | Closed World (없으면 위반) |
| 결과 | 새 트리플 생성 | 위반 리포트 |
| 비유 | 지식 확장 | 품질 검증 |
같은 선언이 정반대로 동작한다:
# 선언
aia:계약자 rdfs:range aia:사람 .
# 데이터
aia:계약_001 aia:계약자 aia:법인_ABC .
| 결과 | |
|---|---|
| OWL 추론기 | aia:법인_ABC a aia:사람 . ← 추론해서 새로 만듦 |
| SHACL 검증기 | 위반: 법인_ABC는 사람이 아님 ← 에러 리포트 |
→ 그래서 실무 RDF는 OWL과 SHACL을 둘 다 쓴다. OWL로 지식을 확장하고, SHACL로 품질을 지킨다. 하나가 다른 하나를 대체하지 않는다.
보험 데이터에서 OWA는 위험하다. 오타나 터무니없는 값이 들어와도 에러가 안 나기 때문이다. RDF를 쓰려면 SHACL은 선택이 아니라 필수다.
OWL — 논리로 더 세게
# 두 개념은 서로 배타적
aia:개인계약 owl:disjointWith aia:단체계약 .
# 역관계 — 하나 넣으면 반대 방향 자동 생성
aia:피보험자 owl:inverseOf aia:보험대상계약 .
# 이행성 — A>B, B>C 면 A>C
aia:포함하는특약 a owl:TransitiveProperty .
# 동일 개체 선언 (다른 시스템의 같은 사람)
aia:홍길동 owl:sameAs legacy:CUST_00123 .
추론(Reasoning)의 실체
추론기(Pellet, HermiT, RDFox)를 돌리면 명시적으로 안 쓴 트리플이 새로 생성된다.
입력: 계약_001 a 종신보험 .
종신보험 rdfs:subClassOf 생명보험 .
생명보험 rdfs:subClassOf 보험계약 .
추론 결과 (자동 생성):
계약_001 a 생명보험 . ← 없던 게 생김
계약_001 a 보험계약 . ← 없던 게 생김
추론기가 계층을 타고 올라가면서 스스로 결론을 만든다.
실무적 의미:
| 추론 없는 DB | 추론 있는 RDF | |
|---|---|---|
| 질의 | “생명보험 그리고 종신보험 그리고 정기보험… 다 찾아줘” | “생명보험 찾아줘” |
| 신상품 추가 시 | 쿼리 소스코드 수정 필요 | 계층만 선언하면 자동 분류 |
이것이 RDF의 대표적 강점이다.
그런데 — 추론은 공짜가 아니다
추론기가 트리플을 “생성한다”고 했는데, 물리적으로 어떻게 생성되는가?
| 방식 | 동작 | 대가 |
|---|---|---|
| Materialization (forward chaining) |
데이터 적재 시 추론 결과를 물리적으로 저장 | 쿼리는 빠름 저장 공간 폭증 데이터 변경 시 재계산 |
| Query Rewriting (backward chaining) |
쿼리 시점에 동적으로 확장 | 저장 공간 절약 쿼리가 느려짐 |
Materialization 예시:
원본 트리플 1개: 계약_001 a 종신보험 .
계층 3단계 → 추론 결과 2개가 디스크에 추가 저장:
계약_001 a 생명보험 . ← 물리적으로 저장됨
계약_001 a 보험계약 . ← 물리적으로 저장됨
계약이 100만 건이면, 추론 트리플만 200만 개가 추가로 디스크를 먹는다. 그리고 계약 하나가 상태를 바꾸면 → 관련 추론을 다시 계산해야 한다.
→ 보험처럼 데이터가 계속 변하는 도메인에서 materialization 비용은 무시할 수 없다.
Named Graph / Quad — RDF는 실은 쿼드다
RDF는 트리플이라고 배웠지만, 실제 트리플스토어는 쿼드(Quad)로 저장한다.
(Subject, Predicate, Object, Graph)
↑ 4번째 칸
왜 필요한가: “이 트리플이 어느 문서에서 나왔는가”를 표현하려면 4번째 칸이 필요하다.
# 21개 파일 중 어느 파일에서 나온 사실인가?
GRAPH <http://aia.co.kr/doc/계약변경_03> {
aia:규칙_감액_001 aia:전제조건 aia:실효 ;
aia:판정 aia:불허 .
}
GRAPH <http://aia.co.kr/doc/부활_01> {
aia:규칙_부활_001 aia:전제조건 aia:실효 .
}
같은 aia:실효를 참조하지만, 출처가 다른 문서임이 그래프 이름으로 구분된다.
SPARQL에서 출처를 함께 조회:
SELECT ?규칙 ?출처문서 WHERE {
GRAPH ?출처문서 {
?규칙 aia:전제조건 aia:실효 .
}
}
이것이 RDF의 두 번째 강점이다. provenance(출처)를 데이터 모델 차원에서 지원한다.
LPG는? Named Graph에 해당하는 게 없다. 속성으로 직접 붙여야 한다:
(:변경규칙)-[:근거문서]->(:문서 {file:"계약변경_03.pdf", 조항:"제4조 2항"})
참고: Neptune은 내부적으로 quad 모델을 쓴다. RDF 모드에서 Named Graph를 네이티브 지원한다.
SPARQL 최소 세트
PREFIX aia: <http://aia.co.kr/ontology#>
SELECT ?계약 ?계약자이름
WHERE {
# '생명보험'에 속하는 계약들
?계약 a aia:생명보험 ;
aia:계약자 ?사람 . # 그 계약의 계약자
?사람 aia:이름 ?계약자이름 . # 그 사람의 이름
FILTER(?계약자이름 != "홍길동")
}
LIMIT 10
읽는 법: WHERE {} 안은 트리플 패턴이다. ?로 시작하면 변수.
SQL의 JOIN이 아니라 패턴 매칭이다. “이 모양의 트리플이 그래프에 존재하면, 거기 맞는 값을 뽑아라.”
property path — 추론 없이 계층 확장:
# subClassOf를 0번 이상 타고 올라가라
?계약 a/rdfs:subClassOf* aia:생명보험 .
이게 나중에 중요해진다. Neptune은 추론기가 없지만 이건 지원한다.
Layer 4 — LPG (Labeled Property Graph)
Neo4j가 쓰는 모델이자, Neptune의 Property Graph 모드가 쓰는 모델이다. RDF와 근본적으로 다르다.
구조
Node:
- 여러 개의 Label 가질 수 있음: :보험계약, :생명보험, :종신보험
- 내부에 Property(key-value)를 직접 품음: {계약번호:"2024-001", 보험료:50000}
Relationship:
- 정확히 하나의 Type: :계약자
- 방향 있음
- ★ Relationship 자신도 Property 가질 수 있음: {체결일:"2024-01-01"}
RDF와의 근본적 차이 두 가지:
- 노드가 속성을 직접 품는다. RDF는 별도 트리플로 표현한다.
- 엣지가 속성을 가질 수 있다. ← 이게 결정적이다.
엣지 속성 — RDF 1.1은 못 하는 것
“홍길동이 계약_001을 2024년 1월 1일에 체결했다”
RDF 1.1로 쓰려면:
# 이건 불가능 — 트리플은 3칸뿐이다
aia:홍길동 aia:체결 aia:계약_001 [체결일 "2024-01-01"] . # ← 이런 문법 없음
# 관계를 노드로 승격시켜야 함 (Reification / n-ary relation)
aia:체결사건_001 a aia:체결사건 ;
aia:주체 aia:홍길동 ;
aia:대상 aia:계약_001 ;
aia:체결일 "2024-01-01" .
트리플 1개가 4개로 늘어났다.
LPG는:
(홍길동)-[:체결 {체결일: "2024-01-01"}]->(계약_001)
한 줄이다.
그런데 — RDF-star가 이걸 해결했다 (중요)
“RDF는 엣지에 속성 못 단다”는 2014년 기준 사실이다. 지금은 아니다.
RDF-star(RDF 1.2)는 트리플을 다른 트리플의 주어로 쓸 수 있게 한다:
<< aia:홍길동 aia:체결 aia:계약_001 >> aia:체결일 "2024-01-01" .
한 줄. reification 불필요. SPARQL-star로 쿼리도 된다.
- W3C에서 RDF 1.2 / SPARQL 1.2로 표준화 진행 중
- 이미 구현됨: GraphDB, Stardog, Jena, Oxigraph, Blazegraph, AWS Neptune(1.3.x+)
그렇다면 왜 여전히 LPG인가 — 진짜 논거는 여기다
RDF-star를 쓰는 순간, RDF를 고를 이유가 사라진다.
RDF의 핵심 강점은 표준 OWL 추론이다. 그런데:
OWL 추론기는 RDF-star의 quoted triple을 추론 대상으로 다루지 못한다. 추론기들(HermiT, Pellet)은 RDF 1.1 시맨틱 위에서 동작하고, RDF-star에 대한 추론 시맨틱은 아직 표준화되지 않았다.
| 엣지 속성 | OWL 추론 | |
|---|---|---|
| RDF 1.1 (고전) | X (reification 필요) | O |
| RDF-star | O | X (추론 시맨틱 미정의) |
| LPG | O | X |
→ 엣지 속성이 필요한 순간, RDF와 LPG의 차이는 문법뿐이다. → RDF-star를 쓰면 = 추론 없는 RDF = LPG를 문법만 어렵게 쓰는 것.
RDF vs LPG 비교표
| RDF | LPG (Neo4j / Neptune PG모드) | |
|---|---|---|
| 원자 단위 | 트리플 (S,P,O) | Node + Relationship |
| 엣지 속성 | RDF 1.1: (reification) RDF-star: |
네이티브 |
| 노드 속성 | 별도 트리플로 표현 | 노드 내부에 저장 |
| 식별자 | 전역 URI | 내부 ID (로컬) |
| 스키마 | RDFS/OWL (형식논리 기반) | 제약 기반 스키마 있음NODE KEY, EXISTS, UNIQUE, PROPERTY TYPE(5.9+)형식논리 스키마는 없음 |
| 추론 | 표준 추론기 존재 | 없음. 단 *0.. 가변길이 패턴으로 계층 흉내 가능 |
| 검증 | SHACL | 제약조건 (unique/exists/node key) |
| 쿼리 | SPARQL | Cypher / openCypher / Gremlin |
| 논리적 가정 | OWA (형식 의미론 존재) | 해당 없음 ← 아래 참조 |
| 출처 추적 | Named Graph (네이티브) | 속성으로 직접 부착 |
| 데이터 통합 | 강함 (URI + owl:sameAs) |
약함 |
| 순회 성능 | 상대적으로 느림 | 빠름 (Neo4j의 IFA) |
| 표준화 | W3C 표준 | Cypher → GQL(ISO) 표준화 진행 |
“LPG = Closed World”는 틀렸다
많은 자료가 이렇게 쓰는데, 개념적으로 잘못된 서술이다.
OWA/CWA는 형식 논리 시스템의 의미론(semantics) 개념이다. “없는 사실을 거짓으로 볼 것인가, 모른다고 볼 것인가”에 대한 논리적 입장이다.
Neo4j는 논리 시스템이 아니라 저장소다. 애초에 그런 입장을 취하지 않는다. Cypher가
NOT EXISTS를 CWA처럼 다루는 건 쿼리 언어의 동작이지, 데이터 모델의 논리적 가정이 아니다.
“LPG가 왜 Closed World야?”: -> “논리 시스템이 아니라서 그 질문 자체가 성립하지 않는다”가 정답.
RDF의 강점은 셋이다 (하나가 아님)
“RDF의 유일한 강점은 추론”이라고 쓴 자료가 많은데, 틀렸다.
| # | 강점 | 내용 |
|---|---|---|
| ① | 표준 추론 (OWL) | subClassOf 계층을 추론기가 자동 확장 |
| ② | 전역 URI → 데이터 통합 | 다른 조직/시스템 데이터를 URI로 병합 (owl:sameAs) |
| ③ | Named Graph → 출처 추적 | 트리플이 어느 문서에서 왔는지 네이티브 지원 |
| ④ | W3C 표준 | 벤더 락인 없음 |
셋 다 진짜 강점이다. 그런데 우리 도메인에서는 셋 다 발현되지 않는다:
| 강점 | 우리 프로젝트에서 |
|---|---|
| ① 표준 추론 | 골든셋 134문항 중 4개(3%)만 요구. 그마저도 문서가 개별 상품을 이미 열거해놔서 추론 불필요 |
| ② URI 데이터 통합 | 요구 0%. AIA 사내 문서 21개. 외부 온톨로지 병합 계획 없음 |
| ③ Named Graph 출처 | 필요하지만 과잉. 규칙 노드에 -[:근거문서]-> 하나 붙이면 충분.Named Graph는 “트리플 집합 단위” 출처 관리용이고, 우리는 “규칙 단위”로 관리 |
반면 LPG의 강점은 골든셋에서 직접 요구된다:
- 엣지 속성 → 골든셋 31% (41문항)
- 다중홉 순회(IFA) → 골든셋 7% (9문항)
→ RDF의 세 강점 모두 미발현, LPG의 강점은 직접 요구. 그래서 LPG.
Neo4j에서 계층 추론은 어떻게 하나
Neo4j에는 OWL 추론기가 없다. 종신보험 subClassOf 생명보험을 넣어도, Neo4j는 그걸 아무 의미 없는 그냥 엣지로 본다.
쿼리에 직접 써야 한다:
MATCH (c)-[:IS_A*0..]->(:Class {name: "생명보험"})
RETURN c
*0.. = “0단계 이상, 아무 depth나 타고 올라가라”
추론을 쿼리로 흉내내는 것이다. 다만 OWL처럼 disjointWith, inverseOf, TransitiveProperty를 자동 처리해주지는 않는다.
neosemantics (n10s) — RDF를 Neo4j로 임포트하고 제한적 추론을 제공하는 공식 플러그인. ! 단, Neptune에서는 못 쓴다. Neptune은 커스텀 프로시저를 지원하지 않는다.
Layer 5 — Neo4j는 물리적으로 어떻게 도는가
! 먼저 오해 제거 — IFA는 “디스크 IO를 없애는 것”이 아니다
혼동하면 안 되는 두 층위:
| 무엇을 제거하나 | 어떻게 | |
|---|---|---|
| Index-Free Adjacency | 인덱스 조회(B-tree 탐색) | 고정 크기 레코드 → 오프셋 직접 계산 |
| 페이지 캐시 | 디스크 IO | 파일을 메모리(off-heap)에 올려둠 |
IFA로 포인터를 따라가도, 그 레코드는 여전히 읽어야 한다. IFA가 제거하는 건 “어디 있는지 찾는 비용”이지 “읽는 비용”이 아니다.
디스크 IO를 줄이는 건 페이지 캐시의 역할이고, 그건 RDBMS도 똑같이 한다.
핵심 개념: Index-Free Adjacency (IFA)
RDBMS에서 그래프 순회가 느린 이유:
-- "홍길동의 친구의 친구"
SELECT ... FROM person p1
JOIN friend f1 ON p1.id = f1.from_id -- B-tree 탐색: O(log N)
JOIN person p2 ON f1.to_id = p2.id -- 또 O(log N)
JOIN friend f2 ON p2.id = f2.from_id -- 또 O(log N)
JOIN person p3 ON f2.to_id = p3.id -- 또 O(log N)
JOIN 할 때마다 인덱스를 뒤진다. depth가 깊어질수록 비용이 곱해진다. 데이터가 커지면(N이 커지면) 매 hop이 다 느려진다.
Neo4j의 방식:
노드가 자기 이웃의 물리적 주소(디스크 offset)를 직접 들고 있다.
Node 레코드:
[inUse][nextRelId][nextPropId][labels][extra]
↑
이 노드에 붙은 첫 번째 관계의 ID
Relationship 레코드:
[inUse][firstNode][secondNode][relType]
[firstPrevRelId][firstNextRelId] ← firstNode 입장에서의 이전/다음 관계
[secondPrevRelId][secondNextRelId] ← secondNode 입장에서의 이전/다음 관계
[nextPropId][firstInChain]
관계 레코드들이 이중 연결 리스트를 이룬다. 노드에서 출발해 관계 체인을 따라가면, 인덱스 탐색 없이 포인터만 따라가면 된다.
진짜 핵심 — 레코드가 고정 크기다
ID를 알면 디스크 offset을 곱셈 한 번으로 계산한다:
offset = record_id × 레코드크기
탐색이 없다. O(1) 접근이다.
숫자를 방어하지 말고 원리를 방어하라
노드 15바이트 / 관계 34바이트가 흔히 인용되는데, 이건 Neo4j 3.x record format 기준이다. 4.x 이후 aligned record format에서는 값이 달라진다.“전통적으로 인용되는 값은 3.x 기준 노드 15바이트, 관계 34바이트입니다. 4.x 이후 포맷에서는 달라집니다.
중요한 건 정확한 바이트 수가 아니라 ‘레코드가 고정 크기’라는 성질입니다. 고정 크기여야
ID × 레코드크기로 오프셋을 곱셈 한 번에 계산할 수 있고, 그게 O(1) 접근의 근거입니다.가변 길이였다면 어디부터 읽어야 할지 몰라서 인덱스가 필요해집니다.”
성능 특성
| RDBMS JOIN | Neo4j IFA | |
|---|---|---|
| 1 hop 비용 | O(log N) — N = 전체 데이터 크기 | O(1) — 전체 크기와 무관 |
| k hop 비용 | O(k · log N) | O(k · d) — d = 평균 차수 |
| 데이터 증가 시 | 느려짐 | 로컬 순회는 안 느려짐 |
“Neo4j는 로컬 순회에서 전체 데이터 크기에 무관한 성능을 낸다” — 이게 IFA의 정확한 주장이다.
정직하게 짚어야 할 한계 (여기까지 말해야 신뢰가 생김)
-
첫 노드를 찾을 때는 인덱스를 쓴다.
MATCH (p:Person {name:"홍길동"})— 이건 B-tree/Lucene 인덱스 탐색이다. IFA는 그 다음부터다. -
Supernode 문제. 관계가 수백만 개 붙은 노드(예:
:보험상품노드에 모든 계약이 연결)는 연결 리스트가 너무 길어서 순회가 느려진다. Neo4j는 관계를 type/방향별로 그룹핑(relationship group)해서 완화하지만, 근본 해결은 아니다. → 엔진이 아니라 모델링으로 피해야 한다. -
집계/스캔은 RDBMS가 낫다. “전체 계약 평균 보험료” 같은 건 그래프 DB의 강점이 아니다.
-
IFA는 Neo4j 고유가 아니다. 다만 Neo4j가 이 용어를 대중화했다.
저장 파일 구조
Neo4j는 역할별로 파일을 분리해서 저장한다.
neostore.nodestore.db ← 노드 레코드 (고정 크기)
neostore.relationshipstore.db ← 관계 레코드 (고정 크기)
neostore.propertystore.db ← 프로퍼티 (작고 정형화된 값)
neostore.propertystore.db.strings ← 긴 문자열 (프로퍼티에 안 들어갈 때)
neostore.labelscanstore.db ← 라벨 인덱스 (":고객 라벨 노드 어디 있나?")
+ 페이지 캐시 (JVM 힙 밖, off-heap) 에 이 파일들을 올려놓고 쓴다.
그래프 전체가 RAM에 들어가면 성능이 극대화된다. Neo4j 사이징의 제1원칙이다.
Layer 6 — Neptune
6-1. Neptune Database vs Neptune Analytics — 다른 제품
가장 먼저 구분해야 한다. 이름이 비슷해서 헷갈리는데, 완전히 다른 서비스다.
| Neptune Database | Neptune Analytics | |
|---|---|---|
| 목적 | 트랜잭션 그래프 DB (OLTP) | 분석 엔진 (OLAP) |
| 저장 | 영구 저장, 자동 확장 | 메모리 최적화, 분석용 로드 |
| 데이터 모델 | RDF 또는 Property Graph | Property Graph만 (RDF는 n-triples로 로드는 가능) |
| 쿼리 언어 | SPARQL / Gremlin / openCypher | openCypher만 |
| 벡터 검색 | 없음 | 네이티브 지원 |
| 그래프 알고리즘 | 제한적 | 내장 (path finding, community detection, centrality, similarity) |
| 제약조건 | 없음 | 없음 |
| Graph RAG | 벡터 스토어 별도 필요 (OpenSearch, pgvector) |
그래프 + 벡터를 한 스토어에서 |
Neptune Analytics는 벡터를 그래프 데이터와 함께 통합 저장하는 메모리 최적화 엔진이다. 그래프 순회와 벡터 유사도 검색을 같은 쿼리 안에서 실행할 수 있다.
벡터를 노드 속성으로 저장하고, topKByNode, topKByEmbedding 같은 함수로 openCypher 쿼리 안에서 그래프 패턴 매칭과 벡터 유사도를 결합한다. 코사인/유클리드 거리를 지원한다.
→ Graph RAG를 한다면 AWS가 미는 답은 Neptune Analytics다.
Amazon Bedrock Knowledge Bases의 관리형 GraphRAG 기능이 Neptune을 백엔드로 쓴다. 그래프와 임베딩 생성/유지를 자동으로 관리해준다.
“Neptune Database인가요, Neptune Analytics인지 확인” Graph RAG면 Analytics일 가능성이 높다. 벡터 검색이 여기 있다.
6-2. Neptune에서 RDF vs LPG
Neptune Database는 RDF도 되고 Property Graph도 된다. 그런데:
두 모델의 데이터는 서로 격리된다. RDF(SPARQL)로 넣은 데이터를 openCypher/Gremlin으로 읽을 수 없다. 반대도 마찬가지다.
즉, “Neptune을 쓴다”는 결정은 데이터 모델을 정하지 않는다. 그 안에서 RDF/SPARQL로 갈지, PG/openCypher로 갈지를 별도로 정해야 한다.
그리고 이건 나중에 바꾸려면 전부 다시 넣어야 한다.
| Neptune 모드 | 쿼리 | 데이터 접근 |
|---|---|---|
| RDF 모드 | SPARQL | Gremlin/openCypher로 못 읽음 |
| Property Graph 모드 | Gremlin 또는 openCypher | SPARQL로 못 읽음 |
참고: Gremlin과 openCypher는 같은 property graph 데이터를 공유한다. 서로 호환된다. 격리되는 건 RDF ↔ Property Graph 사이다.
→ 이것이 RDF를 이해해야 하는 진짜 이유다. 안 쓰기로 결정하더라도, 그 결정을 근거 있게 하려면 뭘 포기하는지 알아야 한다.
“Neptune을 RDF로 쓰는지, Property Graph로 쓰는지 확인”
6-3. Neptune의 openCypher ≠ Neo4j의 Cypher
“Neo4j로 개발하고 Neptune에 배포”가 자연스러워 보이지만, 함정이 있다.
Neptune은 openCypher 표준 명세 version 9를 지원한다. 그런데 Neo4j에서 돌아가는 많은 애플리케이션은 오픈소스가 아닌 독자 기능을 쓰고, Neptune은 그것들을 지원하지 않는다. 예를 들어 APOC 프로시저, 일부 Cypher 전용 절과 함수, Char/Date/Duration 데이터 타입을 지원하지 않는다.
여러 면에서 호환되지만, Neptune은 Neo4j의 drop-in replacement가 아니다.
구체적인 비호환 목록:
| 항목 | Neo4j | Neptune |
|---|---|---|
| APOC 프로시저 | O 수백 개 | X 커스텀 프로시저 미지원 |
| GDS (Graph Data Science) | O 라이브러리 | X 직접 대체품 없음 |
shortestPath() / allShortestPaths() |
O | X 미지원 |
LOAD CSV |
O | X AWS Glue 등 ETL 서비스 사용 권장 |
MANDATORY MATCH, CALL[YIELD...] |
O | X 미지원 |
| Date / Duration 타입 | O | X 미지원 (자동 캐스팅됨) |
| 노드/관계 ID | 정수 | 문자열 — id() 함수가 문자열 반환 |
| geospatial | O | X 네이티브 미지원 |
| 제약조건 (constraint) | O NODE KEY, UNIQUE, EXISTS |
X 없음 |
| 시각화 | Neo4j Browser, Bloom | Jupyter/SageMaker 노트북 |
이 프로젝트에서 치명적인 것 3개
① Date/Duration 미지원 → 시점 계산이 깨진다
앞서 골든셋 분석에서 시점 의존 문항이 12개(9%) 였다. 예:
// Neo4j에서는 됨
WITH duration.inDays(s.시작일, date()).days AS 실효경과일
WHERE 실효경과일 <= 30
Neptune Database에서는 이게 안 된다. DateTime 객체에 대한 덧셈/뺄셈 같은 연산을 현재 지원하지 않는다. Neptune Analytics에서는 지원되며, Neptune Database 1.3.2.0 이상에서는 labmode 파라미터(DatetimeMillisecond=enabled)로 확장 datetime 기능을 켤 수 있다.
→ 애플리케이션 레이어에서 날짜 계산을 해야 한다. 또는 Neptune Analytics를 써야 한다.
② 제약조건 없음 → SHACL 대체 수단이 사라진다
앞서 “RDF의 SHACL 역할을 Neo4j의 CREATE CONSTRAINT로 대체한다”고 했다.
Neptune에는 그 제약조건이 없다.
// Neo4j에서는 됨
CREATE CONSTRAINT 계약_고유 FOR (c:보험계약) REQUIRE c.계약번호 IS UNIQUE;
// Neptune: 이런 게 없음
→ 데이터 품질 검증을 전부 애플리케이션 레이어에서 해야 한다.
③ APOC/GDS 없음 → 그래프 알고리즘을 못 쓴다
MS GraphRAG 방식(커뮤니티 탐지)을 하려면 Leiden/Louvain 알고리즘이 필요한데, Neo4j는 GDS로 제공하지만 Neptune Database에는 없다.
→ Neptune Analytics에는 있다. path finding, community detection(clustering), centrality, similarity 알고리즘을 지원한다.
6-4. Neptune의 물리 구조는 Neo4j와 다르다 (IFA가 아니다)
“Neo4j가 빠른 이유는 IFA”를 Neptune에 그대로 적용하면 틀린다.
Neo4j는 all-in-one 아키텍처다. 데이터 로딩, ETL, 애플리케이션 쿼리, 데이터 저장, 관리 작업이 전부 같은 컴퓨트 리소스에서 일어난다.
Neptune은 OLTP 중심의 오픈 스펙 그래프 DB이며, 아키텍처가 연산과 리소스를 분리해서 동적으로 확장할 수 있게 되어 있다.
| Neo4j | Neptune | |
|---|---|---|
| 아키텍처 | 단일 노드 (all-in-one) | 컴퓨트/스토리지 분리 (Aurora 유사) |
| 순회 최적화 | Index-Free Adjacency | 분산 스토리지 기반. IFA 아님 |
| 저장 모델 | 고정 크기 레코드 + 연결 리스트 | 네이티브 quad 모델 |
| 확장 | 클러스터 구성 필요 | 스토리지 128 TiB까지 자동 확장, 읽기 복제본 15개 |
| 쓰기 | 리더 노드 | 단일 writer (병목 가능) |
| 읽기 일관성 | Causal consistency | 읽기 복제본은 eventual consistency |
! 중요한 결과:
Neptune의 읽기 복제본은 eventual consistency다. 실시간 파이프라인에서 엄격한 읽기 일관성이 필요하면 복잡해진다.
Neo4j의 causal consistency는 read-your-own-writes를 보장한다. 방금 쓴 걸 바로 읽을 수 있다. Neptune 읽기 복제본은 그 보장이 없다.
→ “Neo4j가 IFA로 O(1) 순회한다”는 설명은 Neo4j에만 유효하다. Neptune에 적용하면 안 된다.
6-5. Neo4j vs Neptune — 종합 비교
| Neo4j | Neptune Database | Neptune Analytics | |
|---|---|---|---|
| 데이터 모델 | LPG 전용 | RDF 또는 PG | PG 중심 |
| 쿼리 | Cypher | SPARQL / Gremlin / openCypher | openCypher |
| 운영 | 셀프호스팅 (AuraDB는 관리형) | 완전 관리형 (AWS) | 완전 관리형 |
| 벡터 | O 5.x 벡터 인덱스 내장 | X | O 네이티브 |
| 그래프 알고리즘 | O GDS | X | O 내장 |
| 제약조건 | O | X | X |
| APOC | O | X | X |
| OWL 추론 | X | X | X |
| 계층 확장 | IS_A*0.. |
subClassOf* (SPARQL)IS_A*0.. (openCypher) |
IS_A*0.. |
| 일관성 | Causal (read-your-writes) | 복제본은 eventual | — |
| 비용 | 라이선스 / 셀프호스팅 | 인스턴스 시간당 | 인스턴스 시간당 |
| 생태계 | APOC, GDS, Bloom, Browser | AWS (IAM, CloudWatch, SageMaker, Bedrock) | AWS + Bedrock KB |
6-6. 추론에 대한 결정적 사실
Neptune은 RDFS/OWL 추론(entailment regime)을 지원하지 않는다.
단, SPARQL 1.1의 property path는 지원한다:
# 계층 확장 — 추론기 없이
?계약 a/rdfs:subClassOf* aia:생명보험 .
subClassOf* = “subClassOf를 0번 이상 타고 올라가라”
→ Neo4j의 IS_A*0..와 정확히 같은 방식이다.
여기서 나오는 통찰:
RDF 진영에서도 실제로는 추론기를 안 쓰는 경우가 많다.
AWS의 관리형 RDF 스토어인 Neptune조차 OWL 추론을 지원하지 않고, SPARQL property path로 계층을 흉내낸다. Neo4j와 똑같이.
즉 “추론”은 RDF의 이론적 강점이지, 프로덕션 RDF 스토어의 기본값이 아니다. 추론기를 실제로 쓰려면 GraphDB나 Stardog을 별도로 도입해야 하고, materialization 비용을 감당해야 한다.
→ “추론을 못 써서 아쉽다”가 아니라, “RDF를 골라도 프로덕션에서는 어차피 추론기를 안 쓴다”가 정확한 현실이다.
→ 그리고 우리 회사 스택(Neo4j, Neptune)에는 OWL 추론기가 아예 없다. 이 결정은 이미 내려져 있다.
6-7. 이사님께 물어봐야 할 3가지
이걸 물어보는 것 자체가 “제대로 파악했다”는 신호다. 대부분은 “Neptune 쓴대요”에서 멈춘다.
| # | 질문 | 왜 중요한가 |
|---|---|---|
| 1 | “Neptune을 RDF로 쓰나요, Property Graph로 쓰나요?” | 되돌릴 수 없는 결정. 두 모델 데이터는 서로 격리됨 |
| 2 | “Neo4j와 Neptune을 둘 다 쓰는 이유가 뭔가요? 개발/프로덕션 분리인가요?” | openCypher 비호환 이슈를 미리 알아야 함. Neo4j에서 짠 쿼리가 Neptune에서 안 도는 경우 있음 |
| 3 | “Neptune Database인가요, Neptune Analytics인가요?” | Graph RAG면 Analytics일 가능성 높음. 벡터 검색이 여기 있음 |
가능한 시나리오:
- 개발/PoC는 Neo4j Desktop (로컬), 프로덕션은 Neptune (AWS) ← 가장 흔한 패턴
- Neptune Database는 저장, Neptune Analytics는 Graph RAG 검색용
- 또는 아직 결정 안 됐고 둘 다 검토 중
AWS는 openCypher Compatibility Checker 도구를 제공한다. Neo4j Cypher 쿼리를 Neptune 마이그레이션 전에 검증해서, 미지원 함수·문법 차이·Neo4j 전용 기능을 리포트해준다. “Neo4j로 개발하고 Neptune으로 배포”라면 이 도구를 반드시 써야 한다.
Layer 7 — Graph RAG
Graph RAG는 하나가 아니다 — 네 갈래
(A) Microsoft GraphRAG 방식 — 문서에서 그래프를 뽑기
문서 → LLM으로 엔티티/관계 추출 → 그래프 구축
→ Leiden 알고리즘으로 커뮤니티 탐지
→ 각 커뮤니티를 LLM이 요약
→ 질문 시 커뮤니티 요약들을 계층적으로 활용
푸는 문제: “이 문서 전체의 주요 테마가 뭐야?” 같은 global 질문. Vector RAG는 top-k 청크만 보니까 전체 조망이 불가능하다.
온톨로지 없어도 동작한다. LLM이 알아서 엔티티를 뽑는다. 대신 일관성이 없다 — “계약자” / “보험계약자” / “가입자”가 다 다른 노드가 된다.
→ 보험 도메인에서는 배제. 규제 산업이고 정확성이 필수다.
(B) 온톨로지 기반 KG 방식 — 스키마 먼저
도메인 온톨로지 설계 (사람이)
→ 문서에서 그 온톨로지에 맞춰 인스턴스 추출 (LLM이)
→ KG 구축
푸는 문제: 정확성이 필수인 질의. “계약자와 수익자가 다를 때 필요한 절차는?” — 이건 유사도 검색이 아니라 구조를 타고 가야 나온다.
→ 우리는 이걸로 간다. 설계 단계.
(C) Text2Cypher / Text2SPARQL — 쿼리 생성
질문 → LLM이 그래프 쿼리로 변환 → 그래프 조회 → 결과를 LLM이 자연어화
(B)와 다르다: (B)는 온톨로지 설계가 초점, (C)는 쿼리 생성이 초점. 실무에서는 (B)로 스키마를 만들고, 런타임에 (C)로 질문을 쿼리로 바꾼다.
→ 런타임 단계.
(D) Vector + Graph 하이브리드 검색
질문 → 벡터로 진입점 노드 탐색 → 거기서 그래프 순회 → 컨텍스트 조립
벡터가 “어디서 시작할지”를 정하고, 그래프가 “거기서 어디로 갈지”를 정한다.
AWS GraphRAG Toolkit의 검색 프로세스가 정확히 이것이다. 벡터 유사도 검색으로 진입점을 찾고, 그래프 순회로 1~2홉 이웃을 탐색한다.
→ 최종 아키텍처.
우리 프로젝트는 무엇인가
| 설명 | 채택 | |
|---|---|---|
| (A) MS GraphRAG | LLM이 엔티티 자동 추출 + 커뮤니티 요약 | X 일관성 없음. 보험은 정확성 필수 |
| (B) 온톨로지 KG | 스키마 먼저, 인스턴스 나중 | O 설계 단계 |
| (C) Text2Cypher | 질문 → 쿼리 변환 | O 런타임 |
| (D) 하이브리드 검색 | 벡터 진입 + 그래프 순회 | O 최종 아키텍처 |
우리는 (B) + (C) + (D) 조합이다. (A)만 배제한다.
실무 아키텍처 — 벡터를 버리지 않는다
그래프가 벡터를 대체하는 게 아니다. 역할이 다르다.
질문
│
┌───────────────┴───────────────┐
│ │
[Vector Search] [Graph Query]
관련 문서 청크 구조적 사실 + 근거출처
"약관 원문 그대로" "계약자≠수익자 → 타인을위한해지"
"제4조 2항 전문" "실효경과 28일 → 자동부활 대상"
│ │
└───────────────┬───────────────┘
│
LLM 컨텍스트
│
최종 답변
(사실 판정 + 원문 근거 인용)
| Vector | Graph | |
|---|---|---|
| 역할 | 어디에 그렇게 쓰여 있는가 (근거) | 무엇이 참인가 (사실) |
| 강점 | 원문 인용, 표현 다양성 흡수 | 관계 정확성, 완전성, 다중홉 |
| 약점 | 관계 구분 못함, 완전성 없음 | 원문 뉘앙스 손실 |
연결 고리는 근거문서 관계다. 그래프가 규칙을 판정하면서 (파일명, 조항번호)를 함께 반환하고, 그걸로 벡터 스토어에서 해당 청크를 정확히 끌어온다.
→ 그래프가 벡터의 라우터 역할을 한다. top-k 유사도가 아니라, 구조가 지목한 위치의 원문을 가져온다.
Neptune Analytics를 쓴다면: 벡터와 그래프가 같은 스토어에 있으므로, 위 다이어그램이 하나의 openCypher 쿼리로 합쳐진다.
골든셋 134문항 전수 분석 — 데이터 모델 선택의 근거
CQ 추정이 아니라 실제 골든셋을 전수 분류했다.
결과
| 유형 | 문항 수 | 비율 |
|---|---|---|
| ① 조건부 규칙 판정 (조건 조합 → 가능/불가/서류) | 97 | 72% |
| ④ 엣지 속성 필요 (엣지에 조건/시점/채널이 붙음) | 41 | 31% |
| ② 단순 사실 조회 (정의, 목록, 수치) | 24 | 18% |
| ⑤ 시점/이력 의존 | 12 | 9% |
| ③ 다중홉 / 파급효과 | 9 | 7% |
| ⑥ 계층 추론 (OWL subsumption) | 4 | 3% |
OWL 추론기를 도입할 근거가 되는 문항 = 4개 / 134 = 3%
계층 추론 4문항 — 그마저도 추론이 필요 없다
| # | 질문 | 왜 계층인가 |
|---|---|---|
| 96 | “납입기간 변경 시 저축성 상품 추징금은 카드납부 가능?” | 상품 계층 요구 |
| 44 | “일반부활보험료 카드 결제 가능?” (저축성상품 카드 RTB 불가) | 동일 |
| 111 | “직무직종 통지 의무 대상 상품은?” | 상품 → 통지의무 |
| 106 | “비흡연할인 가능 상품?” | 동일 |
! 그런데 이 4개조차 진짜 계층 추론이 아니다.
96번 답을 보면, 문서가 이미 개별 상품명을 다 열거해놨다.
카드 가능: 유앤아이종신보험, 변액종신, 평생안심+유니버셜종신보험 카드 불가: 연금보험, UL·PUL, VUL, VA 상품군
즉 문서가 이미 추론 결과를 명시하고 있다. 그래프에 (상품)-[:카드RTB]->(가능) 엣지를 박으면 끝이다.
더 중요한 건 — 규제 산업에서 추론기는 오히려 위험하다.
문서에 명시되지 않은 신상품에 대해 추론기가 “이것도 저축성이니 카드 불가”라고 결론내면, 그건 근거 문서가 없는 답변이다. 컴플라이언스상 쓸 수 없다.
우리가 필요한 건 추론이 아니라 근거 추적이다.
조건부 규칙이 72% — OWL은 이걸 못 한다
골든셋 절대 다수가 이 형태다:
Q123 — 감액완납 가능 조건 (5개 AND):
IF 상품 ∈ {프라임평생설계 1/2형, 프라임평생설계플러스 1/2/3형, CEO정기, ...}
AND 납입횟수 ≥ 36회 ← 부등호 비교
AND 계약상태 = 정상 ← Q118: 납입면제면 불가
AND 잔여납입기간 ≥ 1년
AND 감액완납후_가입금액 ≥ 최저가입금액
THEN 감액완납 가능
Q89 — 납입주기 변경 (모듈로 연산):
현재_납입횟차 mod 목표주기_개월수 == 0 → 변경 가능
(월납→6월납: 6, 12, 18... 회 납입 후 가능)
Q73 — 변액 반제금 종가기준 (날짜 + 시각 + 영업일):
계약일 < 2008-04-01 AND 접수시각 < 14:00 → 신청일 종가, 신청일+1영업일 지급
계약일 < 2008-04-01 AND 접수시각 ≥ 14:00 → 신청일+1영업일 종가, +2영업일 지급
계약일 ≥ 2008-04-01 AND 접수시각 < 17:00 → ...
OWL로 이걸 할 수 있는가?
| 요구 | OWL |
|---|---|
mod 연산 (Q89) |
X 불가. SWRL built-in 필요 |
≥ 36회 부등호 (Q123) |
X owl:hasValue는 등가 비교만. SWRL/SHACL-SPARQL 필요 |
| 날짜+시각+영업일 계산 (Q73) | X 완전히 OWL의 영역이 아님 |
OWL은 Description Logic 기반이다. “이 개체가 어떤 클래스에 속하는가”(분류)에 특화되어 있다. if-then 업무 규칙 엔진이 아니다.
→ RDF를 골라도 72%는 OWL 밖(SWRL/SHACL rules/애플리케이션 코드)에서 처리해야 한다. → 그러면 RDF를 고를 이유가 없다.
엣지 속성이 31% — LPG가 필요한 진짜 이유
Q42 vs Q43 — 시점 문제 (결정적)
| # | 질문 | 답 |
|---|---|---|
| 42 | “청약 당시 내국인 → 현재 미국시민권자. 일반부활 가능?” | 명의변경 후 가능 |
| 43 | “청약 당시부터 미국시민권자였다면?” | 그냥 가능 |
답이 정반대인데, 벡터는 이 둘을 절대 구분 못 한다. 임베딩상 거의 같은 문장이다.
LPG:
(계약)-[:계약자_국적 {기준시점:"청약", 값:"내국인"}]->(:국적)
(계약)-[:계약자_국적 {기준시점:"현재", 값:"미국"}]->(:국적)
엣지 속성에 시점을 단다. 네이티브.
RDF 1.1이면 reification. RDF-star면 되지만, 그럼 추론을 포기하는 것이다.
Q26/60/80/110 — 미성년자 접수 (같은 규칙이 6번 반복):
계약변경 --필요주체--> 친권자 {인원: 1, 조건: "다른 친권자 동의 있음"}
계약변경 --필요주체--> 친권자 {인원: 2, 조건: "동의 없음"}
같은 관계인데 조건에 따라 인원이 다르다.
Q107 — 직무변경 서류가 채널마다 다름:
직무변경통지 --필요서류--> {신분증, 변경통지서} {채널: "계약자 내방"}
직무변경통지 --필요서류--> {대리인신분증, 인감도장} {채널: "대리인 내방"}
직무변경통지 --필요서류--> {녹취, 증빙서류} {채널: "전화"}
다중홉 / 파급효과 — 9문항
| # | 질문 | 홉 구조 |
|---|---|---|
| 66 | “바이탈리티 특약 삭제 시 멤버십도 종료?” | 특약삭제 → 회원 자동해지 → 할인 종료 + 보상혜택 종료 |
| 77 | “감액 시 건강관리자금도 줄어드나?” | 주계약 감액 → 건강관리자금 감액 |
| 133 | “연장정기 후 보험계약대출 가능?” | 연장정기 → 순수보장형 → 대출 불가 |
| 61 | “비흡연할인 중인데 기준금액 밑으로 감액하면?” | 감액 → 가입금액 하락 → 할인 조건 미달 → 할인 종료 |
| 35 | “직업 변경됐으면 부활 전에 통지?” | 직무변경 → 보험료 변경 → 부활 접수 전 선행 필수 |
여기가 벡터 RAG가 반드시 깨지는 곳이다.
최종 판정표
| 요구 유형 | 골든셋 | 비율 | RDF/OWL로 되나? | LPG로 되나? |
|---|---|---|---|---|
| 조건부 규칙 판정 | 97 | 72% | X OWL 밖(SWRL/SHACL)에서 처리 필요 | O 규칙 노드 + 엣지 조건 |
| 엣지 속성 필요 | 41 | 31% | X reification (RDF-star 쓰면 추론 포기) | O 네이티브 |
| 시점/이력 의존 | 12 | 9% | X reification | O 엣지에 시점 속성 |
| 다중홉/파급 | 9 | 7% | O 가능 (느림) | O IFA로 최적 (Neo4j) |
| 계층 추론(OWL) | 4 | 3% | O RDF의 대표 강점 | ! *0.. 패턴으로 대체 |
| 단순 조회 | 24 | 18% | O | O |
→ RDF의 세 강점(추론/URI통합/Named Graph) 모두 미발현. → LPG의 강점(엣지 속성 31%, 다중홉 7%)은 직접 요구. → 그리고 회사 스택(Neo4j, Neptune)에는 어차피 OWL 추론기가 없다.
결론: Property Graph (LPG)로 간다.
버리는 것 (정직하게):
- 표준 추론기(HermiT/Pellet) → 어차피 Neo4j/Neptune 둘 다 없음
- SHACL 형식 검증 → Neo4j 제약조건으로 대체. ! Neptune에는 제약조건도 없음 → 애플리케이션 레벨 검증 필수
- Named Graph 출처 추적 →
-[:근거문서]->관계로 대체 - W3C 표준 이식성 → neosemantics로 RDF 전환 경로 (! Neptune에서는 못 씀)
재검토 조건: 다른 도메인(보상/신계약)이 합쳐지며 깊은 상품 계층이나 외부 온톨로지(FIBO) 통합 요구가 생기면 RDF 재검토.
설계 원칙 — 개념 층 (형식 중립)
여기서는 아직 Turtle도 Cypher도 쓰지 않는다. 종이 위 작업이다.
원칙 1 — 역할(Role) vs 본질(Essence)
문제: 홍길동 한 사람이 계약자이면서 동시에 피보험자일 수 있다.
나쁜 설계:
Class: 계약자, 피보험자, 수익자
→ 홍길동은 계약자 클래스이면서 피보험자 클래스에 동시 소속?
왜 나쁜가: “계약자”는 사람의 본질이 아니라 역할이다. 홍길동이 계약을 해지해도 홍길동은 존재한다. 역할은 계약과의 관계에서 파생된다.
판단 기준:
그것이 사라져도 개체가 계속 존재하면 → 관계 (Property/Relationship) 그것이 사라지면 개체도 사라지면 → 클래스 (Class/Label)
| 후보 | 사라지면? | 판정 |
|---|---|---|
| 계약자 | 홍길동은 여전히 존재 | 관계 |
| 피보험자 | 홍길동은 여전히 존재 | 관계 |
| 수익자 | 홍길동은 여전히 존재 | 관계 |
| 사람 | 홍길동이 존재하지 않음 | 클래스 |
| 종신보험 | 그 계약은 종신보험이 아님 (정체성이 바뀜) | 클래스 |
| 실효(계약상태) | 계약은 여전히 존재, 상태만 바뀜 | 관계 ← 아래 |
올바른 설계 (개념 층):
Class: 사람, 보험계약, 계약변경, 서류, 채널, 계약상태, 프로세스
Relationship:
(보험계약) --계약자--> (사람)
(보험계약) --피보험자--> (사람) ← 같은 사람 가능
(보험계약) --수익자--> (사람)
RDF 구현:
aia:계약_001
aia:hasPolicyholder aia:홍길동 ;
aia:hasInsured aia:홍길동 ; # 같은 사람
aia:hasBeneficiary aia:김영희 .
LPG 구현:
CREATE (c)-[:계약자]->(홍길동)
CREATE (c)-[:피보험자]->(홍길동) // 같은 사람
CREATE (c)-[:수익자]->(김영희)
둘 다 같은 온톨로지의 두 가지 구현이다. 이게 Layer 0의 3층 구조가 실제로 작동하는 모습이다.
원칙 2 — 상태(State)는 속성이 아니라 관계다
골든셋 Q42/Q43이 강제하는 결정:
“청약 시점과 현재 시점의 국적이 다를 경우, 어느 시점 기준으로 판단하는가?”
이 질문은 “상태를 단순 속성으로 두면 답할 수 없다”는 뜻이다.
X 나쁜 설계: (계약 {상태: "실효"})
→ 언제부터 실효인지, 이전 상태가 뭐였는지 모름
→ Q5(실효경과일), Q42(시점기준) 답 불가
O 좋은 설계: 상태를 관계로 승격 + 엣지에 시점 부착
(계약)-[:상태이력 {시작일:"2024-03-01", 종료일:null}]->(:계약상태 {name:"실효"})
(계약)-[:상태이력 {시작일:"2023-01-01", 종료일:"2024-02-29"}]->(:계약상태 {name:"정상"})
여기가 LPG를 고른 이득이 직접 발생하는 지점이다.
RDF 1.1이었다면 상태이력 관계에 시작일/종료일을 못 붙이므로 reification이 필요하다.
! Neptune Database에서는 Date 타입이 미지원이므로, 날짜 비교를 애플리케이션에서 하거나 Neptune Analytics를 써야 한다. (6-3 참조)
원칙 3 — 규칙을 그래프에 넣는다 (근거 추적)
골든셋 72%가 조건부 규칙이므로, 이 결정이 아키텍처를 가른다.
| 방식 | 장점 | 단점 |
|---|---|---|
| A. 규칙을 노드로 | 규칙 조회/설명 가능, LLM이 근거 제시 가능 | 복잡한 조건 표현 한계 |
| B. 규칙은 코드로 | 표현력 무제한 | 규칙이 그래프 밖 → RAG가 근거를 못 댐 |
RAG 목적상 A를 우선한다. LLM이 “왜 그런 판정이 나왔는지” 설명하려면 규칙 자체가 검색 가능해야 한다.
(:변경규칙 {id:"R-감액-001", 설명:"실효 상태에서는 감액 불가, 부활 선행 필요"})
-[:적용대상]-> (:계약변경 {name:"감액"})
-[:전제조건]-> (:계약상태 {name:"실효"})
-[:판정]-> (:결과 {value:"불허"})
-[:근거문서]-> (:문서 {file:"계약변경_03.pdf", 조항:"제4조 2항"})
근거문서 관계가 핵심이다. 이게 있어야:
- 그래프 답변에 원문 출처를 붙일 수 있고
- 벡터 검색의 라우터가 된다
규제 산업에서 이게 추론보다 중요하다.
원칙 4 — 규칙 재사용이 그래프의 진짜 가치다
골든셋에 동일 규칙의 중복 질문이 많다.
미성년자 친권자 규칙이 Q5, Q7, Q26, Q60, Q80, Q110 — 6번 반복된다. 표현만 다르고 규칙은 하나다.
벡터는 6개 청크를 따로 저장한다. 그래프는 규칙 노드 1개다.
(:규칙 {id:"R-미성년-친권자"})
-[:조건]-> (:상황 {계약자유형:"미성년", 타친권자동의:true}) -[:요구]-> (:친권자 {인원:1})
-[:조건]-> (:상황 {계약자유형:"미성년", 타친권자동의:false}) -[:요구]-> (:친권자 {인원:2})
-[:적용업무]-> (:미납안내), (:일반부활), (:감액), (:갱신불원), (:건강플러스할인)
한 규칙이 5개 업무에 적용된다. 벡터로는 이 재사용이 안 보인다.
반복 패턴 3개:
- 친권자 규칙 — 6문항
- 신청권자 — Q88, Q97, Q105, Q113(계약자), Q124/129(법인 대표자만)
- 취소 가능 여부 — Q57, Q63, Q121, Q130 (엣지 속성 없으면 표현 불가)
이 프로젝트에서의 논증 구조
① 층위 분리 (혼동 제거)
“온톨로지·RDF·Neo4j는 같은 층위가 아닙니다.
온톨로지는 개념 모델 — 무엇이 존재하고 어떻게 연결되는가. RDF와 LPG는 데이터 모델 — 그걸 어떤 자료구조로 담는가. Neo4j·Neptune·GraphDB는 저장 엔진 — 그 자료구조를 어떻게 디스크에 굽는가.
그리고 중요한 사실 하나: Neo4j는 LPG 전용이지만, Neptune은 RDF도 되고 Property Graph도 됩니다.
즉 ‘Neptune을 쓴다’는 결정만으로는 데이터 모델이 정해지지 않습니다. 그 안에서 또 골라야 하고, 두 모델의 데이터는 서로 격리되어 나중에 못 바꿉니다.
그래서 RDF를 이해해야 했습니다. 안 쓰기로 하더라도, 그 결정을 근거 있게 하려면요.”
② RDF의 본질
“RDF는 (주어, 서술어, 목적어) 트리플 하나가 전부입니다. 그래프는 트리플을 던져 넣으면 저절로 생기는 부산물입니다.
대가가 있습니다. 트리플은 3칸뿐이라 엣지에 속성을 못 답니다. ‘2024년 1월 1일에 체결했다’를 쓰려면 관계를 노드로 승격시켜야 하고(reification), 트리플 1개가 4개로 늘어납니다.
다만 이건 RDF 1.1 얘기입니다. RDF-star(RDF 1.2)가 이걸 해결했고, GraphDB·Stardog·Neptune 1.3+에서 이미 지원합니다.”
③ RDF의 강점은 셋 — 그리고 왜 우리에겐 안 맞는가 ( 실력 판별점)
“RDF의 강점은 셋입니다. ‘유일한 강점이 추론’이라는 건 틀린 말입니다.
① 표준 추론(OWL) —
종신보험 subClassOf 생명보험을 선언하면 추론기가 ‘계약_001은 생명보험이다’를 자동 생성합니다. ② 전역 URI — 다른 조직의 데이터를owl:sameAs로 병합합니다. ③ Named Graph — 트리플이 어느 문서에서 왔는지를 데이터 모델 차원에서 지원합니다.그런데 우리 도메인에서는 셋 다 발현되지 않습니다.
① 추론: 골든셋 134문항 중 4개(3%)만 계층 추론을 요구합니다. 그마저도 문서가 개별 상품을 이미 열거해놔서 추론이 불필요합니다. 오히려 규제 산업에서 추론기는 위험합니다. 문서에 없는 신상품을 추론기가 자동 분류하면, 그건 근거 문서가 없는 답변이라 컴플라이언스상 못 씁니다.
② URI 통합: AIA 사내 문서 21개입니다. 외부 병합 계획이 없습니다. 요구 0%.
③ Named Graph: 필요하지만 과잉입니다. 규칙 노드에
-[:근거문서]->하나 붙이면 충분합니다.그리고 결정적으로 — 우리 스택인 Neo4j와 Neptune, 둘 다 OWL 추론기가 없습니다. Neptune조차 RDF 모드에서 SPARQL property path(
subClassOf*)로 계층을 흉내낼 뿐입니다. Neo4j의IS_A*0..와 정확히 같은 방식입니다.즉 ‘추론’은 RDF의 이론적 강점이지, 프로덕션 RDF 스토어의 기본값이 아닙니다.”
④ Open World와 SHACL
“RDF에서
rdfs:range는 제약이 아니라 추론 규칙입니다. Open World Assumption 때문입니다.계약자의 range를 ‘사람’으로 선언하고 법인을 넣으면, 에러가 아니라 ‘법인도 사람이다’라고 추론합니다.
보험 데이터에서 이건 위험합니다. 그래서 RDF를 쓰면 SHACL로 검증을 따로 걸어야 합니다.
OWL과 SHACL은 목적이 정반대입니다. OWL은 Open World에서 지식을 확장하고, SHACL은 Closed World에서 위반을 리포트합니다. 같은 선언이 정반대로 동작합니다. 실무 RDF는 둘 다 씁니다.”
⑤ Neo4j 물리 구조
“Neo4j가 빠른 이유는 Index-Free Adjacency입니다.
먼저 오해를 하나 짚겠습니다. IFA는 디스크 IO를 없애는 게 아니라, 인덱스 조회를 없애는 겁니다. 디스크 IO를 줄이는 건 페이지 캐시고, 그건 RDBMS도 똑같이 합니다.
RDBMS는 JOIN마다 B-tree를 타서 홉당 O(log N)입니다. Neo4j는 레코드 크기를 고정해서, ID만 알면
ID × 레코드크기로 디스크 오프셋을 곱셈 한 번에 계산합니다. 관계 레코드가 이중 연결 리스트를 이루므로, 이웃으로 가는 건 인덱스 없이 포인터를 따라가는 O(1) 연산입니다.가변 길이였다면 어디부터 읽을지 몰라서 인덱스가 필요해집니다. 고정 크기가 핵심입니다.
단서 둘. 첫 노드는 인덱스를 탑니다 — IFA는 그 다음부터입니다. 그리고 관계가 수백만 개 붙은 슈퍼노드에서는 연결 리스트가 길어져 이점이 무너집니다. 이건 엔진이 아니라 모델링으로 피해야 합니다.
! 그리고 중요한 건 — 이 설명은 Neo4j에만 유효합니다. Neptune은 IFA 구조가 아닙니다. 컴퓨트와 스토리지를 분리한 Aurora 유사 분산 아키텍처라 성능 특성이 다릅니다.”
⑥ Neo4j와 Neptune은 같지 않다 ( 실무 판별점)
“‘Neo4j로 개발하고 Neptune에 배포’는 함정이 있습니다.
Neptune은 openCypher 명세 version 9를 지원하는데, Neo4j Cypher의 부분집합입니다. 구체적으로:
- APOC 프로시저 없음 — Neptune은 커스텀 프로시저를 지원하지 않습니다
- GDS 없음 — 그래프 알고리즘 라이브러리가 없습니다
shortestPath()미지원- Date/Duration 타입 미지원 — 이게 우리에게 치명적입니다. 골든셋 12문항(9%)이 시점 계산을 요구하는데,
duration.inDays()같은 걸 못 씁니다- 제약조건 없음 —
CREATE CONSTRAINT가 없습니다. SHACL 대체 수단이 사라집니다- 노드 ID가 정수가 아니라 문자열
AWS가 openCypher Compatibility Checker를 제공합니다. Neo4j 쿼리를 마이그레이션 전에 검증하는 도구입니다. 이걸 반드시 써야 합니다.
그리고 일관성 모델도 다릅니다. Neo4j는 causal consistency로 read-your-own-writes를 보장하지만, Neptune 읽기 복제본은 eventual consistency입니다.”
⑦ Neptune Database vs Analytics ( Graph RAG 핵심)
“Neptune은 두 제품입니다. 이름이 비슷해서 헷갈리는데 완전히 다릅니다.
Neptune Database — 트랜잭션 그래프 DB. RDF 또는 PG. 벡터 검색 없습니다. Neptune Analytics — 메모리 최적화 분석 엔진. 벡터를 그래프 노드 속성으로 저장하고, openCypher 쿼리 안에서
topKByNode,topKByEmbedding으로 그래프 순회와 벡터 유사도를 같이 실행합니다.Graph RAG를 한다면 AWS가 미는 답은 Analytics입니다. Bedrock Knowledge Bases의 관리형 GraphRAG가 Neptune Analytics를 백엔드로 씁니다.
Database를 쓰면 벡터 스토어를 따로 붙여야 합니다 — OpenSearch나 pgvector로요.”
⑧ 모델링 원칙 (설계 감각)
“설계에서 가장 중요한 판단은 무엇이 클래스이고 무엇이 관계인가였습니다.
‘계약자’를 클래스로 만들면 안 됩니다. 계약자는 사람의 본질이 아니라 역할입니다. 홍길동이 계약을 해지해도 홍길동은 존재합니다. 역할은 관계로 모델링했고, 그래서 홍길동이 계약자이면서 동시에 피보험자인 경우가 자연스럽게 표현됩니다.
기준은 하나입니다. 사라져도 개체가 존속하면 관계, 개체도 사라지면 클래스.
같은 논리로 계약상태도 속성이 아니라 관계로 승격시켰습니다. 골든셋에 이런 문항이 있습니다:
- ‘청약 당시 내국인 → 현재 미국시민권자’ → 명의변경 후 가능
- ‘청약 당시부터 미국시민권자’ → 그냥 가능
답이 정반대인데 벡터는 이 둘을 구분 못 합니다. 임베딩상 거의 같은 문장입니다.
(계약)-[:국적 {기준시점:"청약", 값:"내국인"}]->처럼 엣지에 시점을 답니다. 이게 LPG를 고른 이득이 직접 발생하는 지점입니다.”
⑨ 왜 LPG인가 ( 표를 보여준다)
“CQ 추정이 아니라 실제 골든셋 134문항을 전수 분류했습니다.
- 조건부 규칙 판정: 97문항 (72%)
- 엣지 속성 필요: 41문항 (31%)
- 시점/이력 의존: 12문항 (9%)
- 다중홉/파급효과: 9문항 (7%)
- 계층 추론(OWL): 4문항 (3%)
RDF의 대표 강점인 계층 추론이 3%입니다.
반면 72%가 조건부 규칙인데, OWL은 규칙 엔진이 아닙니다. Description Logic 기반이라 분류(subsumption)에 특화되어 있습니다.
- 납입주기 변경(Q89)은
현재_횟차 mod 목표주기 == 0— 모듈로 연산- 감액완납(Q123)은
납입횟수 ≥ 36회— 부등호 비교- 변액 종가기준(Q73)은 날짜 + 시각 + 영업일 계산
OWL은 이걸 전부 못 합니다. SWRL을 붙여야 하고 추론기 성능이 붕괴합니다.
즉 RDF를 골라도 72%는 OWL 밖에서 처리해야 합니다. 그러면 RDF를 고를 이유가 없습니다.
그래서 Property Graph를 선택했습니다. 유행이 아니라 골든셋 134문항 전수 분류에 근거한 선택입니다.”
⑩ 벡터를 버리지 않는 이유
“그래프로 벡터를 대체하는 게 아닙니다. 역할이 다릅니다.
- 그래프: 무엇이 참인가. ‘계약자 ≠ 수익자 → 타인을위한해지 적용’은 유사도로 안 나옵니다.
- 벡터: 어디에 그렇게 쓰여 있는가. 약관 원문을 그대로 인용해야 하는 상황에 필요합니다.
연결 고리는 그래프의
근거문서관계입니다. 규칙 노드가 반드시(파일명, 조항번호)를 갖도록 강제했고, 그래프가 판정을 내리면서 그 좌표를 함께 반환합니다. 그걸로 벡터 스토어에서 해당 청크를 정확히 끌어옵니다.즉, 그래프가 벡터의 라우터 역할을 합니다. top-k 유사도가 아니라, 구조가 지목한 위치의 원문을 가져옵니다.
Neptune Analytics를 쓴다면 이게 하나의 openCypher 쿼리로 합쳐집니다. 벡터와 그래프가 같은 스토어에 있으니까요.”
다음 액션
이사님께 물어야 할 3가지 ( 최우선)
| # | 질문 | 왜 |
|---|---|---|
| 1 | “Neptune을 RDF로 쓰나요, Property Graph로 쓰나요?” | 되돌릴 수 없는 결정. 두 모델 데이터는 격리됨 |
| 2 | “Neo4j와 Neptune을 둘 다 쓰는 이유가? 개발/프로덕션 분리인가요?” | openCypher 비호환 이슈. Neo4j 쿼리가 Neptune에서 안 돌 수 있음 |
| 3 | “Neptune Database인가요, Analytics인가요?” | Graph RAG면 Analytics. 벡터 검색이 여기 있음 |
이 3개를 묻는 것 자체가 “제대로 파악했다”는 신호다. 대부분은 “Neptune 쓴대요”에서 멈춘다.
그 다음
- 벡터 RAG 실패 5개 실측 — 도입 근거. 골든셋 Q42/Q43(시점), Q66(파급) 등으로 실패 로그 확보
- 골든셋 134문항 분류표 완성 — 이게 데이터 모델 선택의 근거 전부
- 파일 2개로 소규모 그래프 구축 — 21개 전부로 시작하지 말 것. 스키마는 반드시 틀린다. 2개로 틀리고 고치는 게 21개보다 10배 싸다
- 감액/감액완납 파일 우선 — 골든셋 134문항 중 감액 관련이 28문항(21%)로 가장 많다. 조건부 규칙 + 엣지 속성 + 파급효과 + 취소규칙을 전부 포함하는 가장 복잡한 케이스다. 여기서 스키마가 통과하면 나머지도 통과한다
- 골든셋 5문항을 그 그래프에서 실제 쿼리 — 답이 나오면 스키마 검증 완료
- openCypher Compatibility Checker 돌려보기 — Neo4j로 짠 쿼리가 Neptune에서 도는지
자주 틀리는 것 정리
| # | 흔한 오해 | 정확한 사실 |
|---|---|---|
| 1 | “RDF는 엣지에 속성 못 단다” | RDF-star(1.2)가 해결. Neptune 1.3+ 지원. 단, OWL 추론기가 RDF-star를 못 다룸 |
| 2 | “RDF의 유일한 강점은 추론” | 셋이다. 추론 + URI 통합 + Named Graph |
| 3 | “LPG는 Closed World” | LPG는 논리 시스템이 아니다. OWA/CWA는 형식 의미론 개념. 애초에 해당 없음 |
| 4 | “IFA는 디스크 IO를 0으로” | 인덱스 조회를 0으로. 디스크 IO는 페이지 캐시가 담당 |
| 5 | “노드 15바이트, 관계 34바이트” | 3.x 기준. 4.x aligned format은 다름. 중요한 건 ‘고정 크기’라는 성질 |
| 6 | “LPG는 스키마 없음” | 제약 기반 스키마 있음 (NODE KEY, EXISTS, UNIQUE, PROPERTY TYPE). ! 단 Neptune에는 없음 |
| 7 | “RDF는 스키마리스라 LPG와 다름” | LPG도 스키마리스. RDBMS와 다른 것이지 RDF만의 특성 아님 |
| 8 | “Neptune 쓰면 RDF다” | Neptune은 RDF도 LPG도 된다. 별도 결정 필요. 데이터는 서로 격리됨 |
| 9 | “Neptune openCypher = Neo4j Cypher” | 부분집합. APOC/GDS/Date타입/제약조건/shortestPath 없음 |
| 10 | “Neo4j가 IFA로 빠르니 Neptune도” | Neptune은 IFA가 아니다. 분산 스토리지 아키텍처 |
| 11 | “Neptune에 벡터 검색 있다” | Neptune Analytics에만. Neptune Database에는 없음 |
| 12 | “추론은 공짜” | Materialization 비용. 트리플 폭증 + 데이터 변경 시 재계산 |