Bluetape4k Graph Part 5: 가상 스레드 벤치마크 해석

bluetape4k-graph는 동기 API와 가상 스레드 API를 함께 제공합니다. 따라서 다음 질문에 답할 기준이 필요합니다.
그래프 데이터베이스 호출이 I/O 중심이라면 가상 스레드를 기본으로 사용해야 하는가?
그렇지 않습니다. TinkerGraph처럼 메모리에서 수 µs 안에 끝나는 작업은 동기 호출이 더 빠릅니다. 가상 스레드로
감싸면 CompletableFuture 생성, 작업 제출, 스케줄링, join() 비용이 추가되기 때문입니다. 반면 Neo4j, Memgraph,
AGE처럼 네트워크와 서버 대기가 포함된 백엔드에서는 단일 호출 지연 시간보다 많은 대기 작업을 어떻게 수용하는지가
중요해질 수 있습니다.
가상 스레드는 코드를 더 빠르게 실행하는 기능이 아니라, 대기 작업이 많은 요청당 스레드 서비스의 처리량을 높이기 위한 실행 모델입니다. 이 글은 보존된 TinkerGraph 측정 결과의 범위를 먼저 확인하고, 그 결과를 운영 백엔드에 과도하게 일반화하지 않는 방법을 설명합니다.
같은 그래프 작업, 다른 실행 경로
섹션 제목: “같은 그래프 작업, 다른 실행 경로”GraphOperations는 동기 API입니다. 가상 스레드 퍼사드는 별도의 백엔드를 구현하지 않고 같은 동기 작업을 공유
가상 스레드 실행기에 제출한 뒤 CompletableFuture<T>를 반환합니다.

어댑터의 핵심도 동기 위임 호출을 virtualFutureOfNullable로 감싸는 것입니다.
class VirtualThreadVertexAdapter( private val delegate: GraphVertexRepository,) : GraphVirtualThreadVertexRepository {
override fun findVertexByIdAsync( label: String, id: GraphElementId, ): CompletableFuture<GraphVertex?> = virtualFutureOfNullable { delegate.findVertexById(label, id) }}delegate.findVertexById() 자체는 여전히 동기 호출입니다. 따라서 결과는 위임 객체가 메모리 연산을 수행하는지,
네트워크·JDBC·서버 대기를 포함하는지에 따라 달라집니다.
벤치마크 조건부터 확인해야 합니다
섹션 제목: “벤치마크 조건부터 확인해야 합니다”아래 수치는 2026-04-17에 기록한 graph-benchmark 모듈의 TinkerGraph 픽스처 결과입니다. 현재 소스의 동일
애너테이션과 일치하지만 이번 교정에서 다시 측정한 값은 아닙니다.
| 항목 | 값 |
|---|---|
| 백엔드 | TinkerGraph 메모리 내 실행 |
| JVM | Java 25 |
| 측정 | JMH AverageTime, µs/op |
| 워밍업 / 측정 | 3 × 2초 / 5 × 3초 |
| 비교 | 동기 호출과 가상 스레드 어댑터 + CompletableFuture.join() |
픽스처는 GraphBenchmarkState에서 Person 정점 4개와 KNOWS·FOLLOWS 간선을 만들고, 같은 그래프에서 동기
호출과 가상 스레드 호출을 비교합니다.
@Benchmarkfun syncFindVertexById(): Boolean = syncOps.findVertexById("Person", aliceId) != null
@Benchmarkfun vtFindVertexById(): Boolean = vtOps.findVertexByIdAsync("Person", aliceId).join() != null이 조건은 의도적으로 네트워크 변동을 제외합니다. 따라서 결과는 실제 Neo4j 운영 지연 시간이 아니라, 짧은 메모리
연산에 가상 스레드 경로를 추가했을 때의 종단 간 비용을 보여줍니다. 각 벤치마크는 CompletableFuture를 만든 직후 join()하므로
다수 요청의 동시 처리량도 측정하지 않습니다.
결과가 보여주는 범위
섹션 제목: “결과가 보여주는 범위”TinkerGraph 메모리 내 실행에서는 동기 경로가 모든 단일 호출 벤치마크에서 앞섭니다.

대표 수치는 이렇습니다. AverageTime이므로 낮을수록 빠릅니다.
| 벤치마크 | 동기 µs/op | 가상 스레드 µs/op | 해석 |
|---|---|---|---|
findVertexById | 1.758 | 8.055 | 단건 조회보다 실행 경로 추가 비용이 큼 |
findVerticesByLabel | 2.485 | 8.679 | 질의 자체가 작아 동기 경로가 우세 |
neighbors | 2.985 | 10.595 | 작은 탐색에서는 가상 스레드 비용이 그대로 나타남 |
bfs | 4.216 | 12.588 | 작은 픽스처의 알고리즘 호출도 동기 경로가 우세 |
pageRank | 7.270 | 15.257 | 작업이 커져도 추가 비용이 남음 |
shortestPath | 22.031 | 31.540 | 작업이 커질수록 배율 차이는 줄어듦 |
이 결과에서는 배율보다 절대 차이를 봐야 합니다. 기록 문서는 가상 스레드 경로의 추가 비용을 약 6~8µs로
요약합니다. 다만 이는 해당 JVM·픽스처·어댑터·즉시 join() 조건에서 관측한 값이지 고정 상수가 아닙니다. 실제
그래프 데이터베이스가 수 ms 이상 대기한다면 이 차이의 상대적 비중은 작아질 수 있지만, 커넥션 풀, 서버 처리량,
질의 계획을 포함한 동시성 벤치마크로 다시 확인해야 합니다.
동기 API가 적합한 경우
섹션 제목: “동기 API가 적합한 경우”다음 조건에서는 동기 API가 더 단순합니다.
| 상황 | 이유 |
|---|---|
| TinkerGraph 같은 메모리 내 그래프 | 호출 자체가 매우 짧아 실행 경로 추가 비용이 크게 나타남 |
| 단발성 단순 조회 | CompletableFuture를 조합할 필요가 없음 |
| 호출량이 낮고 스레드 블로킹이 병목이 아님 | 실행 모델을 추가해도 얻는 이점이 작음 |
| 벤치마크 픽스처나 단위 테스트 | 가장 단순한 실행 경로가 결과 해석에 유리함 |
테스트와 작은 메모리 내 그래프에서는 동기 호출이 빠르고 스택 추적도 직접적이며 불필요한 join()도 없습니다.
가상 스레드를 검토할 경우
섹션 제목: “가상 스레드를 검토할 경우”다음 조건에서는 가상 스레드 API를 동시성 후보로 검토할 수 있습니다.
| 상황 | 이유 |
|---|---|
| Neo4j, Memgraph, AGE처럼 네트워크·JDBC 대기가 있는 백엔드 | 호환되는 블로킹 I/O에서 캐리어 스레드 점유 시간을 줄일 수 있음 |
| Spring MVC나 기존 블로킹 서비스 코드 | suspend를 전파하지 않고 요청당 스레드 구조를 유지할 수 있음 |
| 많은 그래프 요청을 동시에 처리 | 대기 작업마다 가벼운 스레드를 할당할 수 있음 |
Java 소비자가 CompletableFuture 조합을 사용함 | 코루틴 없이 비동기 단계를 조합할 수 있음 |
가상 스레드는 데이터베이스를 더 빠르게 만들지 않습니다. 연결 수, 질의 계획, 트랜잭션 범위, 백엔드 처리량이 먼저 한계를 만들 수 있습니다. 따라서 단일 호출 지연 시간이 아니라 목표 동시성에서의 처리량·오류율·커넥션 풀 대기를 함께 측정해야 합니다.
val vtOps = graphOperations.asVirtualThread()
val friends = vtOps .neighborsAsync(userId, NeighborOptions(edgeLabel = "FOLLOWS")) .thenApply { vertices -> vertices.map { it.id } }기존 동기 저장소를 유지하면서 Java나 Spring MVC에서 비동기 결과를 조합할 수 있습니다. 다만 CompletableFuture를 만든 직후
join()하면 호출 흐름은 해당 결과가 끝날 때까지 기다립니다. 독립 작업을 먼저 제출한 뒤
CompletableFuture.allOf(...)로 합치거나, 의존 단계는 thenCompose와 thenApply로 표현해야 조합의 목적이
분명해집니다.
그래프 데이터베이스 선택 기준과 연결하기
섹션 제목: “그래프 데이터베이스 선택 기준과 연결하기”Part 1의 그래프 데이터베이스 선택 기준과 함께 보면 실행 모델의 검증 범위가 명확해집니다.
| 백엔드 | 실행 모델 검토 기준 |
|---|---|
| TinkerGraph | 메모리 내 픽스처·프로토타입·테스트에는 동기 API가 일반적으로 충분 |
| Neo4j | 네트워크 대기가 크고 동시 요청이 많을 때 가상 스레드 후보를 실제 드라이버와 함께 측정 |
| Memgraph | 지연 시간이 짧더라도 네트워크 대기와 목표 동시성을 포함해 비교 |
| Apache AGE | PostgreSQL JDBC 경로의 대기와 커넥션 풀 한계를 함께 측정 |
이 표는 백엔드별 가상 스레드 효과를 보장하지 않습니다. “현재 대기 시간이 추가 실행 경로 비용보다 충분히 큰가?”와 “목표 동시성에서 처리량이 실제로 개선되는가?”를 측정 항목으로 바꾸기 위한 기준입니다.
bluetape4k-graph의 가상 스레드 API는 동기 API를 대체하지 않습니다. 같은 그래프 추상화에서 서비스의 실행 모델을
선택할 수 있게 하는 어댑터입니다.
- 메모리 내 µs 단위 단건 호출에서는 동기 API가 가장 단순하고 빠릅니다.
- 네트워크·JDBC 대기와 높은 동시성이 함께 있을 때 가상 스레드를 후보로 검토합니다.
- 보존된 TinkerGraph 수치는 어댑터 추가 비용을 보여주며 운영 백엔드의 처리량을 입증하지 않습니다.
- 즉시
join()하는 단건 벤치마크와 여러CompletableFuture를 조합하는 동시 처리량 벤치마크는 서로 다른 질문에 답합니다.
실행 모델은 API 이름이 아니라 대기 지점, 목표 동시성, 백엔드 자원 한계를 함께 측정해 선택해야 합니다.
참고한 소스
섹션 제목: “참고한 소스”- 동기·가상 스레드 벤치마크 결과
- 그래프 데이터베이스 선택 기준
- 가상 스레드 확장 설계
- 가상 스레드 확장 계획
- GraphBenchmarkState
- VertexOperationsBenchmark
- TraversalBenchmark
- AlgorithmBenchmark
- ApiModelBenchmark
- VirtualThreadVertexAdapter
- VirtualThreadOperationsAdapter
- OpenJDK JEP 444: 가상 스레드
시리즈
섹션 제목: “시리즈”- Part 1: 그래프 데이터베이스 선택 기준
- Part 2: 핵심 API와 실행 모델
- Part 3: 그래프 입출력과 벤치마크 해석
- Part 4: 워크숍 시나리오와 서비스 통합
- Part 5: 가상 스레드 벤치마크 해석
댓글
GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.