Bluetape4k Graph Part 3: graph I/O와 benchmark 읽기

서비스에서 만든 그래프를 항상 database 안에서만 다루지는 않습니다. migration, snapshot, analytics, 재현 가능한 test
fixture를 만들려면 vertex와 edge를 파일로 내보내고 다시 불러와야 합니다. 운영에서 한 번쯤은 “이 graph를 파일로
빼서 다시 재현해 볼 수 있나?”라는 질문을 만나게 됩니다. 그때부터 콘솔 복붙은 급격히 덜 귀여워집니다. bluetape4k-graph의
graph-io는 이 작업을 포맷별 모듈과 I/O 조합 레이어로 나눕니다.

그래프 데이터 포맷 선택
섹션 제목: “그래프 데이터 포맷 선택”현재 graph-io에서 구현되어 있는 포맷은 CSV, Jackson2 NDJSON, Jackson3 NDJSON, GraphML입니다.
| Format | 잘 맞는 용도 |
|---|---|
| CSV | 사람이 열어보고 수정하기 쉬운 작은 fixture, 데이터 확인 |
| Jackson2 NDJSON | Jackson2를 쓰는 기존 서비스와 호환 |
| Jackson3 NDJSON | 신규 Kotlin/JVM 서비스의 line-delimited interchange |
| GraphML | 다른 graph tool과 교환하거나 표준 XML 형식이 필요할 때 |
NDJSON은 포맷이고, Jackson2/Jackson3는 그 NDJSON을 읽고 쓰는 serializer 구현입니다. Fastjson2도 같은 NDJSON 계열로
확장하기 좋은 후보지만, 현재 bluetape4k-graph 소스에는 Jackson2/Jackson3 모듈만 들어 있습니다. 이 글에서는 실제로
구현된 모듈만 기준으로 보겠습니다.
OkIO는 포맷이 아닙니다. bluetape4k-io, bluetape4k-okio는 CSV/NDJSON/GraphML 경로 위에 buffering, compression,
encryption, async I/O 작업을 decorator 방식으로 붙일 수 있게 해줍니다. 쉽게 말하면 “어떤 파일 형식인가”와 “그 파일을
어떤 I/O 경로로 읽고 쓸 것인가”를 분리하는 구조입니다. 포맷과 I/O 경로를 섞어 두면 나중에 압축 하나 붙이려고 코드가
의외로 크게 흔들립니다.
공통 contract는 graph-io-core에 있고, 각 format 모듈이 sync, virtual-thread, suspend 변형을 제공합니다. 여기서도
실행 모델 선택은 “무조건 coroutine”이 아닙니다. 파일 I/O와 parser 비용, graph storage write 비용, graph 크기에 따라
결과가 달라집니다.
벤치마크 구현과 실행 방식
섹션 제목: “벤치마크 구현과 실행 방식”아래 결과는 docs/benchmark/2026-04-18-graph-io-bulk-results.md의 quick-run을 기준으로 합니다. 구현은
BulkGraphIoBenchmark와 BulkGraphIoBenchmarkState에 있습니다.
@State(Scope.Benchmark)class BulkGraphIoBenchmarkState { @Param("small", "medium") lateinit var size: String
lateinit var ops: TinkerGraphOperations lateinit var tempDir: Path}benchmark state는 임시 디렉터리를 만들고, TinkerGraph in-memory graph에 데이터를 넣습니다. small dataset은
Person vertex 1,000개와 KNOWS edge 2,000개입니다. random seed는 고정되어 있어 같은 fixture를 반복해서 만들 수
있습니다. benchmark method는 이 graph를 CSV/NDJSON/GraphML 파일로 export하거나, 파일을 다시 TinkerGraph로 import합니다.
@Benchmarkfun csvSyncExport(state: BulkGraphIoBenchmarkState) { state.csvExporter.exportGraph(state.ops, state.csvPath, state.exportOptions)}실행 환경은 Mac mini Pro M4, memory 48GB, Java 25입니다. 이 Graph I/O quick-run 자체는 TinkerGraph in-memory와 임시 파일을 사용하므로 별도 graph DB Docker container를 띄우지 않습니다. Neo4j/Memgraph/AGE/FalkorDB 비교 benchmark나 통합 테스트에서는 Docker/Testcontainers를 사용합니다.
quick-run 설정은 @Fork(0), warmup 1×1초, measurement 2×1초입니다. 빠르게 regression을 확인하기 위한 숫자이지,
발표용 정밀 benchmark는 아닙니다. 숫자가 너무 예쁘게 나오면 오히려 한 번 더 의심하는 편이 좋습니다. 정밀하게 보려면
@Fork(1), warmup 3×3초, measurement 5×3초 이상으로 다시 돌리는 편이 좋습니다.
./gradlew :graph-io-benchmark:smokeBenchmark벤치마크 결과
섹션 제목: “벤치마크 결과”아래 차트는 작은 graph 1,000 vertices / 2,000 edges 기준의 평균 latency를 요약한 것입니다. JMH mode는
AverageTime이고 단위는 ms/op입니다. 이 quick-run은 빠른 회귀 확인용으로 평균값만 보고, 분산까지 확인해야 하는
정밀 비교는 raw JSON을 남기는 긴 실행으로 따로 돌리는 편이 낫습니다.

대표 숫자는 이렇습니다.
| 작업 | 평균 ms/op |
|---|---|
| CSV export | 1.017 |
| Jackson2 NDJSON export | 1.194 |
| Jackson3 NDJSON export | 1.275 |
| GraphML export | 2.582 |
| CSV import | 17.854 |
| Jackson2 NDJSON import | 18.831 |
| Jackson3 NDJSON import | 19.852 |
| GraphML import | 21.111 |
GraphML은 factory caching과 buffered stream 적용 전후 차이가 특히 컸습니다. 이전에는 export가 약 413ms 수준까지
튀었고, 개선 후에는 약 2.5ms 수준으로 내려왔습니다. 이 결과는 “GraphML은 느리다”로 끝낼 이야기가 아닙니다. 문제는
포맷 이름이 아니라 XMLInputFactory/XMLOutputFactory 생성, buffering, parser reuse 같은 I/O 기본기였습니다. 성능
문제를 잡다 보면 가끔 아주 평범한 코드가 범인입니다. 억울하지만 자주 그렇습니다.
Suspend import에서 Jackson2/Jackson3가 약 150ms까지 튄 결과도 있습니다. benchmark 문서에서는 runBlocking 안에서
Dispatchers.IO 초기화 비용이 함께 잡힌 것으로 분석합니다. 실제 coroutine 서비스에서는 dispatcher가 이미 준비되어
있을 가능성이 높지만, benchmark에서는 이런 초기화 비용까지 숫자에 섞일 수 있습니다. 그래서 benchmark는 표와 차트만
보지 말고 harness도 같이 읽어야 합니다.
어떻게 쓰면 좋을까
섹션 제목: “어떻게 쓰면 좋을까”테스트 fixture는 CSV나 NDJSON으로 시작하는 편이 편합니다. 사람이 파일을 열어보고 vertex/edge를 확인해야 하면 CSV가 좋고, 서비스 간 교환이나 snapshot replay를 생각하면 NDJSON이 더 자연스럽습니다. GraphML은 graph tool과의 교환이 필요할 때 쓰는 선택지입니다. “나중에 사람이 열어봐야 하나?”라는 질문 하나만 해도 포맷 선택이 꽤 빨리 좁혀집니다.
대용량 파일을 다루기 시작하면 포맷보다 I/O 경로가 더 크게 느껴질 때가 있습니다. 그때 OkIO 기반 adapter로 buffering, compression, encryption, async 경로를 조합합니다. 포맷 선택과 I/O decorator 선택을 분리해 두면, CSV에서 NDJSON으로 바꾸거나 GraphML export만 별도 경로로 빼는 작업이 훨씬 단순해집니다.
다음 글에서는 bluetape4k-workshop의 abuser detection, recommendation, knowledge graph, social network 예제로
서비스에서 graph API를 어떻게 적용하는지 보겠습니다.
참고한 소스
섹션 제목: “참고한 소스”- graph-io core README
- CSV graph I/O
- Jackson2 graph I/O
- Jackson3 graph I/O
- GraphML graph I/O
- OkIO graph I/O
- BulkGraphIoBenchmark
- BulkGraphIoBenchmarkState
- Graph I/O benchmark result
시리즈
섹션 제목: “시리즈”- Part 1: 그래프 데이터베이스 선택 지도
- Part 2: core API와 실행 모델
- Part 3: graph I/O와 benchmark 읽기
- Part 4: workshop 시나리오와 서비스 통합
- Part 5: Virtual Threads benchmark 읽기
댓글
GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.