정확성, 속도, 그리고 단순성에 중점을 둔 jq 클론, Jaq
(github.com/01mf02)- jaq는 JSON 데이터 처리 도구
jq의 클론으로, 대부분의 경우jq와의 호환성을 유지하면서 더 예측 가능한 구현을 목표로 함 - 명령줄 프로그램
jaq는jq의 드롭인 대체재로 사용할 수 있고, Rust 라이브러리jaq-core는 Rust 프로그램 안에서 jq 프로그램을 컴파일하고 실행할 수 있음 jq에는 없는 YAML, CBOR, TOML, XML 지원을 제공하며,jaq-core는 멀티스레드 환경에서 안전하게 사용할 수 있고 JSON을 넘어선 임의 데이터 타입을 지원함- 성능 평가에서
jaq-3.0은 31개 벤치마크 중 20개에서 가장 빨랐고,jq-1.8.1은 5개,gojq-0.12.18은 6개에서 가장 빨랐음 - 보안 측면에서 패닉 방지, 메모리 안전성, 입력 데이터와 jq 필터의 I/O 제한을 보장하려 하지만, 시간·메모리·스택 같은 리소스 고갈에는 대응하지 않음
jaq가 제공하는 것
- jaq는 JSON 데이터 처리 도구
jq의 클론이며, 발음은/ʒaːk/로 Jacques와 같음 jq에는 없는 데이터 형식 지원이 있음-
YAML
-
CBOR
-
TOML
- XML
- 별도의 manual이 있고, playground에서 사용해볼 수 있음
- jaq는 두 가지 형태로 제공됨
- 명령줄 프로그램
jaq:jq의 드롭인 대체재로 사용 가능 - 라이브러리
jaq-core: Rust 프로그램 안에서 jq 프로그램을 컴파일하고 실행 가능
-
설계 목표
-
정확성
- jaq는 대부분의 경우
jq와 호환성을 유지하면서, 더 정확하고 예측 가능한 jq 구현을 목표로 함
- jaq는 대부분의 경우
-
성능
- jaq는 원래
jq 1.6의 긴 시작 시간이 불편해서 만들어졌으며, 해당 환경에서 시작 시간은 약 50ms였음 - 많은 수의 작은 파일을 처리할 때 이 시작 시간이 특히 두드러짐
jq 1.7에서 시작 시간은 크게 개선됐지만, jaq는 여러 벤치마크에서 여전히jq보다 빠름
- jaq는 원래
-
단순성
- jaq는 버그 가능성을 줄이고 기여를 쉽게 하기 위해 단순하고 작은 구현을 지향함
설치와 빌드
- Linux, Mac, Windows용 바이너리는 releases page에서 받을 수 있음
- macOS나 Linux에서는 homebrew로 설치 가능함
brew install jaqbrew install --HEAD jaq
- 소스에서 빌드하려면 Rust 툴체인이 필요함
cargo install --locked jaqcargo install --locked --git https://github.com/01mf02/jaq
- 저장소를 클론한 경우
cargo build --release나cargo install --locked --path jaq로 빌드 또는 설치 가능함 - jaq는 Rust가 지원하는 모든 시스템에서 동작해야 하며, 그렇지 않으면 이슈를 등록해달라고 안내함
성능 평가
- 성능 평가는 jaq, jq, gojq를 비교하는 여러 벤치마크로 구성됨
empty벤치마크는 null 입력으로empty필터를n번 실행해 시작 시간을 측정함bf-fib벤치마크는 jq로 작성된 Brainfuck 인터프리터로 Fibonacci 수를 생성하는 Brainfuck 스크립트를 실행함- 벤치마크 데이터는 Linux 시스템의 AMD Ryzen 5 5500U에서 생성됐음
- 사용 명령은
bench.sh target/release/jaq jq-1.8.1 gojq-0.12.17 | tee bench.json - 표에는
jaq-3.0,jq-1.8.1,gojq-0.12.18결과가 밀리초 단위로 표시됨 N/A는 오류 또는 10초 초과를 의미함
- 사용 명령은
- 결과 요약
jaq-3.0은 20개 벤치마크에서 가장 빠름jq-1.8.1은 5개 벤치마크에서 가장 빠름gojq-0.12.18은 6개 벤치마크에서 가장 빠름
gojq는tree-flatten에서 훨씬 빠른데,flatten필터를 정의가 아니라 네이티브로 구현하기 때문임
보안 모델과 제한
- jaq는 다음을 보장하려고 함
- 리소스 고갈 사례를 제외하고 패닉이 발생하지 않음
- 메모리를 손상시키지 않는 메모리 안전성
- jq 필터 실행 전 파일 읽기를 제외하면, 입력 데이터와 jq 필터가 I/O 작업을 시작할 수 없음
- 이 보장이 깨지는 경우는 버그로 간주되며 보고 대상임
- jaq는 어떤 종류의 리소스 고갈에도 대응하지 않음
- 실행 시간이 무제한으로 길어질 수 있음
- 메모리를 무제한으로 사용할 수 있음
- 스택 공간을 무제한으로 사용할 수 있음
- 예시로 입력 데이터를 읽거나 jq 필터를 실행할 때 스택 오버플로가 발생할 수 있음
jaq -nr 'repeat("[")' | jaqjaq -n 'def f: 1+f; f'
감사와 테스트
- jaq core는 두 차례의 NLnet grant의 일부로 Radically Open Security의 감사를 받음
- 첫 번째와 두 번째 보안 감사에서는 중간 또는 낮은 심각도의 이슈들이 발견됨
- 보안 감사의 모든 이슈는 처리됐고,
jaq-core/fuzz에 jaq용 퍼징 대상이 여러 개 추가됨 - jaq의 JSON 파서 hifijson도 이미 퍼징 대상을 갖고 있었음
- jaq에는 500개 이상 테스트로 구성된 테스트 스위트가 있음
사용자 사례
- 한 사용자는
jaq가 직접jq지원을 구현하는 것보다 큰 도움을 줬고,ValTtrait을 통한 확장성 덕분에 자체 타입에 jq 지원을 쉽게 추가할 수 있었다고 평가함 - 다른 사용자는 jaq를 쓰는 Rust 프로그램이 Python의
jqPyPI crate와 Python 루프가 전체 파일에 대해 쿼리 하나를 실행하는 동안, 전체 파일에 대해 모든 쿼리를 세 번 실행할 수 있었다고 밝힘 wsjq인터프리터 사례에서는 jaq가 다른 jq 구현보다 상당히 빠르고 정확성에 대한 강조가 인상적이었다는 평가가 있음- 해당
wsjq벤치마크에서 jaq는 jq보다 5~10배, gojq보다 15~196배 빠름
- 해당
- certificate transparency log 데이터를
certstream-server로 처리하던 사용자는jq파이프 처리에서 문제가 있었고, jaq로 바꾼 뒤 더 빠른 시작 시간 덕분에 저사양 VM에서도 따라갈 수 있었다고 함
자금 지원
- jaq 프로젝트는 NLnet이 설립한 NGI0 Entrust와 NGI0 Commons 펀드를 통해 지원받음
- 재정 지원은 European Commission의 Next Generation Internet 프로그램에서 제공됨
- 추가 자금은 Swiss State Secretariat for Education, Research and Innovation에서 제공됨
댓글과 토론
Hacker News 의견들
-
jq 개발이 5년간 멈춰 있다가 최근에야 다시 살아난 걸 감안하면, 알려진 버그든 새 버그든 그동안 보고가 쌓여 있던 건 이상하지 않음
이제 속도를 되찾고 오래 쌓인 미해결 목록을 천천히 정리해 나갈 것 같음- jq 1.7에서 고쳐졌음: https://github.com/jqlang/jq/pull/2646
- 왜 개발이 멈춰 있었는지 궁금함
-
비슷하거나 영감을 받은 프로젝트, 대체재가 아닌 프로젝트를 README에서 같이 소개해 주는 방식이 좋음
이 프로젝트 README에서 https://github.com/yamafaktory/jql을 알게 됐고, 오래 찾던 도구라 고마움
JAQ를 깎아내리려는 건 아니지만, JQ식 문법은 너무 이해하기 어려워서 jql이 더 잘 맞음- 이 관점에서는 gron도 좋음
JSON을 키-값 형식의 줄들로 평탄화해서grep같은 단순한 스트림 작업과 잘 맞게 해줌: https://github.com/tomnomnom/gron - 괜찮은 발견이라 한번 써볼 생각임
다만 진짜 SQL 같은 경험을 기대했음. 왜 그냥 SQL을 베껴서"SELECT * FROM $json WHERE x>1"처럼 질의할 수 있게 하지 않는지 모르겠음
다들 코드 골프라도 하듯 자기만의 난해한 기호식 질의 언어를 만들고 싶어 하는 것 같음. 극도로 짧지만 자명하지 않은 옛 Unix식 문법에서 벗어나 PowerShell 쪽 방식에 더 가까워졌으면 함 - https://github.com/tidwall/jj도 확인해볼 만함
- 그런 불편에는 어느 정도 공감하지만, 적어도 jql이 해법처럼 보이진 않음
|={"b""d"=2, "c"}는 jq의select(."b"."d" == 2 or ."c" != null)같은 의미로 보이는데, jq 쪽이 더 길어도 더 명확하게 느껴짐
실제로는.[] | select(...)가 필요하겠지만 jql도 비슷한 전제가 있을 수 있고, 예제가 완전한지 확실치 않아서 결론에는 큰 영향이 없음 - jql의 동형성(homoiconicity) 은 꽤 Lisp 같아 보임
자기 자신에 적용하거나 “매크로”를 쓰는 식도 가능해 보임
- 이 관점에서는 gron도 좋음
-
jq의 아이디어는 좋아하지만 자주 쓰지 않다 보니 원하는 걸 하려면 매번 문법을 매뉴얼에서 찾아봐야 함
안타깝게도 jq로 하는 일의 99%는| jq .임- 같은 문제가 있었음
별개로 설정 언어를 만들기 시작했는데, 알고 보니 JSON 질의에도 꽤 괜찮았음: https://docs.ruuda.nl/rcl/rcl_query/
jq로는 풀지 못했지만 RCL로는 풀 수 있었던 예시가 여기 있음: https://fosstodon.org/@ruuda/111120049523534027 - 같은 문제 때문에 jq의 강력함을 제대로 활용하지 못했는데, 이런 경우에는 Copilot이 있어서 정말 도움이 됨
필요한 작업과 줄인 원본 JSON 샘플을 같이 주면 올바른 jq 스크립트를 만들어 줌
복잡한 요구사항은 한 번에 정확히 설명하기보다 Copilot과 조금씩 반복하며 해법으로 유도하는 편이 더 쉽고 안정적임. 반복하는 동안 처음보다 더 나은 아이디어가 떠오르기도 함
ChatGPT나 다른 도구도 비슷하게 동작할 듯함 - 최근에는 ChatGPT로 필요한 jq 문법을 빠르게 얻었음: https://chat.openai.com/share/40b68d73-d2dd-412d-867f-9f375e...
- 같은 문제가 있었음
-
https://github.com/01mf02/jaq/blob/main/Cargo.lock
의존성이 꽤 많음- gojq와 비교하면 정말 많음: https://github.com/itchyny/gojq/blob/main/go.mod
- Rust 생태계에서는 이런 경우가 보통 어떻게 흘러가는지 궁금함
의존성이 많으면 시간이 지나며 서로 근본적으로 호환되지 않을 위험이 커 보이고, 유지보수가 큰 일이 될 것 같음
예를 들어 2년 뒤에도 컴파일이 잘 될까 싶음
-
jq는 매우 강력한 도구지만, 요즘은 DuckDB도 많이 쓰고 있음
데이터가 어느 정도 표 형태라면 SQL이 훨씬 자연스러운 언어임- 예전에 Retool을 써봤는데 “Query JSON with SQL”이 있었고 꽤 편리했음: https://docs.retool.com/queries/guides/sql/query-json
C#의 LINQ와 조금 비슷하지만 SQL이 더 표준화되어 있어서 더 마음에 듦
언어 안에서 원시 컬렉션을 SQL로 질의할 수 있으면 훌륭할 것 같고, 더 나아가 컬렉션을 투명하게 Sqlite에 저장할 수 있으면 더 좋겠음
데이터베이스 등에서 데이터를 가져온 뒤 루프나 스트림 API로 단순 처리하는 코드를 보면 늘 아쉬움. 이런 용도에서는 SQL이 Java/Kotlin/Python/JavaScript보다 훨씬 고수준이고 간결함 - 비슷하게 느낌
원본 JSON 출력을 전부 sqlite 테이블에 저장하고, 거기서 가상 컬럼을 만든 다음select결과를 셸 루프로 돌림
중첩 루프가 풀리고, 정확한 레코드를 DB에서 확인하고 재실행할 수 있어서 디버깅 가능성이 훨씬 좋아짐
만들고 있는 것이 DAG라는 걸 깨달았고, 항상 마지막으로 성공 처리된 레코드부터 다시 시작하고 있음. 이를 표현할Make비슷한 도구가 있는지 궁금함
Make에는 SQL 대상이 없고, Airflow 같은 완전한 DAG 처리기는 셸 조각을 붙이기엔 너무 무거움 - 맞음. 엄격한 스키마가 있는 관계형 데이터에는 SQL이 훨씬 좋음
그래도 SQL에서 재귀 질의를 간결하게 표현하는 방법은 여전히 얻기 어려움 - 이 용도에는 개인적으로 textql이 더 좋음. 머릿속 모델이 더 단순함
https://github.com/dinedal/textql
- 예전에 Retool을 써봤는데 “Query JSON with SQL”이 있었고 꽤 편리했음: https://docs.retool.com/queries/guides/sql/query-json
-
정확성 관점에서 uint64 숫자를 잘리지 않고 표시할 수 있는지 궁금함
현재 jq에서 가장 거슬리는 부분임- 안타깝게도 JSON 숫자를 64비트 부동소수점으로 보면, 표준을 따른다면 그렇게 다뤄야 하고 정수 정밀도는 53비트가 됨
다만 최신 사양인 RFC 8259는 숫자의 텍스트 형식만 명시하고 의미론은 정하지 않는다고 정정함
실제로는 대부분의 구현이 JSON을 JavaScript의 부분집합처럼 다루기 때문에 숫자가 64비트 부동소수점이라는 가정으로 이어짐 - jq 1.7에서 개선된 것으로 알고 있음: https://github.com/jqlang/jq/releases/tag/jq-1.7
십진수 숫자 리터럴을 사용해 정밀도를 보존하고, 비교 연산은 정밀도를 존중하지만 산술 연산은 잘릴 수 있다고 되어 있음 - jq 1.7은 큰 정수를 보존하지만, 그 위에 어떤 연산이 수행되면 잘림
현재는 decimal64로 잘라서 조금 헷갈리는데, 다음 릴리스에서는 JSON 사양의 제안에 맞춰 binary64(double) 로 자르도록 고쳐질 예정임: https://github.com/jqlang/jq/pull/2949
- 안타깝게도 JSON 숫자를 64비트 부동소수점으로 보면, 표준을 따른다면 그렇게 다뤄야 하고 정수 정밀도는 53비트가 됨
-
jless로 갈아탄 뒤로 돌아보지 않음
사용자 인터페이스가 다른 것들보다 훨씬 앞서 있음- 같은 종류가 아님
jq는 단순 뷰어가 아니라 JSON 질의 언어 처리기임
- 같은 종류가 아님
-
Rust 어딘가에 터미널 라인 아트 라이브러리가 있는 건 귀엽지만, jaq를 실행해 보니 iTerm에 이스케이프 코드를 메가바이트 단위로 쏟아내다가 결국 iTerm이 프린터로 출력하려고 했음
너무 영리하게 굴었음
echo *json | rush -- jaq -rf ./this-program.jq {} | datamash ...같은 맥락에서는 TTY에 예술적 효과를 넣으려는 게 적절하지 않다고 봄
에러의 원인은 어쨌든jaq에strftime이 없다는 점이었음 -
첫인상은 멋진 에러 메시지는 있지만
halt_error/0가 없다는 것임
halt_error를 주석 처리한 뒤에는 jq와 gojq보다 느렸음
같은 입력에서jq는 약 0.023초,gojq는 약 0.070초,jaq는 약 0.103초가 걸렸음
사용한aoc22-13.jq는 https://pastebin.com/raw/YiUjEu2n이고,input.txt는 https://pastebin.com/raw/X0FSyTNf임 -
jq 대신 yq를 쓰기 시작했는데, 중요한 차이가 있는지 궁금함
- 어떤 yq인지에 따라 다름
개인적으로는 https://github.com/mikefarah/yq를 https://github.com/kislyuk/yq보다 선호함 - jq가 yq보다 훨씬 더 견고한 도구처럼 느껴짐
YAML 처리가 JSON보다 훨씬 어렵다는 건 이해하지만, yq는 버전 3에서 4로 가며 문법을 jq에 더 가깝게 바꿨는데도 왠지 완전히 같지는 않음
또 yq에는 if-then-else가 없어서 설계가 좋지 않거나 빠뜨린 것처럼 보임: https://github.com/mikefarah/yq/issues/95
YAML을 처리해야 할 때는 yq가 잘 동작하고 주석도 꽤 잘 다루지만, 순수 JSON 처리에는 jq가 더 나은 도구임
- 어떤 yq인지에 따라 다름