5 BUILDING KNOWLEDGE GRAPHS · CHAPTER 5

지식 그래프와
정보 시스템의 통합

Integrating Knowledge Graphs with Information Systems

지식 그래프는 그 자체로도 유용한 시스템이지만, 진짜 힘은 다른 시스템과 통합될 때 나온다. 통합된 지식 그래프는 다른 시스템을 풍요롭게 하고, 그 대가로 다시 풍요로워지는 선순환을 이룬다. 이 장은 클라이언트 사이드 데이터베이스 드라이버부터 (커스텀) 함수·프로시저, API, 스트리밍 미들웨어, 그리고 ETL 도구까지 — 데이터 패브릭에 지식 그래프를 통합하는 대중적 선택지들을 안내한다.

Jesús Barrasa · Jim Webber O'Reilly Media, 2023 Part I. Graph Fundamentals pp. 65–92
Towards a Data Fabric

데이터 패브릭을 향하여 — 사일로 위의 정교한 인덱스

데이터 패브릭(data fabric)은 기저 시스템들의 데이터에 대한 연결된 뷰를 제공하는 범용, 조직 전체 데이터 접근 계층이다. 서드파티 시스템의 데이터 접근 세부를 추상화하여, 사용자가 지식 그래프를 통해 그 데이터에 투명하게 접근하도록 한다. 지식 그래프를 받치는 데이터 패브릭은 시스템들 위의 전사적 인덱스를 제공하여, 고객·제품·이벤트 같은 중요한 엔터티의 골든 레코드(golden record)를 갖춘 신뢰할 수 있는 마스터 데이터 계층으로 데이터를 소비하게 한다.

와이어 수준의 바이트 전송은 전통적 통합 기법으로 해결되지만, 정보 모델은 패브릭의 심장부에 있는 지식 그래프로 수렴한다. 지식 그래프의 임무는 데이터가 관계형 테이블로 저장되든 NoSQL 키-값으로 저장되든 무관하게, 사일로를 가로질러 데이터를 큐레이션하는 정교한 인덱스를 제공하는 것이다.

클라이언트 애플리케이션 / 사용자 "Seattle의 고객 John Smith를 줘" 지식 그래프 사일로 간 큐레이션 인덱스 · 리프 노드가 타 시스템 레코드로 리다이렉트 INTEGRATION LAYER — DATA FABRIC (기저 시스템은 대개 패브릭을 모른다) 문서 데이터베이스 고객 레코드 그래프 데이터베이스 제품 카탈로그 관계형 데이터베이스 청구(billing) 정보
그림 5-1 · 데이터 통합에 지식 그래프 사용 — 클라이언트는 여러 백엔드가 쓰였다는 사실조차 모른 채 전체론적 데이터 집합을 받는다

데이터 아키텍트들이 그래프에 눈을 돌리는 이유는, 각 소스 시스템이 기술하는 같은 엔터티의 이질적 표현 여럿을 수용할 만큼 그래프가 유연하기 때문이다. 그래프에서는 데이터가 발견되는 대로 기저 레코드를 점진적으로 연관시킬 수 있다. 비즈니스 민첩성을 저해할 뿐인 크고 선행적인(big, up-front) 설계가 필요 없다. 데이터 패브릭 통합은 일회성 노력이 아니며, 그래프 데이터 모델은 데이터 도메인의 수명 내내 유연함을 유지한다.

지식 그래프로 데이터를 큐레이션하면 상당한 이점이 있다. 노드 차수, 이웃 정보, 중심성 지표 같은 특성으로 정교하고 효과적인 데이터 통합을 구축할 수 있다. 예컨대 이런 매칭 규칙을 정의할 수 있다 — 두 노드의 이름이 강한 문자열 유사도(예: 95% 초과)를 보이고 클러스터링 계수(6장 참조)가 동일하면 같은 제품이므로 중복 제거한다. 그 결과 지식 그래프로 다중 데이터 소스를 정렬하는 데이터 소유자들은 현저히 개선된 매칭 률을 얻는다.

Note — 조직화 원리를 얹으면 검증이 가능해진다

데이터의 품질과 정확성을 높이고 전사 상호운용성을 키우려면, 통합된 데이터 위에 온톨로지·택소노미·전사 정준 모델(enterprise canonical model) 같은 조직화 원리를 오버레이하여 지식 그래프로 소비자에게 제시할 수 있다. 조직화 원리는 시스템 간 데이터 검증과 불일치·시맨틱스 위반의 탐지를 가능하게 한다.

The Database Driver

데이터베이스 드라이버 — 가장 흔한 통합의 관문

지식 그래프와 통합하는 가장 흔한 방법은 드라이버다. 드라이버는 애플리케이션 코드 아래에 위치해 지식 그래프로 네트워크 호출을 수행하는 클라이언트 사이드 미들웨어다. 사실 이 책에서 이미 드라이버를 보았다 — Neo4j Browser가 데이터베이스에 직접 연결된 것처럼 보이지만 그렇지 않다. Browser는 연결 문자열로 (JavaScript로 작성된) Neo4j 드라이버 인스턴스를 만들고, 그 드라이버가 (로컬 네트워크 인터페이스일지라도) 네트워크를 거쳐 지식 그래프를 호스팅하는 Neo4j 서버와 통신한다. 즉 클라이언트-서버 아키텍처다.

Sidebar — Neo4j 드라이버 목록

공식 드라이버: Java · Spring Framework와 선택적 Object-Graph Mapper(Java) · .NET · JavaScript · Python · Go
커뮤니티 드라이버: Ruby · PHP · Erlang/Elixir · Perl · C/C++ · Clojure · Haskell · R · Py2Neo Object-Graph Mapper(Python)

선호 언어(또는 가장 가까운 언어 — 예: 다른 JVM 언어에는 Java 드라이버)의 드라이버를 고르면 된다. 모든 드라이버는 같은 기능을 제공한다 — Bolt 프로토콜 또는 HTTP로 애플리케이션 코드를 Neo4j(서버 클러스터와 AuraDB 클라우드 서비스 포함)에 연결한다.

Example 5-1 · Neo4j Java 드라이버 사용 (타 언어도 유사 패턴)Java
import org.neo4j.driver.*;
import static org.neo4j.driver.Values.parameters;

public class JavaDriverExample implements AutoCloseable {
    // 드라이버 인스턴스는 꽤 비싸다.
    // 애플리케이션 수명 전체에 하나만 두는 것이 보통이다.
    private final Driver driver;

    public JavaDriverExample(String uri, String user, String password) {
        // 1. 드라이버 객체는 비싸다
        driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password));
    }

    public Result findFriends(final String name) {
        // 2. 세션은 싸다. 필요할 때마다 쓰면 된다.
        try (Session session = driver.session()) {
            // 3. 질의가 그래프를 갱신하는지 여부를 드라이버에 알리면
            //    클러스터의 라우팅 결정이 좋아진다. writeTransaction도 있다.
            return session.readTransaction(tx -> {
                Result result = tx.run("MATCH (a:Person)-[:FRIEND]->(b:Person) " +
                                "WHERE a.name = $name " + // 4. 매개변수가 좋다
                                "RETURN b.name",
                        parameters("name", name));
                return result;
            });
        }
    }

    public static void main(String... args) throws Exception {
        try (JavaDriverExample example =
                new JavaDriverExample("bolt://localhost:7687", "neo4j", "password")) {
            example.findFriends("Rosa");
        }
    }
}

어느 Neo4j 드라이버를 쓰든 기억해야 할 네 가지가 있다.

① 드라이버 객체는 비싸다

지식 그래프에 연결하는 애플리케이션의 수명 동안 하나만 둔다. 인증을 포함한 네트워크 연결의 설정·해제를 다루므로 비싸며, 질의마다 그 비용을 치르고 싶지는 않을 것이다.

② 세션 객체는 싸다

꽤 정교하기도 하다 — 클라이언트 처리를 매끄럽게 하는 다양한 비동기 API를 포함하고, 인과적 경계(북마크)를 지원해 전 지구적으로 분산된 클러스터에서도 클라이언트가 언제나 최소한 자신의 쓰기를 보게 한다.

③ 읽기 전용/읽기·쓰기를 서버에 알려라

클러스터가 높은 처리량을 유지하도록 질의를 어디서 처리할지 더 나은 라우팅 결정을 내리는 데 도움이 된다.

④ 모든 질의를 매개변수화하라

매개변수화된 질의는 재파싱이나 새 질의 계획 생성이 필요 없어 리터럴 문자열보다 빠르다. {name:'Karl'} 대신 {name: $name}을 쓰고 매개변수를 따로 보내라. 인젝션 공격 방어에도 도움이 된다.

Note — Neo4j 5의 자동 매개변수화

Neo4j 5부터 Cypher는 그렇게 작성되지 않은 질의도 자동으로 매개변수화하려고 시도한다. 그래도 파서가 못 잡을 경우를 대비해 모든 매개변수를 직접 식별하는 것이 여전히 좋은 실천이다.

Graph Federation

복합 데이터베이스로 그래프 연합하기

Neo4j Composite Databases는 여러 그래프 데이터 소스를 한데 모아 단일한 통합 접근점을 제공한다. 연합(federation)이라 불리는 이 기법은 데이터베이스를 물리적으로 결합하지 않고 가상의 복합체를 만든다. 소비자 애플리케이션은 데이터가 여러 소스에서 왔다는 사실을 모른다.

시나리오: 독립적인 그래프 데이터베이스 셋 — 하나는 제품 카탈로그, 둘은 판매 정보다. 두 판매 데이터베이스는 같은 그래프 데이터 모델을 쓰지만, 하나는 EMEA(유럽·중동·아프리카) 고객 주문을, 다른 하나는 APAC(아시아-태평양) 고객 주문을 담는다.

Example 5-2 · 논리 DB globalsales 구성Cypher
# Cypher shell 또는 Neo4j Browser에서 한 줄씩
CREATE DATABASE db1;
CREATE DATABASE db2;

CREATE COMPOSITE DATABASE globalsales;

CREATE ALIAS emeasales FOR DATABASE db1;
CREATE ALIAS apacsales FOR DATABASE db2;
Example 5-3 · 10만 건 초과 주문 고객 (단일 질의 연합)Cypher
UNWIND ['globalsales.apac', 'globalsales.emea'] AS g
CALL {
  USE graph.byName(g)
  MATCH (c:Customer)-[:PURCHASED]->()-[o:ORDERS]->(:Product)
  WITH c, sum(o.quantity * o.unitPrice) AS totalOrdered
  WHERE totalOrdered > 100000
  RETURN c.customerID AS name,
         c.country AS country, totalOrdered
}
RETURN name, country, totalOrdered

Example 5-3은 분리된 기저 그래프들에서 EMEA와 APAC 판매를 연합하여 소비 애플리케이션에 집계된 뷰를 제공한다. 제품 카탈로그까지 포함하도록 쉽게 확장할 수 있다 — 특정 범주(예: Beverages) 제품을 구매한 글로벌 고객 목록을 반환하는 식이다.

Example 5-4 · 크로스-그래프 질의: 음료를 구매한 글로벌 고객Cypher
CALL {
  USE globalsales.catalog
  MATCH (p:Product{discontinued:false})-[:PART_OF]->(:Category{categoryName:'Beverages'})
    RETURN COLLECT(p.productID) AS pids
}
WITH pids, [g IN fabricnw.graphIds()] AS gids
UNWIND gids AS gid
CALL {
    USE globalsales.graph(gid)
        MATCH (c:Customer)-[:PURCHASED]->()-[o:ORDERS]->(p:Product)
    WHERE p.productID
    RETURN distinct c.customerID AS name, c.country country, p.productName AS product
}
RETURN name, country, product

그래프 자체를 제외하면 Example 5-4에서 가장 설득력 있는 점은 구현 복잡도가 사용자로부터 추상화되어 있다는 것이다. (최소) 두 개의 기저 그래프 데이터베이스가 하나의 가상 데이터베이스로 합성되는 일은 Neo4j Fabric이 처리하고, 개발자는 도메인에만 집중하면 된다.

Server-Side Procedures

서버 사이드 프로시저 — 복사 없이 질의 시점에 보강한다

Cypher와 Neo4j 드라이버를 쓰는 클라이언트-서버 상호작용이 일반적이지만, 그 패러다임만으로 충족되지 않는 요구도 있다 — SQL 데이터베이스의 저장 프로시저처럼, 특정 로직이 지식 그래프 가까이 서버에서 실행되어야 하는 경우다. Neo4j는 사용자·역할 관리, 스키마 시각화, (6장에서 볼) 데이터 과학 연산 등을 위한 훌륭한 내장 함수·프로시저 라이브러리를 제공하며, 4장에서 본 APOC에는 흔한(그리고 흔치 않은) 경우를 돕는 수많은 함수·프로시저가 있다.

지식 그래프는 통합될 때 더 좋아진다 — 그렇다면 APOC으로 SQL 데이터베이스를 호출해 질의 시점에 지식 그래프 데이터를 보강하면 어떨까. Cypher 질의의 일부로 SQL 데이터베이스에서 데이터를 추출해 지식 그래프 데이터와 섞을 수 있다. 복사는 필요 없다.

Example 5-5 · 프로시저에서 SQL 데이터베이스 호출Cypher
CALL apoc.load.driver("com.mysql.jdbc.Driver");

WITH "select firstname, lastname from employees
      where firstname like ? and lastname like ?" AS sql

CALL apoc.load.jdbcParams("northwind", sql, ['F%', '%w'])
YIELD row

MATCH (row)-[:WORKS_FOR*1..3]->(boss:Person)
RETURN (boss)

이 코드는 SQL 드라이버를 Neo4j에 적재하고 northwind라는 SQL 데이터베이스에 접근한다. SQL 질의로 직원의 이름·성을 가져와 Cypher의 노드로 반환하고, 그 노드를 보통의 Cypher 질의에 섞는다 — 여기서는 SQL에서 온 사람이 지식 그래프에서 누구를 위해 일하는지 최대 세 단계 위까지 찾는다.

Note — rip-and-replace는 필요 없다, 그러나 지연은 숨길 수 없다

프로시저를 쓰면 소스 시스템에서 데이터를 옮기거나 복사하지 않고도 질의 시점에 다른 데이터베이스의 데이터로 지식 그래프를 보강할 수 있다. 다만 원격 호출의 지연시간(과 신뢰성)이 지식 그래프 질의의 지연에 더해진다는 점은 고려해야 한다.

Example 5-6 · MongoDB 접근 (실험 오염물 탐색)Cypher
CALL apoc.mongodb.find('mongodb://mongo:neo4j@mongo:27017',
    'results', '2022-02-22-wetlab',
    {`status`: "failed"}, null, null)
YIELD failed_experiments
MATCH (:Experiment { id, failed_experiments.id})-[*1..2]->(environmental_factors)
RETURN environmental_factors.contaminants AS contaminants
Example 5-7 · JSON 웹 API 호출 → MERGE 보강Cypher
WITH "https://example.org/karl.json" AS url
CALL apoc.load.json(url) YIELD value
UNWIND value.products AS product

WITH product

MATCH (k:Person {name:'Karl'})
MERGE (k)-[:BOUGHT]->(:Product {name:product.name,
    price:product.price,
    description:product.description})

Example 5-6은 (그 자체로는 지식 그래프 능력이 없는) MongoDB에서 데이터를 추출하는 APOC 상호운용 프로시저다. CALL이 원격 데이터베이스 연결을 처리하고 MongoDB 질의 결과를 Cypher가 이어서 처리하기 좋은 형태로 돌려준다 — MongoDB 문서의 id를 지식 그래프의 노드에 MATCH하고, 매칭된 실험 노드에서 깊이 1~2로 탐색해 환경 오염물을 찾아 반환한다. Example 5-7은 파일이나 웹 페이지(예: Amazon S3의 파일)의 JSON을 적재하는 단순한 해법으로, JSON 데이터를 이용해 제품을 구매한 사람들로 기저 그래프를 MERGE 보강한다.

Data Virtualization with APOC

데이터 가상화 — 가상 노드로 통신망 디지털 트윈 만들기

기저 그래프 데이터베이스들을 하나의 가상 지식 그래프로 결합하는 호사를 누리지 못할 때도 있다. 기저 데이터가 시계열, 클릭스트림, 로그 같은 비그래프 소스에 있다면 다른 접근이 필요하다. 다행히 APOC은 가상 리소스(virtual resource) 카탈로그의 정의를 지원한다. 가상 리소스는 Neo4j가 필요할 때 질의·조회할 수 있는 외부 데이터 소스로, 그래프에 저장된 데이터를 보강하는 가상 노드로 제시된다. 질의가 실행되면 APOC이 CSV 파일과 관계형 데이터베이스에서 데이터를 가져와 가상 노드·관계로 질의 엔진에 제시하고, Neo4j의 네이티브 그래프 데이터와 섞는다.

Note — 지연은 숨길 수 없다, 신중하게 써라

CSV 파일이나 타 데이터베이스의 데이터 접근은 대개 로컬 지식 그래프 접근보다 지연이 크다. 이 접근법의 가치는 그래프와 비그래프 데이터의 차이를 숨기고, 다른 데이터의 사본을 만들지 않고도 지식 그래프를 매끄럽게 보강하게 해 준다는 데 있다. 그러나 지연 자체는 숨길 수 없으니 분별 있게 사용하라.

사례 — 통신망 디지털 트윈

이 접근으로 통신망의 디지털 트윈을 만들 수 있다. 그래프는 물리·논리·가상화 장치와 그 연결을 모두 담은 네트워크의 표현을 갖는다.

Example 5-8 · 디지털 트윈 토폴로지 구축Cypher
// 네트워크 토폴로지 생성 — 장치 추가
MERGE (nsm:NetworkDevice { name: "Nogent sur Marne",
  nd_id: "N08",
  pos: point({latitude:2.46995, longitude:48.834374}),
  gtype: 1, code: "nog-1" });
MERGE (ms:NetworkDevice { name: "Montrouge Sud",
  nd_id: "N11", …, code: "mon-3"});
// 장치 간 링크 추가
MATCH (nsm:NetworkDevice { code: "nog-1" })
MATCH (ms:NetworkDevice { code: "mon-3" })
MERGE (nsm)-[l:LINK]->(ms)
SET l= { linkId: "s1_385/ol", bundleId: "s1_128/ob",
  linkType: "OTN Line", capacity: 1920 };
Example 5-9 · 장애 링크의 양 끝점 조회 + Example 5-10 · 24시간 성능 SQLCypher · SQL
MATCH (aend:NetworkDevice)-[l:LINK]->(zend:NetworkDevice)
WHERE l.linkId = "s1_385/ol"
RETURN aend.code, zend.code

-- 성능 메트릭은 그래프 밖(급변하는 하드웨어 카운터)에 둔다
SELECT latency, packetloss, packetdupl, jitter, throughput
FROM device_perf
    WHERE device_id = $device_id
          AND timestamp > now() - interval '1 day';

이 정적인 뷰만으로도 쓸모가 있다 — 단순 근본 원인 분석은 링크 장애를 양 끝점의 최근 성능 메트릭과 상관시키는 것이다. 기능적으로 완전한 시스템이 되려면 장치별 실시간 성능 메트릭으로 모델을 보강해야 하는데, 이 메트릭은 그래프에 없고, 물리 통신망 하드웨어의 급변하는 카운터이므로 그래프에 넣지 않는 것이 맞다. 여기서 가상화 리소스를 만든다 — apoc.dv.catalog.add 저장 프로시저를 호출하고 실행할 SQL 질의를 지정한다.

Example 5-11 · 가상화 리소스 생성Cypher
CALL apoc.dv.catalog.add("metrics-by-device-id", {
  type: "JDBC",
  url: "jdbc:postgresql://localhost/metrics_collector
        ?user=bob&password=bobby",
  labels: ["Town","PopulatedPlace"],
  query: "SELECT latency, packetloss, packetdupl,
             jitter, throughput
             FROM device_perf
             where device_id = $device_id
                   and timestamp > now() - interval '1 day';",
  desc: "french towns by department number"
})
Example 5-12 · 가상화 리소스 질의 (토폴로지 + 온디맨드 메트릭)Cypher
MATCH (aend:NetworkDevice)-[l:LINK]->(zend:NetworkDevice)
WHERE l.linkId = "s1_385/ol"
WITH aend, zend
CALL apoc.dv.query("metrics-by-device-id",
    {device_id: aend.nd_id}) YIELD node
RETURN aend.code, properties(node), zend.code

질의 결과는 그래프의 데이터(토폴로지 정보)와 성능 메트릭 데이터베이스에서 온디맨드로 가져온 데이터를 결합한다. 지식 그래프가 논리적 통합 계층(데이터 패브릭)으로 동작하여 사용자와 소비 앱에 네트워크와 실시간 메트릭의 통합 뷰 — 디지털 트윈 — 를 제공한다. 개발자는 기술 구현을 알지만, 사용자는 투명하게 최신인 데이터를 갖춘 단일한 전체론적 지식 그래프만 본다.

Sidebar — 전문 데이터 가상화 플랫폼과의 조합

Dremio, Denodo, Data Virtuality 같은 전문 플랫폼은 데이터 가상화 위에 더 넓은 데이터 패브릭을 배치하는 역량을 제공한다 — 다중 데이터 소스 커넥터, 고급 질의 위임, 캐싱, 논리 뷰 설계 GUI 등이다. 그런 전문 데이터 패브릭 기술과 함께 배치될 때, 지식 그래프는 골든 레코드·중복 제거 등을 위해 패브릭에 통합되는 하나의 마스터 데이터 소스로 동작한다.

Custom Functions & Procedures

커스텀 함수와 프로시저 — Cypher는 확장 가능하다

내장 프로시저와 APOC 라이브러리는 광범위하지만, 때로는 어떤 라이브러리도 다루지 않는 고도로 도메인 특화된 방식으로 지식 그래프를 다뤄야 한다. 다행히 Cypher는 확장 가능하다 — 사용자 정의 함수와 사용자 정의 프로시저를 모두 허용하므로, 자신의 까다로운 요구에 맞는 것을 직접 만들 수 있다.

아키텍처 관점: 보통의 Cypher는 Cypher 런타임에서 커널(저수준) API에 대해 실행되지만, 사용자 정의 함수·프로시저는 더 친절한 추상화 수준인 Neo4j Java API로 정의한다. 올바른 애너테이션으로 장식하고 그에 함의된 인터페이스를 구현하면 임의의 기능을 가진 프로시저·함수를 만들 수 있다 — 지식 그래프를 검사하고, 계산하고, 갱신까지 하는 프로시저도 가능하다.

Note — SQL 저장 프로시저의 오명은 없다

Neo4j의 사용자 정의 함수·프로시저는 SQL의 저장 프로시저 같은 오명(stigma)을 지지 않는다. JVM(Java, Scala 등) 코드를 작성하며, 데이터베이스와 독립적으로 테스트할 수 있다. 솔루션 전체에 쓰는 것과 같은 버전 관리·빌드 실천을 채택할 수 있어 커스텀 코드가 견고해진다.

Example 5-13 · 노드 차수 히스토그램을 만드는 커스텀 프로시저Java
public class GetNodeDegrees {

    @Context   // DBMS API 참조가 실행 전에 주입된다
    public GraphDatabaseService graphDatabaseService;

    @Procedure(value = "getNodeDegrees")   // 진입점과 친숙한 이름
    @Description("Get degrees of all nodes in the knowledge graph")
    public Stream<NodeDegree> getNodeDegrees() {

        Map<Long, Long> nodeDegrees = new TreeMap<>(Long::compare);

        graphDatabaseService.getAllNodes().forEach(n -> {
            final long nodeDegree = n.getDegree();
            long numberOfNodeWithCurrentDegree =
                                  nodeDegrees.getOrDefault(nodeDegree, 0L);
            nodeDegrees.put(nodeDegree, numberOfNodeWithCurrentDegree + 1);
        });

        final Stream.Builder<NodeDegree> streamBuilder = Stream.builder();
        for (Map.Entry<Long, Long> entry : nodeDegrees.entrySet()) {
            streamBuilder.add(new NodeDegree(entry.getKey(), entry.getValue()));
        }
        return streamBuilder.build();   // 레코드 타입의 Stream 반환은 필수
    }

    public class NodeDegree {
        public Long degree;
        public Long numberOfNodes;
        public NodeDegree(Long key, Long value) { degree = key; numberOfNodes = value; }
    }
}

코드 대부분은 노드에 부착된 관계를 세는 로직이고, 흥미로운 부분은 프로시저가 기저 지식 그래프에 바인딩되는 지점이다 — @Context 애너테이션과 public GraphDatabaseService 필드가 DBMS API 참조 주입을 보장하고, @Procedure가 친숙한 이름의 진입점을, @Description이 설명을 선언하며, 레코드 타입의 Stream 반환은 필수다.

Example 5-7에서 웹·파일시스템의 JSON을 읽어 그래프를 보강했지만, 대부분의 API는 그보다 복잡하다. Example 5-13과 같은 패턴으로 서비스 API에 접근하는 커스텀 프로시저도 작성할 수 있다 — 커스텀 프로시저는 Neo4j 프로시저 인터페이스에 부합하기만 하면 어떤 코드든 될 수 있다.

Note — 글로벌 vs 로컬 프로시저

Example 5-13의 프로시저는 지식 그래프의 모든 노드(와 관계)를 건드리는 글로벌 프로시저다. 특정 노드에서 시작하는 프로시저를 쓰려면 GraphDatabaseService 주입 대신 프로시저 메서드에 노드·관계 매개변수를 두면 된다 — 예: public Stream<RelationshipTypes> getRelationshipTypes(@Name("currentNode") Node node) {...}

Complementary Tools — GraphQL

GraphQL — 지식 그래프를 위한 API 프론트

드라이버·함수·프로시저 외에도, 주변 시스템에 따라 다른 통합 도구가 필요할 수 있다 — 광범위한 사용자 대면 앱에서 소비할 API, 업스트림 데이터로 그래프를 보강하는 ETL, 분석 시스템과의 데이터 이동, 실시간 스트리밍 등이다. 각 요구에 대응하는 풍부한 도구가 존재한다.

GraphQL은 Facebook에서 시작된 API 툴킷이다. 원래 목적은 TAO(Facebook의 소셜 그래프 서비스)에 저장된 소셜 그래프의 API 제공이었다. 오늘날 GraphQL은 API 구축 프레임워크(타입·스키마·메시지 등을 정의)이자 API 호출을 실행하는 런타임 환경(GraphQL 서버)이다. 이름에 "그래프"가 들어 있고 지식 그래프 노출에 확실히 유용하지만, 비그래프 시스템에도 무리 없이 쓸 수 있는 범용 툴킷이다. 기본 아키텍처는 단순한 클라이언트-서버 구성이다.

네이티브 그래프 데이터베이스의 지식 그래프에 GraphQL로 접근하면 일관된 그래프 데이터 모델과 — 특히 GraphQL이 전사적으로 널리 쓰일 때 — 개발자 생산성 향상이라는 이점이 있다. 이름과 달리 GraphQL에는 지식 그래프의 토폴로지를 다루는 내장 장치가 없지만, Neo4j의 GraphQL 구현에서는 @relationship 지시자로 노드 간 관계를 기술할 수 있다.

Example 5-14 · @relationship으로 레코드 간 링크를 포함한 타입 시스템GraphQL
type Place {
    city: String!
    country: String
    people: [Person!]! @relationship(type: "LIVES_IN", direction: IN)
}

type Person {
    name: String!
    home: Place! @relationship(type: "LIVES_IN", direction: OUT)
}

@relationship은 단순한 문서 스키마를 지식 그래프로 저장할 수 있는 능력으로 보강한다. Place는 city·country와 함께 LIVES_IN 관계로 연결된 non-nullable Person 배열을 갖고, 역으로 Person은 name과 Place로의 non-nullable LIVES_IN 관계를 갖는다. 이 스키마는 기저 지식 그래프에 깔끔하게 매핑되며, 중요하게는 API로 조회 가능한 데이터의 성격에 대해 호출자에게 엄격한 안내를 제공한다.

Example 5-15 · GraphQL 뮤테이션 (그래프 갱신)GraphQL
mutation {
  createPlaces(
    input: {
      city: "Sydney"
      country: "Australia"
      people: {
        create: [
           { node: { name: "Skippy" } }
           { node: { name: "Cate Blanchet" } }
        ]
      }
    }
  ) {
    places { city country people { name } }
  }
}
Example 5-16 · 서버 응답JSON
{
  "data": {
    "createPlaces": {
      "places": [
        {
          "city": "Sydney",
          "country": "Australia",
          "people": [
            { "name": "Skippy" },
            { "name": "Cate Blanchet" }
          ]
        }
      ]
    }
  }
}

API 기술(記述)·구축 프레임워크로서 GraphQL은 지식 그래프와 매우 잘 맞으며, 특히 지식 그래프가 시스템 아키텍처에서 사용자 가까이 자리할 때 그렇다. 지식 그래프와 GraphQL 통합의 심화서로는 Full Stack GraphQL Applications with React, Node.js, and Neo4j(William Lyon, Manning)가 있다.

Kafka · Spark · Apache Hop

스트리밍과 ETL — 흐르는 데이터의 파이프라인

Kafka Connect 플러그인 — 지식 그래프를 소스이자 싱크로

Apache Kafka는 시스템들 사이의 접착제 역할을 하는 인기 엔터프라이즈 미들웨어 플랫폼이다. 단순하게는 대용량 메시지를 옮기는 신뢰성 있는 발행/구독(pub/sub) 시스템이고, 정교하게는 비즈니스를 흐르는 레코드를 처리하는 데이터베이스이자 질의 엔진이다. Kafka Connect Neo4j Connector를 쓰면 지식 그래프가 (변경 데이터 캡처 같은) 데이터 소스도, Kafka 이벤트를 받아들이는 싱크도 될 수 있다.

소스 방향에서는 지식 그래프 위에서 Cypher 질의가 주기적으로 실행되어 결과를 Kafka로 발행하고, 다운스트림 시스템이 소비한다. 설정 예: 베를린 신규 전입자의 상세를 5초(5,000ms)마다 발행한다. 이를 위해 3장의 LIVES_IN 관계에 있던 since 속성을 모델과 발행 메커니즘 모두를 충족하는 더 세밀한 타임스탬프로 바꾼다.

Example 5-17 · 소스 커넥터 설정 (주기 발행)JSON
{
  "name": "Neo4jSourceConnectorAVRO",
  "config": {
    "topic": "new-arrivals-in-Berlin",
    …
    "neo4j.streaming.poll.interval.msecs": 5000,
    "neo4j.streaming.property": "timestamp",
    "neo4j.streaming.from": "NOW",
    "neo4j.enforce.schema": true,
    "neo4j.source.query":
      "MATCH (p:Person)-[li:LIVES_IN]->
        (:Place {city:'Berlin', country:'DE'})
       WHERE li.timestamp > $lastCheck
       RETURN p.name AS name,
              li.timestamp AS since"
  }
}
Example 5-18 · 싱크 커넥터 설정 (토픽 → MERGE)JSON
{
  "name": "Neo4jSinkConnector",
  "config": {
    "topics": "new-arrivals-in-Berlin",
    …
    "neo4j.topic.cypher.new-arrivals-in-Berlin":
      "MERGE (person:Person {name: event.name})
       MERGE (place:Place {city: event.city,
                           country: event.country})
       MERGE (person)-[LIVES_IN
         {since:'2021-06-12T10:31:11.553+0100'}]->(place)"
  }
}

싱크 방향도 아키텍처·실무 모두 똑같이 직관적이다 — 특정 토픽(예: new-arrivals-in-Berlin)을 구독하면 발행된 메시지의 데이터에 접근할 수 있고, event 변수로 메시지 데이터에 접근하여 MERGE 같은 Cypher 기능으로 지식 그래프를 보강한다. Kafka 설치와 Streams 플러그인 설치 같은 운영 세부는 Neo4j Kafka 문서가 도움이 된다.

Neo4j Spark Connector — 테이블 세계와 그래프 세계의 배관

Apache Spark는 대용량 데이터셋을 다루는 (분산) 데이터 처리 프레임워크다. 커넥터를 쓰면 Spark가 지식 그래프를 소스이자 싱크로 다룰 수 있다 — 파이프라인에서 그래프 데이터를 읽어 들이고, 계산 결과를 그래프에 되쓴다. 설치는 적절한 .jar 파일을 Spark 환경에 복사하면 끝이다. 가장 단순한 연산은 레이블과 술어(predicate)에 의한 읽기·쓰기다(술어 where를 생략해도 되지만 Spark와 Neo4j 사이에 더 많은 데이터가 오간다).

Example 5-19 · 레이블+술어로 읽기Scala
import org.apache.spark.sql.{SaveMode, SparkSession}

val spark = SparkSession.builder().getOrCreate()

val df = spark.read.format("org.neo4j.spark.DataSource")
  .option("url", "bolt://localhost:7687")
  .option("authentication.basic.username", "neo4j")
  .option("authentication.basic.password", "neo4j")
  .option("labels", ":Person")
  .load()

df.where("name = 'John Doe'").where("age = 32").show()
Example 5-20 · Cypher 질의로 읽기Scala
val spark = SparkSession.builder().getOrCreate()

spark.read.format("org.neo4j.spark.DataSource")
  .option("url", "bolt://localhost:7687")
  .option("query", "MATCH (n:Person)-[FOLLOWS]->
                    (:Person name:'emileifrem')
                    WITH n LIMIT 20
                    RETURN id(n) AS id, n.name AS name")
  .load()
  .show()
// 전체 노드·관계 대신 데이터 값만 반환하는 것이
// 전송 작업을 최소화하는 좋은 실천이다
Example 5-21 · 레이블로 쓰기Scala
case class Person(name: String, surname: String, age: Int)

val ds = ( /* 데이터 소스 채우기 */ ).toDS()

ds.write
  .format("org.neo4j.spark.DataSource")
  .mode(SaveMode.ErrorIfExists)
  .option("url", "bolt://localhost:7687")
  .option("labels", ":Person")
  .save()
Example 5-22 · Cypher 패턴으로 관계 쓰기Scala
df.write
  .format("org.neo4j.spark.DataSource")
  .option("url", "bolt://second.host.com:7687")
  .option("relationship", "FOLLOWS")
  .option("relationship.source.labels", ":Person")
  .option("relationship.source.save.mode", "Overwrite")
  .option("relationship.source.node.keys", "source.name:name")
  .option("relationship.target.labels", ":Person")
  .option("relationship.target.save.mode", "Overwrite")
  .option("relationship.target.node.keys", "target.name:name")
  .save()
Example 5-23 · Spark 쓰기가 Neo4j에서 실행되는 Cypher로 변환된 결과Cypher
UNWIND $events AS event
MERGE (source:Person {name: event.source.name})
SET source = event.source
MERGE (target:Product {name: event.target.name})
SET target = event.target
CREATE (source)-[rel:FOLLOWS]->(target)
SET rel += event.rel
Note — SaveMode가 CREATE vs MERGE를 결정한다

Spark에서 지식 그래프로 쓸 때는 중복 패턴을 허용하는 CREATE와 허용하지 않는 MERGE 중 무엇을 쓸지 안내를 제공해야 한다 — SaveMode.ErrorIfExistsCREATE 질의를, SaveMode.OverwriteMERGE 질의를 만든다. Spark는 강력하지만 그 데이터 모델은 지식 그래프와 상당히 다르다 — 테이블 모델과 그래프 사이의 매핑을 직접 관리해야 하며, Spark 커넥터는 두 세계 사이의 변환을 지정하게 해 주는 배관(plumbing), 즉 더 낮은 텍스트 추상화 수준의 전통적 ETL 도구로 생각하면 된다.

Apache Hop — 범용 ETL과 벨기에 맥주 지식 그래프

Spark 커넥터가 특수 목적 ETL이라면 범용 ETL 도구도 알아 둘 만하다. 현대 ETL 도구는 로컬 설치 유틸리티부터 클라우드 호스팅 서비스까지 다양하며, 그 가치는 (지식 그래프나 데이터베이스에 국한되지 않는) 광범위한 시스템과의 연결성, 그리고 시스템 간 데이터 요소 매핑의 편의성에 있다. 대부분 로우코드/노코드 환경으로 지식 그래프(및 기타 시스템)와의 데이터 흐름을 오케스트레이션하게 해 준다.

Apache Hop의 예제 워크플로(벨기에 맥주 데이터 적재)는 이렇게 구성된다 — ① 시스템 가용성 확인, ② 지식 그래프 인덱스·제약 조건 설정, ③ Wikipedia 임포트 데이터 정제, ④ 지식 그래프로의 임포트 잡 실행. 워크플로 안에서 소스 시스템에서 지식 그래프로의(또는 반대의) 매핑을 구성하는데, 이 역시 로우코드/노코드다. 매핑은 직관적이다(양조를 이해한다면!) — 브랜드·양조장 등은 노드가 되고, 도수(percentage) 레이블은 강도(strength)와 평가(evaluation) 노드 사이의 관계가 된다. 이 작업에는 약간의 도메인 지식이 필요하지만(저자들은 벨기에 맥주 시음이 재미있는 학습 프로젝트가 되리라 여긴다) 기술 지식은 많이 필요 없다 — 포인트 앤드 클릭이면 된다.

더 정교한 매핑에서는 지식 그래프 유입·유출용 Cypher 질의를 직접 지정할 수도 있다 — 매개변수화된 Cypher를 Hop에 제공하고 UI 도구로 그 매개변수에 데이터를 매핑하면, 런타임에 Hop이 값을 바인딩하고 Cypher를 실행해 데이터를 적재·조회한다. 워크플로가 끝나면 벨기에 맥주에 관한 지식 그래프가 완성된다 — 유용하고도 맛있다!

Note — ETL 도구 선택은 불가지론적으로

저자들은 어떤 ETL 도구를 고르든 불가지론적이다. 이 절에서 Apache Hop을 고른 이유는 오픈소스이고 Neo4j 호스팅 지식 그래프와의 연결성이 좋기 때문이다. 권고는 기업에서 이미 선택된 ETL 도구를 쓰라는 것이며, 전사 표준이 없다면 오픈소스나 클라우드 도구(AWS Glue, Cloud ETL 등)가 합리적 선택이다.

Summary

요약 — 패브릭의 심장으로서의 지식 그래프

여기까지 그래프 데이터베이스 기초, 데이터 적재, 데이터 패브릭이라는 중대한 기술 주제를 다뤘다. 이 도구들과 그 중첩을 이해하는 데는 진짜 노력이 든다.

기법방향핵심 용도
드라이버 (Bolt/HTTP)앱 ↔ KG가장 흔한 통합. 드라이버 1개·세션 다수·읽기/쓰기 명시·매개변수화
Composite DatabasesKG ↔ KG여러 그래프의 가상 연합. USE graph.byName으로 단일 질의
APOC 프로시저KG ← SQL/MongoDB/JSON복사 없이 질의 시점 보강. 원격 지연은 감수
APOC 데이터 가상화KG ← 비그래프 소스apoc.dv 카탈로그의 가상 노드. 디지털 트윈
커스텀 프로시저서버 내부Java API + 애너테이션으로 임의 기능. 독립 테스트 가능
GraphQL사용자 앱 → KG@relationship로 타입 시스템에 관계 표현. 사용자 근접 API
Kafka ConnectKG ↔ 스트림소스(주기 발행·CDC)와 싱크(토픽 → MERGE)
Spark Connector / HopKG ↔ 분석·ETLSaveMode가 CREATE/MERGE 결정. 범용 ETL은 로우코드 매핑
1

데이터 패브릭의 심장은 지식 그래프다. 바이트 전송은 전통적 통합이 맡고, 정보 모델은 사일로 위의 큐레이션 인덱스인 지식 그래프로 수렴한다. 조직화 원리를 얹으면 시스템 간 검증까지 가능해진다.

2

드라이버 4원칙. 드라이버는 비싸니 하나만, 세션은 싸니 자유롭게(북마크 지원), 읽기/쓰기를 명시해 라우팅을 돕고, 모든 질의를 매개변수화하라(성능 + 인젝션 방어).

3

이동 없는 보강. 연합(Composite), APOC 원격 호출, 가상 리소스는 모두 데이터 사본 없이 지식 그래프를 확장한다 — 단 원격 지연은 숨길 수 없으니 분별 있게.

4

Cypher는 확장 가능하다. JVM 코드로 커스텀 함수·프로시저를 만들고 일반 소프트웨어처럼 테스트·버전 관리하라. @Context 주입이면 글로벌, 매개변수 바인딩이면 로컬 프로시저다.

5

흐름의 도구들. GraphQL은 사용자 근접 API, Kafka는 실시간 소스/싱크, Spark는 테이블-그래프 배관, 범용 ETL은 로우코드 오케스트레이션 — 주변 시스템이 도구를 결정한다.

Next — 6장 예고

여기서부터는 도구를 벗어나 스택 위쪽, 사용자를 향해 여정을 계속한다. 6장에서는 그래프 알고리즘으로의 첫걸음을 내디뎌, 지식 그래프를 처리하여 실행 가능한 인사이트를 얻는 법 — 그래프 데이터 과학(Graph Data Science) — 을 배운다.