Jetro: 고성능 Rust JSON 쿼리 엔진
JSON은 현대 웹의 주요 데이터 교환 형식이지만, 대규모 환경에서 이를 효율적으로 쿼리하는 것은 종종 병목 현상이 됩니다. jq가 JSON 처리를 위한 널리 인정받는 업계 표준이지만, 이를 고성능 Rust 애플리케이션에 임베딩하는 것은 항상 간단하지 않습니다. 이때 Rust로 작성되어 표현력이 풍부하고 접근하기 쉬운 새로운 JSON 쿼리 엔진인 Jetro가 등장했습니다.
Jetro는 단순히 jq의 클론이 아니라, JSON 쿼리가 실행되는 방식에 대한 재사고입니다. 함수형 프로그래밍 패러다임을 활용하고 플래너 주도형(planner-driven) 아키텍처를 채택함으로써, Jetro는 도메인 특화 언어(DSL)의 강력함과 성능 및 리소스 효율성을 우선시하는 더욱 간소화되고 임베디드 가능한 엔진을 제공하는 것을 목표로 합니다.
아키텍처: 플래너 주도형 실행
Jetro의 가장 중요한 기술적 차별점 중 하나는 쿼리 실행 방식입니다. 많은 전통적인 쿼리 언어에서 매핑(mapping), 필터링(filtering), 수집(collecting)과 같은 일련의 작업은 실체화된 단계(materialized stages)의 시퀀스로 실행됩니다. 예를 들어, 쿼리가 주문 목록을 필터링한 다음 첫 번째 결과만 가져오는 경우, 단순한 구현 방식은 다음 단계로 결과를 전달하기 전에 목록의 모든 주문을 처리합니다.
Jetro는 **수요 전파(demand propagation)**라는 기술을 사용합니다. 수요 전파를 통해 엔진은 쿼리 체인을 끝에서부터 역방향으로 읽어 쿼리를 충족시키기 위해 정확히 무엇이 필요한지 결정할 수 있습니다. 전체 데이터셋을 처리하는 대신, 요소들은 지연(lazy)된 풀 기반(pull-based) 모델을 통해 파이프라인을 통과합니다.
다음 쿼리를 살펴보십시오:
let out = j.collect("$.orders .map({ id: @.id, total: @.items.map(@.price * @.qty).sum() }) .filter(@.total > 100) .map(@.id) .first()");
Jetro에서는 이것이 모든 주문을 실체화하고 모든 주문에 대해 합계를 계산하지 않습니다. 대신 엔진은 다음과 같이 묻습니다: "ID 하나가 필요한가? 네. 일치하는 주문 하나가 필요한가? 네. 주문 하나를 가져와서 합계를 계산하고, 필터와 일치하면 ID를 내보내고 즉시 중단하십시오."
이 접근 방식은 특히 필요한 데이터가 전체의 작은 부분집합인 대규모 JSON 문서를 다룰 때 CPU 및 메모리 오버헤드를 크게 줄여줍니다.
표현력과 함수형 패러다임
Jetro의 DSL은 함수형 언어에서 영감을 받았으며, 개발자가 복잡한 데이터 변환을 구축할 수 있도록 합니다. 이는 고급 객체 형태 정의(object-shaping) 쿼리, 객체 매핑 및 패턴 매칭을 지원합니다.
예를 들어, Jetro는 다음 예시와 같이 쿼리 내에서 복잡한 conditional logic을 처리할 수 있습니다:
{
errors: $.events .drop_while(@.level != 'error') .filter(@.service == 'checkout') .map(match @ with {
{ level: 'error', message: msg, timestamp: ts } -> { kind: 'error', ts: ts, msg: msg },
{ level: 'error', message: msg } -> { kind: 'warning', msg: msg },
_ -> { kind: 'other' }
}) .take(20),
slow_orders: $.orders .filter(@.latency_ms > 500) .map({ id: @.id, latency: @.latency_ms }) .take(10),
first_vip: $.customers .filter(@.tier == 'vip') .map({ id: @.id, region: @.region }) .first()
}
이러한 수준의 표현력은 변환 로직이 동일한 쿼리 내에 포함될 수 있게 하여, 애플리케이션의 나머지 코드가 깔끔하고 최종적인 결과만을 받도록 합니다.
성능 및 통합
최대 처리량을 보장하기 위해 Jetro는 주요 JSON 파서로 simd-json을 사용하며, SIMD (Single Instruction, Multiple Data) 명령어를 활용하여 파싱 프로세스를 가속화합니다. 빠른 파서와 수요 전파 실행 모델을을 결합함으로써, Jetro는 불필요한 작업과 중간 결과의 불필요한 실체화(materialization)를 피합니다.
라이브러리 외에도, 이 프로젝트는 jq-와 유사한 경험을 선호하는 터미널 사용자를 위한 jetrocli를 제공하며, 언어를 배우기 위한 포괄적인 문서를 제공합니다.
표준화의 과제
Jetro가 jq에 대한 강력한 강력한 대안을 제공하지만, 더 넓은 커뮤니티 논의에서는 한 가지 과제를 강조합니다: jq 언어에 대한 공식적인 사양(specification)이 부족하다는 점입니다. Hacker News의 한 사용자가 언급했듯이, jq의 구현체들은 여러 가지가 있습니다 (Python의 yq와 Go의 yq를 포함하여), 하지만 이들은 종종 "대부분 호환되지만"에 그과합니다.
"구현체 간의 미세한 차이가 당신을 곤란하게 만듭니다..."
Jetro의 접근 방식—jq의 함수형 강력함을 유지하면서 더 작고 접근하기 쉬운 쿼리 언어를 만드는 것—은 Rust 애플리케이션에 직접 임베딩할 수 있는 단일하고 일관된 엔진을 제공함으로써 이러한 모호성을 해결합니다.