Kdrant 2.0: 공식 클라이언트를 사용할 이유를 제거했습니다
요약
Kotlin용 Qdrant 클라이언트인 Kdrant 2.0이 출시되었습니다. REST와 gRPC 프로토콜을 동일한 API로 선택하여 사용할 수 있는 추상화 계층을 제공하며, 성능 병목 현상을 해결합니다.
핵심 포인트
- REST와 gRPC 엔진을 동일한 API 인터페이스로 상호 교체 가능
- gRPC 엔진 도입 시 기존 kdrant-core 코드 수정 불필요
- gRPC 사용 시 불필요한 의존성 없이 가벼운 grpc-okhttp 활용
- 프로토콜에 독립적인 테스트 스위트를 통해 안정성 검증
Kdrant를 소개했던 포스트에서 제가 쓰면서도 마음에 들지 않았던 문단이 하나 있었습니다:
순수 처리량 (throughput)과 스트리밍 (streaming) 측면에서는 여전히 gRPC/HTTP2가 승리합니다. 만약 그것이 병목 현상 (bottleneck)이라면, 공식 클라이언트를 사용하세요.
그것은 사실이었고, 또한 그 포스트에서 사람들을 다른 곳으로 떠나게 만든 단 한 줄이기도 했습니다. Kdrant 2.0이 그 해답을 제시합니다. 이제 REST와 동일한 QdrantClient 뒤에 gRPC 엔진이 자리 잡았습니다.
동일한 클라이언트, 선택 가능한 프로토콜 (wire)
val qdrant = Kdrant(host = "localhost") // REST, 포트 6333
val qdrant = KdrantGrpc(host = "localhost") // gRPC, 포트 6334
이것이 마이그레이션 (migration) 방법입니다. 그 라인 이후의 모든 호출은 동일합니다. 왜냐하면 API는 프로토콜을 인지하지 않도록 설계되었기 때문입니다. 프로토콜 (wire)은 첫 번째 릴리스부터 QdrantTransport 인터페이스 뒤에 숨겨져 있었습니다.
이것이 어떤 가치가 있는지 정확히 말씀드리겠습니다. 단순히 "우리는 깔끔한 추상화 (abstraction)를 가지고 있다"라고 말하는 것은 비용이 들지 않기 때문입니다. gRPC 엔진을 추가하면서 kdrant-core의 코드는 단 한 줄도 바뀌지 않았습니다. 몇 줄의 작은 변경조차 없었습니다. 해당 모듈의 diff (차이점)는 비어 있습니다. 이것은 의미 있는 유일한 이음새 (seam) 테스트이며, 이를 실행하기 위해서는 두 번째 구현체를 작성해야 했습니다.
이 엔진은 io.qdrant:client를 래핑 (wrapping)하는 대신 Qdrant의 .proto 파일로부터 직접 스텁 (stub)을 생성합니다. 여기에는 두 가지 이유가 있습니다. 첫째, grpc-kotlin은 suspend 함수와 Flow를 생성하는데, 이는 이미 트랜스포트 (transport)가 가지고 있는 형태이므로 다시 코루틴 (coroutines)으로 변환하기 위한 ListenableFuture가 필요하지 않습니다. 둘째, 생성 방식을 택함으로써 의존성 세트 (dependency set)를 상속받는 대신 직접 결정합니다. 이 엔진은 공식 클라이언트에서 단독으로 약 9MB를 차지하는 쉐이디드 (shaded) Netty jar 대신 grpc-okhttp를 해결합니다.
gRPC를 원하지 않는다면 그에 따른 비용을 전혀 지불하지 않아도 됩니다. 이는 별도의 아티팩트 (artifact)이며, kdrant-transport-rest에 의존하는 빌드는 gRPC, protobuf, 그리고 Netty를 전혀 해결 (resolve)하지 않습니다. 이는 README에서 주장하는 것이 아니라 매 빌드마다 확인됩니다. REST 엔진의 런타임 클래스패스 (runtime classpath)에 해당 그룹의 요소가 나타나면 실패하는 Gradle 태스크가 있으며, 저는 해당 모듈에 grpc-stub을 추가하여 나타나는 5개의 아티팩트를 확인함으로써 이것이 작동함을 검증했습니다.
테스트 스위트가 흥미로운 부분이 되었습니다
두 엔진은 무언가가 이를 증명할 수 있을 때만 상호 교체 가능하며, 기존 테스트는 이를 증명할 수 없었습니다. 기존 테스트는 HTTP 요청 본문 (request bodies)을 검증하는데, gRPC 엔진은 구조적으로 이를 충족할 수 없기 때문입니다.
그래서 이제 공유된 스위트 (suite)가 있습니다. 외부에서 QdrantClient를 가져와 Docker 내의 실제 Qdrant와 통신하며, 프로토콜을 전혀 언급하지 않는 약 30개의 동작 테스트 (behavioural tests)입니다. 두 엔진 모두 동일한 서버를 대상으로 동일한 파일을 실행합니다.
이 테스트는 첫 실행에서 제 가치를 증명했습니다. 하나의 테스트가 gRPC에서만 실패했는데, 오직 gRPC에서만 발생했습니다. 정렬된 scroll 요청이 페이로드 (payload)가 전혀 없는 상태로 반환된 것입니다.
만약 여러분이 Qdrant를 직접 대상으로 코드를 작성한다면 그 원인을 알아둘 가치가 있습니다. scroll과 retrieve에서 with_payload는 기본값이 true로 설정되어 있습니다. REST 엔진은 아무것도 요청하지 않았을 때 단순히 해당 필드를 생략하므로, 그 기본값을 얻게 됩니다. 반면 제 gRPC 엔진은 "호출자가 아무것도 말하지 않음"을 명시적인 enable = false로 번역하고 있었는데, 이는 서로 다른 요청입니다. 페이로드를 요청하지 않은 모든 scroll과 retrieve는 조용히 빈 상태로 반환되고 있었습니다.
코드를 읽는 것만으로는 이를 발견할 수 없으며, 어느 엔진의 유닛 테스트 (unit test)로도 이를 잡아낼 수 없었을 것입니다. 이는 양쪽 모두에서 실행되며, 고유한 동작 방식(opinions)을 가진 서버와 비교하는 테스트가 필요합니다.
코어는 멀티플랫폼 (multiplatform)입니다
kdrant-core는 JVM 및 iOS, macOS, Linux, Windows를 포함하는 8개의 Kotlin/Native 타겟을 위해 빌드됩니다. 위에서 언급한 것과 동일한 이유 때문입니다. 네트워크 전송(wire)의 존재를 전혀 몰랐던 코드는 이식해야 할 플랫폼 특화 코드가 없습니다. 이를 commonMain으로 옮기면서 변경된 것은 단 하나의 선언인 기본 디스패처(default dispatcher)뿐인데, 이는 JVM만이 Dispatchers.IO를 공개하기 때문입니다.
이것이 현재 여러분에게 무엇을 제공하고 무엇을 제공하지 않는지 명확히 하고 싶습니다. Ktor CIO와 grpc-java가 JVM 전용이기 때문에 엔진은 여전히 JVM 전용입니다. Android 앱과 iOS 앱은 모델, 필터 DSL(Domain Specific Language), 쿼리 빌딩(query building)을 공유할 수 있으며, iOS 측은 자체적인 트랜스포트(transport)를 가져옵니다. 이것은 "Kdrant가 iOS에서 실행된다"는 것과는 다르며, 타겟 이름 목록이 그런 의미를 암시하게 두느니 차라리 이렇게 말하는 편이 낫겠습니다.
Kotlin/JS는 의도적으로 제외되었습니다. JS 엔진이 없다면 보낼 수 있는 것이 없는 DSL만 배포하게 될 것이며, 기존의 테스트 도구는 저장소 내의 유일한 npm 의존성 그래프였는데 심각도가 높은 보안 권고(advisory)를 포함하고 있었습니다. 그곳에 가치 있는 엔진이 생기는 날, 단 한 줄의 코드로 다시 가져올 수 있습니다.
업그레이드 비용
API는 변경되지 않았습니다. "대체로 변경되지 않은 것"이 아닙니다. 바이너리 호환성(binary-compatibility) 덤프는 1.2.0과 바이트 단위로 동일하며, 모든 1.x 호출 지점은 변경 없이 컴파일됩니다.
주요 변경 사항은 아티팩트 레이아웃(artifact layout)입니다. kdrant-core는 이제 멀티플랫폼(multiplatform)이므로, 그 좌표(coordinate)는 Gradle 모듈 메타데이터를 포함하며 JVM 클래스는 kdrant-core-jvm에 존재합니다. Gradle에서는 버전 번호만 변경하면 되며 그 외에는 아무것도 바꿀 필요가 없습니다. Gradle이 해당 메타데이터를 읽어 변체(variant)를 선택하기 때문입니다. Maven의 경우, kdrant-core를 직접 지정했다면 kdrant-core-jvm으로 이동해야 합니다. 하지만 제가 권장하는 방식인 kdrant-transport-rest 또는 kdrant-transport-grpc에 의존하고 있다면, 두 경우 모두 해당되지 않습니다.
엔진을 교체하기 전에 알아두어야 할 한 가지 제한 사항이 있습니다. Qdrant는 HTTP를 통해서는 텔레메트리 (telemetry), Prometheus 메트릭 (Prometheus metrics), 이슈 엔드포인트 (issues endpoint), 스냅샷 복구 (snapshot recovery), 스냅샷 다운로드 및 업로드 (snapshot download and upload), 그리고 샤드 범위 스냅샷 (shard-scope snapshots) 등 11가지 작업만을 제공합니다. gRPC 엔진에서는 각 작업이 자신의 이름과 REST임을 명시하며 오류를 발생시킵니다. 아무런 반응 없이 아무것도 반환하지 않는 스냅샷 다운로드는 백업이 존재하지 않는데도 존재한다고 착각하게 만들 수 있으므로, 저는 차라리 명확하게 실패하는 쪽을 택하겠습니다.
사용해 보기
dependencies {
implementation("io.github.nacode-studios:kdrant-transport-rest:2.0.0")
// 또는, gRPC의 경우:
...
- Repo: https://github.com/NaCode-Studios/Kdrant
- API docs: https://nacode-studios.github.io/Kdrant/
- Changelog: https://github.com/NaCode-Studios/Kdrant/blob/main/CHANGELOG.md
Spring Boot, Spring AI, LangChain4j, Koog 및 Micrometer 모듈도 모두 동일한 버전에 포함되어 있습니다.
피드백은 언제나 환영하며, 특히 실제 부하(load) 환경에서 gRPC 엔진을 실행 중인 분들의 의견을 기다립니다. 그것이 제가 이 엔진을 만든 목적이자, 저 혼자서는 제대로 테스트할 수 없는 부분이기 때문입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기