콘텐츠로 이동
로봇 작업자들이 GraphOperations, 스키마 DSL, 트랜잭션, 코루틴 실행 모델 블록을 조립하는 3D 작업대 일러스트
공통 API의 목적은 데이터베이스의 차이를 숨기는 것이 아니라 반복 코드를 줄이는 데 있습니다.

Part 1에서는 상황에 맞는 그래프 데이터베이스를 선택하는 기준을 살펴봤습니다. 이번에는 그 위에 놓이는 graph-core를 설명합니다. 서비스 코드는 정점, 간선, 탐색 같은 그래프 개념으로 작성하고, 드라이버와 쿼리의 차이는 그래프 저장소 어댑터가 맡습니다. 이 경계가 없으면 서비스마다 드라이버 호출, 입력 검증, 스키마 초기화 코드가 서로 다른 형태로 누적됩니다.

도메인 서비스에서 GraphOperations, 스키마 DSL, 실행 모델, 그래프 저장소 어댑터로 이어지는 핵심 API 실행 흐름
서비스는 그래프 개념으로 코드를 작성하고, 저장소 어댑터는 실제 쿼리와 드라이버의 차이를 처리합니다.

GraphOperations는 서비스에서 자주 사용하는 그래프 작업을 하나의 퍼사드로 묶습니다. GraphSession, GraphVertexRepository, GraphEdgeRepository, GraphGenericRepository를 상속하며, GraphGenericRepository는 탐색과 알고리즘 저장소 계약을 결합합니다.

영역대표 메소드서비스 코드에서 하는 일
세션createGraph, dropGraph, graphExists테스트 픽스처나 테넌트별 그래프 생명주기를 관리합니다.
정점createVertex, createVertices, findVertexById, findVerticesByLabel, updateVertex, deleteVertex, countVertices사용자, 상품, 문서 같은 노드를 만들고 조회합니다.
간선createEdge, createEdges, findEdgesByLabel, findEdgesByStartId, findEdgesByEndId, deleteEdgeKNOWS, PURCHASED, MENTIONS 같은 관계를 저장하고 추적합니다.
탐색neighbors, shortestPath, allPaths, aStarPath공유 식별 정보, 친구 추천, 경로 탐색 같은 그래프 쿼리를 표현합니다.
알고리즘pageRank, degreeCentrality, connectedComponents, bfs, dfs, detectCycles순위 계산, 연결 요소 탐지, 순환 탐지 같은 분석 작업을 실행합니다.

작은 서비스에서는 드라이버를 직접 사용해도 됩니다. 그래프 쿼리가 늘면서 같은 검증, 일괄 처리, 테스트 픽스처 코드가 여러 서비스에 흩어질 때 공통 API의 이점이 생깁니다. 반복 코드는 GraphOperations 아래에 두고, 그래프 데이터베이스마다 달라야 하는 부분만 어댑터에 남깁니다.

val people = ops.createVertices(
"Person",
listOf(
mapOf("email" to "alice@example.com", "name" to "Alice"),
mapOf("email" to "bob@example.com", "name" to "Bob"),
)
)
ops.createEdges(
"KNOWS",
listOf(BatchEdge(people[0].id, people[1].id, mapOf("since" to 2026)))
)

createVerticescreateEdges의 기본 구현은 단건 API를 순차 호출합니다. 중간 호출이 실패하면 앞서 생성한 정점이나 간선이 남을 수 있습니다. 따라서 이 기본 구현은 호환성을 위한 최소 기능입니다. 운영용 저장소 어댑터는 백엔드 고유의 일괄 쓰기로 재정의할 수 있지만, 원자성 보장은 각 구현의 트랜잭션 경계를 확인해야 합니다.

스키마 DSL은 Exposed의 Table처럼 VertexLabelEdgeLabel을 객체로 선언합니다.

object PersonLabel : VertexLabel("Person") {
val email = string("email")
val name = string("name")
}
object KnowsLabel : EdgeLabel("KNOWS", PersonLabel, PersonLabel) {
val since = integer("since")
}

스키마 관리는 모든 그래프 데이터베이스에서 같은 의미로 동작하지 않습니다. 일부 저장소는 고유 제약 조건이나 인덱스 DDL을 제공하지만, 다른 저장소에서는 같은 수준의 이식 가능한 DDL을 제공하기 어렵습니다.

그래서 schemaManager()는 선택한 어댑터가 GraphSchemaManagementOperations를 구현할 때만 관리자를 반환합니다. 지원하지 않는 구현은 무작업 성공으로 처리하지 않고 UnsupportedOperationException을 던집니다. UnsupportedGraphSchemaManager도 조회에는 빈 목록을 반환하지만, 스키마 변경은 명시적으로 실패시킵니다. 호출자가 존재하지 않는 제약 조건을 적용했다고 오인하지 않도록 하는 계약입니다.

트랜잭션 DSL은 정점과 간선의 CRUD만 노출합니다. 그래프 생성과 삭제 같은 생명주기 명령은 데이터베이스마다 DDL과 자동 커밋의 의미가 다르므로 트랜잭션 블록에 포함하지 않습니다. GraphTransactionalOperations를 구현하지 않은 저장소에서는 자동 커밋으로 대체하지 않고 즉시 실패합니다.

GraphOperations의 그래프 트랜잭션과 일괄 쓰기 순서
트랜잭션 블록은 정점과 간선 쓰기를 묶고, 지원하는 어댑터가 커밋 또는 롤백을 처리합니다.

병합도 같은 원칙을 따릅니다. 각 구현은 GraphMergeValidation으로 입력을 검증합니다. 정점의 matchProperties는 비어 있을 수 없고, setProperties는 식별 속성을 덮어쓸 수 없습니다. 선택한 저장소가 GraphMergeOperations를 구현하지 않으면 읽은 뒤 쓰는 방식으로 대체하지 않고 즉시 실패합니다. 이러한 대체 방식은 동시 요청에서 중복 정점을 만들 수 있기 때문입니다.

val alice = ops.mergeVertex(
label = "Person",
matchProperties = mapOf("email" to "alice@example.com"),
setProperties = mapOf("name" to "Alice"),
)

bluetape4k-graph는 동기 API, 가상 스레드 퍼사드, suspend API를 제공합니다.

실행 모델잘 맞는 상황
동기단일 작업, 테스트, 블로킹 드라이버를 사용하는 일반 서비스
가상 스레드블로킹 드라이버를 높은 동시성으로 다룰 때
코루틴Ktor 같은 코루틴 기반 서비스에서 구조화된 동시성이 중요할 때

아래 결과는 공유 TinkerGraph 픽스처에서 ApiModelBenchmark를 짧게 실행한 간이 측정입니다. 네트워크와 외부 데이터베이스 I/O를 제외한 API 모델의 비용을 비교합니다. 측정 시간이 짧고 오차가 크므로 릴리스 판단이나 일반적인 성능 순위의 근거로 사용할 수 없습니다.

동기, 가상 스레드, 코루틴 모델의 PageRank 처리량과 BFS 지연 시간을 비교한 API 모델 벤치마크 차트
이 간이 측정에서는 단일 BFS의 동기 실행과 코루틴 작업 생성 비용이 각각 낮았습니다. 실제 선택은 워크로드와 드라이버 특성에 따라 달라집니다.
시나리오API 모델평균오차할당량
PageRank 처리량동기138,943.484 ops/s±40,362.14628,451 B/op
PageRank 처리량가상 스레드40,283.460 ops/s±9,678.72029,456 B/op
PageRank 처리량코루틴 Flow36,879.554 ops/s±85,084.78129,516 B/op
BFS 깊이 5동기4.724 us/op±3.02221,990 B/op
BFS 깊이 5가상 스레드18.668 us/op±8.22923,152 B/op
BFS 깊이 5코루틴 Flow20.244 us/op±11.26823,455 B/op
BFS 동시 요청 100개가상 스레드240.903 us/op±167.5022,318,801 B/op
BFS 동시 요청 100개코루틴 async279.828 us/op±329.9422,367,754 B/op
작업 100개 생성가상 스레드51.042 us/op±173.74561,464 B/op
작업 100개 생성코루틴 async5.916 us/op±3.12728,373 B/op

실행 조건은 benchmark/graph-benchmarkApiModelBenchmark를 기준으로 합니다. GraalVM JDK 25.0.3에서 JMH 포크 1회, 워밍업 1회, 측정 3회, 반복당 1초, -prof gc 조건으로 측정했습니다.

Terminal window
java -jar benchmark/graph-benchmark/build/benchmarks/main/jars/graph-benchmark-main-jmh-*-JMH.jar \
'.*ApiModelBenchmark.*' \
-wi 1 -i 3 -r 1s -w 1s -f 1 \
-prof gc \
-rf json \
-rff docs/benchmark/2026-05-21-api-model-jmh.json

이 결과만으로 코루틴을 항상 더 빠른 실행 모델이라고 결론 내릴 수 없습니다. 코루틴은 Kotlin 서비스에서 조합성, 취소, 구조화된 동시성이 필요할 때 선택합니다. Spring MVC 서비스에서 블로킹 그래프 드라이버를 높은 동시성으로 실행해야 한다면 가상 스레드를 검토할 수 있습니다. 실행 모델은 요청 경로, 드라이버의 블로킹 특성, 필요한 동시성 수준을 함께 측정해 선택해야 합니다.

다음 글에서는 그래프 데이터를 CSV, NDJSON, GraphML로 가져오고 내보내는 graph-io와 벤치마크 결과를 읽는 방법을 다룹니다. OkIO는 파일 형식이 아니라 버퍼링, 압축, 비동기 I/O 경로를 조합하는 역할로 구분합니다.

댓글

GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.