콘텐츠로 이동

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

로봇 작업자들이 동기와 가상 스레드 실행 경로를 나눠 그래프 벤치마크 작업대를 살펴보는 3D 일러스트
가상 스레드의 이점은 짧은 계산보다 대기 시간이 긴 동시 작업에서 드러납니다.

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 메모리 내 실행
JVMJava 25
측정JMH AverageTime, µs/op
워밍업 / 측정3 × 2초 / 5 × 3초
비교동기 호출과 가상 스레드 어댑터 + CompletableFuture.join()

픽스처는 GraphBenchmarkState에서 Person 정점 4개와 KNOWS·FOLLOWS 간선을 만들고, 같은 그래프에서 동기 호출과 가상 스레드 호출을 비교합니다.

@Benchmark
fun syncFindVertexById(): Boolean =
syncOps.findVertexById("Person", aliceId) != null
@Benchmark
fun vtFindVertexById(): Boolean =
vtOps.findVertexByIdAsync("Person", aliceId).join() != null

이 조건은 의도적으로 네트워크 변동을 제외합니다. 따라서 결과는 실제 Neo4j 운영 지연 시간이 아니라, 짧은 메모리 연산에 가상 스레드 경로를 추가했을 때의 종단 간 비용을 보여줍니다. 각 벤치마크는 CompletableFuture를 만든 직후 join()하므로 다수 요청의 동시 처리량도 측정하지 않습니다.

TinkerGraph 메모리 내 실행에서는 동기 경로가 모든 단일 호출 벤치마크에서 앞섭니다.

TinkerGraph의 동기 호출과 가상 스레드 호출 지연 시간을 연산당 마이크로초로 비교한 차트
연산 자체가 µs 단위로 끝나면 가상 스레드 경로의 추가 비용이 결과에 크게 나타납니다.

대표 수치는 이렇습니다. AverageTime이므로 낮을수록 빠릅니다.

벤치마크동기 µs/op가상 스레드 µs/op해석
findVertexById1.7588.055단건 조회보다 실행 경로 추가 비용이 큼
findVerticesByLabel2.4858.679질의 자체가 작아 동기 경로가 우세
neighbors2.98510.595작은 탐색에서는 가상 스레드 비용이 그대로 나타남
bfs4.21612.588작은 픽스처의 알고리즘 호출도 동기 경로가 우세
pageRank7.27015.257작업이 커져도 추가 비용이 남음
shortestPath22.03131.540작업이 커질수록 배율 차이는 줄어듦

이 결과에서는 배율보다 절대 차이를 봐야 합니다. 기록 문서는 가상 스레드 경로의 추가 비용을 약 6~8µs로 요약합니다. 다만 이는 해당 JVM·픽스처·어댑터·즉시 join() 조건에서 관측한 값이지 고정 상수가 아닙니다. 실제 그래프 데이터베이스가 수 ms 이상 대기한다면 이 차이의 상대적 비중은 작아질 수 있지만, 커넥션 풀, 서버 처리량, 질의 계획을 포함한 동시성 벤치마크로 다시 확인해야 합니다.

다음 조건에서는 동기 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(...)로 합치거나, 의존 단계는 thenComposethenApply로 표현해야 조합의 목적이 분명해집니다.

그래프 데이터베이스 선택 기준과 연결하기

섹션 제목: “그래프 데이터베이스 선택 기준과 연결하기”

Part 1의 그래프 데이터베이스 선택 기준과 함께 보면 실행 모델의 검증 범위가 명확해집니다.

백엔드실행 모델 검토 기준
TinkerGraph메모리 내 픽스처·프로토타입·테스트에는 동기 API가 일반적으로 충분
Neo4j네트워크 대기가 크고 동시 요청이 많을 때 가상 스레드 후보를 실제 드라이버와 함께 측정
Memgraph지연 시간이 짧더라도 네트워크 대기와 목표 동시성을 포함해 비교
Apache AGEPostgreSQL JDBC 경로의 대기와 커넥션 풀 한계를 함께 측정

이 표는 백엔드별 가상 스레드 효과를 보장하지 않습니다. “현재 대기 시간이 추가 실행 경로 비용보다 충분히 큰가?”와 “목표 동시성에서 처리량이 실제로 개선되는가?”를 측정 항목으로 바꾸기 위한 기준입니다.

bluetape4k-graph의 가상 스레드 API는 동기 API를 대체하지 않습니다. 같은 그래프 추상화에서 서비스의 실행 모델을 선택할 수 있게 하는 어댑터입니다.

  • 메모리 내 µs 단위 단건 호출에서는 동기 API가 가장 단순하고 빠릅니다.
  • 네트워크·JDBC 대기와 높은 동시성이 함께 있을 때 가상 스레드를 후보로 검토합니다.
  • 보존된 TinkerGraph 수치는 어댑터 추가 비용을 보여주며 운영 백엔드의 처리량을 입증하지 않습니다.
  • 즉시 join()하는 단건 벤치마크와 여러 CompletableFuture를 조합하는 동시 처리량 벤치마크는 서로 다른 질문에 답합니다.

실행 모델은 API 이름이 아니라 대기 지점, 목표 동시성, 백엔드 자원 한계를 함께 측정해 선택해야 합니다.

댓글

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