RDF-LPG

RDF-LPG

RDF와 LPG(Labeled Property Graph)는 둘 다 “그래프”라고 불리지만, 데이터를 저장하는 최소 단위 자체가 다르다.

  • RDF: 모든 것을 주어-술어-목적어의 트리플(Triple) 하나로 통일해서 저장한다. 속성이든 관계든 구분 없이 전부 트리플이다.
  • LPG(Neo4j): 노드/관계/속성을 물리적으로 분리된 저장 구조로 나눠서 저장한다. 값(속성)과 연결(관계)의 역할이 다르기 때문이다.

이 차이는 취향이나 설계 철학의 문제가 아니라, 디스크에 어떻게 저장하고 어떻게 빠르게 순회할 것인가라는 물리적인 이유에서 나온다.

RDF: 모든 것이 트리플이다

RDF(Resource Description Framework)는 세상의 모든 사실을 “주어-술어-목적어” 세 조각짜리 문장으로만 표현한다.

(정환, 직급, "GenAI엔지니어")
(정환, 소속, 베스핀글로벌)

여기서 중요한 포인트: 직급도, 소속똑같이 트리플의 두 번째 자리(predicate, 술어)에 들어간다. RDF 입장에서는 이 둘이 구조적으로 완전히 동일한 존재다 — 둘 다 “주어-술어-목적어” 문장일 뿐이고, 목적어가 단순 문자열(“GenAI엔지니어”)이냐 다른 리소스(베스핀글로벌)냐의 차이만 있을 뿐이다.

RDF의 데이터 모델 안에는 “속성”이라는 별도 카테고리가 아예 존재하지 않는다. 값이든, 다른 개체와의 연결이든, 전부 predicate라는 동일한 형태로 흡수된다.

RDF는 이 트리플 구조 위에 RDFS/OWL이라는 별도의 의미론(semantics) 레이어를 얹어서 논리적 추론(상속, 이행성 등)을 수행한다. RDFS/OWL 자체도 트리플로 표현되는 메타데이터이며, 이 추론 엔진이 DB 차원에서 기본 내장되어 있다는 게 RDF 계열의 특징이다.

LPG(Neo4j): 노드·관계·속성을 물리적으로 분리한다

LPG는 반대로, 값(속성)과 연결(관계)을 저장 구조 자체에서부터 다른 파일/레코드로 분리한다.

노드: 정환 { 직급: "GenAI엔지니어" }        ← 속성(property, key-value)
관계: (정환) -[:소속]-> (베스핀글로벌)        ← 관계(relationship, 노드 간 연결)

직급은 노드 내부에 박힌 key-value 값이고, 소속은 노드와 노드를 잇는 화살표다. 이 둘은 저장 구조 자체가 다르다 — 하나는 노드 내부 데이터, 하나는 노드 간 연결이다.

LPG는 RDF처럼 OWL/RDFS 같은 별도의 의미론 레이어(추론 규칙 체계)를 DB 엔진 차원에서 강제하지 않는다. 대신 클래스(노드)와 관계(링크) 양쪽 모두에 key-value 속성을 자유롭게 붙일 수 있다는 게 LPG의 실무적 강점이다.

관계에 속성을 붙일 수 있다는 것 — LPG의 최대 강점

(계약)-[:감액신청 {신청일자: "2026-07-01", 감액후보험료: 30000, 반제금여부: true}]->(계약자)

“계약과 계약자가 감액신청이라는 관계로 연결되어 있다”는 사실만 저장하는 게 아니라, 그 관계가 언제, 어떤 조건으로 발생했는지까지 관계 자체에 데이터로 붙일 수 있다. 이는 다른 지배적인 그래프 모델(RDF)에 비해 LPG가 가진 가장 큰 실용적 이점으로 꼽힌다. RDF였다면 이런 “관계에 딸린 세부 정보”를 표현하기 위해 관계 자체를 별도 노드로 승격시키는 우회(reification)가 필요했을 것이다.

왜 이렇게 다르게 저장하는가 — 진짜 이유는 디스크 구조와 성능

이 차이는 철학적 선택이 아니라 물리적 저장 방식에서 비롯된다.

Neo4j는 노드, 관계, 속성을 각각 별도의 저장 파일(store file)에 나눠서 저장한다. 그리고 각 관계 레코드는 시작 노드와 끝 노드의 주소를 직접 담고 있는 고정 크기 레코드다. 노드나 관계의 식별자를 해당 저장 파일의 단순 인덱스로 취급하면, 상수 시간(O(1))에 접근이 가능해진다 — 예를 들어 어떤 노드가 ID 3인 관계를 참조한다면, 이는 관계 저장 파일의 세 번째 레코드라는 뜻이고, 레코드 크기가 고정되어 있으므로 바이트 오프셋 계산만으로 즉시 접근할 수 있다. ID 자체가 곧 위치 계산식이 되어서 인덱스 프리로 산수 한 번으로 노드를 찾아갈 수 있다.

이것이 인덱스 프리 인접성(Index-free adjacency)이다 — 노드가 관계를 인덱스 검색 없이 포인터로 직접 따라간다는 뜻이며, 이것이 그래프 데이터베이스가 존재하는 근본적인 아키텍처적 이유다.

여기서 핵심은 관계(구조)와 속성(값)의 역할이 다르다는 것이다.

  • 관계: “순회(traversal)”를 위한 구조. 포인터를 따라 다음 노드로 즉시 이동하는 게 목적.
  • 속성: “값 조회”를 위한 데이터. 특정 노드/관계에 도달했을 때만 필요한 세부 정보.

이 둘을 분리해두면, 그래프를 여러 단계 순회할 때(multi-hop) 불필요한 속성 값을 함께 읽어들이지 않고 포인터만 쭉 따라갈 수 있다. 관계가 자신만의 속성을 가질 수 있다는 것 자체가 그래프 알고리즘에 추가 메타데이터를 제공하고, 관계에 의미(품질, 가중치 등)를 더하고, 런타임에 쿼리를 제약하는 데 특히 유용하다.

반면 RDF처럼 값과 연결을 모두 동일한 트리플 구조로 뭉쳐버리면, 순회할 때마다 불필요한 값 데이터까지 함께 읽어야 해서 다단계 순회 성능이 떨어질 수 있다.

인덱스 프리 인접성이 실제로 어떻게 동작하는가 — 바이트 오프셋과 고정 크기 레코드

“O(1) 상수시간 접근”이라는 말이 추상적으로 들릴 수 있는데, 원리는 배열의 인덱스 접근과 동일하다.

핵심 아이디어: 파일에 저장된 모든 레코드(데이터 한 덩어리, 예: 노드 하나)의 크기가 정확히 고정되어 있다면, N번째 레코드가 파일의 몇 바이트 지점에서 시작하는지 계산만으로 알 수 있다.

레코드 크기가 15바이트로 고정되어 있다면:

0번 레코드: 파일의 0 ~ 14바이트
1번 레코드: 파일의 15 ~ 29바이트
N번 레코드: 파일의 (N × 15) ~ (N × 15 + 14)바이트

N × 15바이트 오프셋(byte offset) — “이 레코드가 파일 시작점에서 몇 바이트 떨어져 있는가”라는 뜻이다. 이 계산은 곱셈 한 번이라 레코드가 100개든 1억 개든 계산량이 항상 동일하다 — 이것이 상수시간(O(1))이다. 배열의 array[N] 접근이 O(1)인 것과 정확히 같은 원리다.

반대로 레코드 크기가 고정되어 있지 않다면, N번째 레코드가 어디서 시작하는지 계산할 방법이 없다. 앞의 모든 레코드를 순서대로 읽으며 누적 바이트 수를 세야 하고, 이는 링크드리스트를 처음부터 순회하는 것과 같아 O(N)이 걸린다.

Neo4j에 적용하면: 노드 레코드(15바이트), 관계 레코드(34바이트)가 고정 크기이고, 각 관계 레코드 안에는 시작 노드·끝 노드의 ID가 숫자로 박혀 있다. 이 ID가 곧 “몇 번째 레코드인가”라는 위치 정보이기 때문에:

관계 레코드에 "시작 노드 ID = 3"이라고 적혀 있으면
→ 3 × 15바이트 = 45바이트 지점으로 즉시 점프
→ 별도 인덱스(B-tree 등) 검색 없이 해당 노드 레코드를 바로 읽음

ID 자체가 위치 계산식이 되기 때문에 “인덱스 프리(index-free)”라 불린다. 반면 RDF/SPARQL이나 관계형 DB에서 “이 값을 가진 레코드를 찾아라”는 보통 인덱스(B-tree)를 거쳐야 하고, 이는 로그 시간(O(log N))이 걸려 데이터가 늘어날수록 조금씩 느려진다. Neo4j는 “다음 관계로 이동”이라는 연산만큼은 인덱스 검색 자체를 생략하기 때문에, 그래프 크기와 무관하게 순회 속도가 일정하게 유지된다.

RAG 파이프라인에서 청크와 엔티티를 그래프에 연결하는 방법

지금까지는 “그래프가 물리적으로 어떻게 저장되는가”였다면, 이건 “문서 조각(청크)과 그래프 노드를 실제로 어떻게 이어붙이는가”의 문제다.

기본 구조는 다음과 같다.

[원본 문서] → [청크] → [청크에서 엔티티 추출] → [그래프 노드/관계 생성]
                                              ↓
                             [청크 자체도 노드로 저장, 엔티티와 연결]

1. 청크도 그래프의 노드가 된다.

(:Chunk {chunk_id: "chunk_042", text: "...", 문서명: "계약변경_감액", 페이지: 12})

2. 청크에서 엔티티를 추출해 별도 노드로 만들고, 청크와 연결한다.

(:개념 {name: "타인을위한해지프로세스"})
(:당사자유형 {name: "수익자"})

(chunk_042)-[:MENTIONS]->(타인을위한해지프로세스)
(chunk_042)-[:MENTIONS]->(수익자)
(타인을위한해지프로세스)-[:요구한다]->(수익자동의)

여기서 관계는 두 종류로 나뉜다.

  • (청크)-[:MENTIONS]->(엔티티): 이 청크가 이 개념을 언급했다는 출처 추적용 관계. RAG에서 답변의 근거를 원본 청크로 역추적할 때 사용.
  • (엔티티)-[:관계이름]->(엔티티): 청크와 무관한, 도메인 지식 자체의 관계.

같은 개념이 여러 청크·여러 문서에 걸쳐 반복 언급될 수 있으므로, 매번 새 노드를 만들지 않고 개념은 하나의 노드로 통합하고 그 개념이 어느 청크들에서 언급됐는지만 MENTIONS로 연결한다. 이렇게 하면 “이 개념이 뭐야?”는 엔티티 노드 자체로 즉답하고, “어느 문서 몇 페이지에 나와?”는 MENTIONS 관계를 역추적해서 답할 수 있다.

엔티티 추출 시 실무 원칙: 추출 작업 자체는 LLM에게 맡기되, “어떤 카테고리의 엔티티만 뽑을지”는 미리 정의한 클래스 스키마로 제한해서 프롬프트에 주입해야 한다. 그렇지 않으면 “휴대폰 인증”, “신분증 인증”, “본인인증”처럼 같은 개념이 표현만 다르게 각각의 별도 엔티티로 파편화된다.

스키마는 한번 정하면 계속 쓰는가

원칙적으로는 그렇다. 다만 “고정불변”이 아니라 “함부로 바꾸지 않는다”에 가깝다.

안정적으로 유지해야 하는 이유: 스키마가 자주 바뀌면 이미 저장된 데이터를 전부 마이그레이션해야 하고, 애플리케이션 코드(Pydantic 모델, Cypher 쿼리)가 스키마를 전제로 짜여 있어 스키마 변경 시 코드도 함께 고쳐야 한다.

그럼에도 진화는 필요하다: 새로운 도메인이 편입되거나 기존 클래스로 표현 안 되는 케이스가 계속 나오면 스키마를 확장(schema evolution)해야 한다. 실무에서는 핵심 스키마는 엄격히 잡되, 기존 클래스의 이름을 바꾸거나 구조를 갈아엎는 것은 최대한 피하고, 새로운 클래스·관계를 추가하는 것은 비교적 유연하게 허용하는 방향으로 운영한다. LPG는 관계형 DB처럼 엄격한 테이블 스키마가 없어 새 라벨·관계 타입 추가 자체의 마이그레이션 부담이 상대적으로 적다는 장점도 있다.

즉 처음 설계 시점에 Competency Questions로 충분히 검증해서 갈아엎는 비용을 최소화하고, 이후로는 “추가는 유연하게, 변경은 신중하게”가 원칙이다.

실무에서 “이걸 속성으로 둘지, 관계(노드)로 둘지”는 어떻게 정하는가

LPG 설계 시 자주 나오는 고민이 “이 값을 그냥 속성으로 둘지, 아니면 별도 노드로 승격시켜 관계로 연결할지”이다. 이건 Neo4j가 자동으로 정해주지 않고, 설계자가 스키마 설계 단계에서 직접 판단해야 한다.

실무적인 판단 기준은 “이 값으로 그래프에 접근(검색·필터링)해야 하는가”이다. 그래프에 접근하는 가장 좋은 방법은 노드, 라벨, 패턴 매치를 통하는 것이며, 접근 방식이 결국 성능을 좌우하는 핵심 요인이자 데이터를 노드+라벨로 모델링할지, 관계로 모델링할지, 속성으로 모델링할지를 정하는 주된 기준이 되어야 한다.

  • 그 값 자체로 검색하거나, 여러 노드가 그 값을 공유하며 서로 연결될 필요가 있다 → 노드로 승격 + 관계
  • 특정 노드에 도달했을 때만 참조하면 되는 부가 정보다 → 속성

예: “베스핀글로벌”을 회사라는 별도 개체로 취급해 다른 계약·다른 직원과도 연결해야 한다면 노드로 만들고, 단순히 “정환님 소속이 어디인지”만 알면 된다면 속성으로 둬도 충분하다.

정리

  RDF LPG(Neo4j)
최소 저장 단위 트리플(주어-술어-목적어) 하나 노드/관계/속성이 물리적으로 분리된 구조
속성과 관계의 구분 구분 없음 — 전부 predicate로 통일 명확히 구분 — 노드 내부 값 vs 노드 간 연결
관계 자체에 속성 부여 어려움 (reification 우회 필요) 자유로움 — LPG의 핵심 강점
의미론(추론) 레이어 RDFS/OWL이 DB 차원에서 기본 내장 기본 내장 없음 — 필요시 애플리케이션 로직으로 구현
구분하는 이유 트리플 통일이 웹 데이터 교환·표준화에 유리 인덱스 프리 인접성으로 다단계 순회 성능 최적화

결국 이 차이는 “어떤 게 더 우월한 기술인가”의 문제가 아니라, RDF는 표준화된 지식 교환과 형식 추론에, LPG는 실시간 순회 성능과 관계 자체의 풍부한 표현에 최적화된 서로 다른 설계 목표를 가진 저장 모델이라는 것으로 이해하는 게 정확하다.


© 2022 JeongHwan Yun.