코드를 거의 보지 않고 만든 70배 빠른 SQL 파서
(posthog.com)- PostHog는 ANTLR 기반 C++ SQL 파서를 여러 Claude Code 세션으로 재작성해 16K줄의 Rust 파서와 5K줄의 도구, 수천 줄의 테스트를 만들었으며 노트북 기준 약 70배의 속도 향상을 얻음
- 새 구현은 예측형 재귀 하강 파서와 Pratt 표현식 코어를 중심으로, 필요한 곳에만 제한적 미리보기와 추측적 백트래킹을 적용해 ANTLR의 범용 그래프 순회 비용을 없앰
- 기존 C++ 파서를 기준 구현으로 두고 속성 기반 테스트, 익명화된 실제 쿼리, 회귀 테스트, 코드 커버리지 유도 생성, ShrinkRay 축소를 결합해 두 파서의 불일치를 반복적으로 제거함
- 프로덕션 섀도 모드에서 수백만 번의 파싱 결과가 기존 파서와 한 번도 달라지지 않아 몇 시간 만에 트래픽을 전환했으며, 프로덕션 평균 454배의 속도 향상을 기록함
- 파서 생성기가 문법과 기준 구현을 제공하고 LLM이 퍼징으로 동등성을 검증하는 방식은, 전문 지식이 필요했던 수개월 규모의 고성능 파서 개발을 며칠로 줄일 가능성을 보여줌
PostHog에 SQL 파서가 필요한 이유
- PostHog는 사용자가 SQL로 데이터에 직접 접근할 수 있도록 입력 SQL을 원시 ClickHouse SQL로 변환함
- 데이터베이스의 물리적 배치와 독립된 논리적 데이터 뷰를 제공함
- 데이터베이스 계층을 변경해도 기존 쿼리가 깨지지 않도록 보호함
- 변환 과정에 성능 최적화와 접근 제어를 추가함
- 제품 분석, 세션 재생, 오류 추적 등 대부분의 PostHog 도구도 SQL로 작성한 쿼리를 같은 변환 과정에 통과시킴
- SQL을 변환하려면 먼저 추상 구문 트리(AST) 로 바꿔야 하며, 이 트리가 다시 ClickHouse SQL로 변환됨
- 파서는 신뢰할 수 없는 쿼리 입력을 처음 처리하는 구성 요소임
- 이후의 접근 제어와 최적화도 모두 파서가 만든 트리에 의존함
기존 ANTLR 파서의 구조와 비용
- AI 코딩 이전에는 파서를 직접 작성하고 유지하기가 매우 어려워 PostHog는 오픈소스 파서 생성기 ANTLR을 사용함
- 문법을 선언형 .g4 파일로 제공하면 ANTLR이 파서 코드 대부분을 생성함
- 기존 파서도 이미 C++로 생성됐으므로, 이번 성능 향상은 단순히 구현 언어를 Rust로 바꾼 결과가 아님
- ANTLR은 강력하고 유연하지만 각 토큰을 처리할 때 더 많은 작업이 필요함
- 문법을 증강 전이망(ATN), 즉 스택을 포함한 비결정적 유한 오토마톤(NFA)에 가까운 형태로 컴파일함
- 런타임에는 범용 인터프리터가 이 그래프를 순회함
parseExpression()같은 직접 작성 함수 대신 추가적인 추상화와 간접 호출 계층을 거침
- 임의의 동적 미리보기도 지원하므로 대안이 여러 개면 하나만 유효하게 남을 때까지 모든 해석을 동시에 시뮬레이션함
- 충분히 최적화된 ANTLR라도 그래프 순회 인터프리터 구조로는 직접 구현한 재귀 하강 파서보다 빨라지기 어려움
두 가지 파서 구현을 병렬로 실험
- AI를 활용하면 직접 구현한 파서를 작성하고 유지하기가 쉬워지지만, Claude에 오류 없는 Rust 파서를 요청하는 것만으로는 충분하지 않았음
- Claude는 많은 실수를 만들었음
- 재작성 가능성 자체를 반복해서 의심함
- 각 코딩 라운드가 끝날 때마다 작업을 마치려는 경향을 보임
- 여러 장기 실행 Claude Code 세션에서 두 접근법을 병렬로 시험함
- 성능 중심 접근법은 재귀 하강 파서와 Pratt 표현식 루프를 사용하고 필요한 곳에만 미리보기와 백트래킹을 추가함
- 성공 가능성 중심 접근법은 ANTLR의 동작을 최대한 따르되, 범용 그래프 순회 대신 전이를 명시적인 코드로 구현함
- 두 접근법은 결국 비슷한 수준으로 작동했지만, 이를 확인하기까지 며칠이 걸림
- 목표는 현실적인 모든 쿼리에서 기존 C++ 파서와 완전히 일치하고, 인위적인 쿼리에서도 가능한 한 가까운 결과를 내는 것이었음
SELECT SELECT FROM FROM WHERE WHERE AND AND처럼 비정상적으로 보이지만 유효한 SQL도 테스트 대상에 포함함
- 기존 C++ 파서를 기준 구현(오라클) 으로 삼아 다르게 처리되는 SQL을 찾고, 새 파서를 수정한 뒤 다시 비교하는 테스트 주도 방식으로 개발함
속성 기반 테스트로 불일치 생성
- 기존 파서 개발 과정에서 쌓인 회귀 테스트를 먼저 통과시킨 뒤 새로운 불일치를 체계적으로 생성함
- Hypothesis를 이용한 속성 기반 테스트(PBT) 에서 검증 속성을 “새 파서가 기준 구현과 일치한다”로 정의함
- 입력값은 SQL 쿼리임
- Hypothesis가 두 파서의 결과가 일치하지 않는 쿼리를 탐색함
- 흥미로운 SQL을 생성하기 위해 ANTLR 문법 파일에서 SQL 생성기를 코드 생성하는 도구를 Claude와 함께 만듦
.g4파일 자체를 읽는 별도의 파서도 작성함- 이후 토큰 교환이나 괄호 추가 같은 변형을 생성 단계에 포함함
취약한 수정을 막는 프롬프트 구성
- PBT는 새 테스트를 안정적으로 만들었지만, Claude는 한 토큰 미리보기로 특정 사례를 고친 뒤 나중에 두 토큰 미리보기가 필요하다는 사실을 발견하는 식의 취약한 수정을 반복함
- 컨텍스트 창이 자주 가득 차 압축되면서 실제 문법이나 기준 파서의 동작을 잊은 상태로 수정했을 가능성이 있었음
- 각 불일치를 고치는 코드를 작성하기 직전에 관련 문법 파일과 C++ 소스 코드를 모두 컨텍스트로 불러오도록 지시해 이 문제를 완화함
CPU와 Claude를 함께 계속 가동
- PBT가 백그라운드에서 계속 실행되며 실패 사례를 파일에 기록하고, Claude는 다른 작업이 없을 때 해당 사례를 가져오도록 도구를 구성함
- 실패 사례는 여러 경로에서 수집함
- 기존 회귀 테스트
- PBT로 생성한 SQL
- 프로덕션 쿼리 로그에서 가져온 익명화 쿼리
- 백그라운드 에이전트에 “경계 사례를 정말 깊게 생각하라”고 요청해 만든 사례
- 병렬로 개발한 두 파서는 회귀 테스트 모음을 공유해 한 세션에서 발견한 실패가 다른 세션에도 즉시 반영됨
- Hypothesis는 직접 생성한 사례를 최소 재현으로 축소할 수 있지만 외부 SQL에는 적용할 수 없어, 해당 사례에는 ShrinkRay를 사용함
- 이후 코드 커버리지 유도 테스트 생성도 추가함
- 아직 실행되지 않은 SQL 구성 요소를 감지해 해당 구조가 더 자주 생성되도록 편향함
- 프로덕션 쿼리 집합에서 100% 정확도에 도달하는 데 필수는 아니었지만, 매우 미묘한 사례를 찾는 데 도움을 줌
실패를 찾고 고치는 자동 반복 과정
- 최종 개발 루프는 다음 단계를 자율적으로 반복함
- PBT, 실제 쿼리 집합, 회귀 테스트, 경계 사례 탐색으로 새로운 실패를 생성함
- 실패를 축소해 계속 확장되는 회귀 테스트 목록에 추가함
- 일반적인 해결책을 우선하면서 문법과 C++ 기준 구현의 처리 방식을 확인함
- 수정한 뒤 사람이 읽을 수 있는 한 문단 요약을 출력함
- 전체 회귀 테스트를 실행해 모든 사례가 통과하는지 확인함
- 같은 과정을 다시 자율 실행함
- 새 파서가 훨씬 빨랐기 때문에 프로덕션에서 기존 C++ 파서와 함께 섀도 모드로 실행하며 결과 차이를 보고할 수 있었음
프로덕션 검증과 전환
- 프로덕션 쿼리 로그를 이용한 사전 비교에서는 약 5만 개의 쿼리를 테스트함
- 섀도 모드에서는 수백만 번의 파싱을 빠르게 실행했으며 기존 파서와의 불일치가 한 건도 발생하지 않음
- 원래 며칠 동안 섀도 모드를 유지할 계획이었지만 결과가 충분히 강해 몇 시간 만에 프로덕션 트래픽을 새 파서로 전환함
- 전환 후에는 기존 파서로 다시 비교하는 0.1% 역방향 섀도를 유지함
- 새 파서는 AST뿐 아니라 소스 위치 정보까지 C++ ANTLR 파서와 동일하게 출력함
노트북 70배, 프로덕션 454배
- 노트북 벤치마크에서는 기존 파서보다 약 70배 빠른 성능을 기록함
- 프로덕션 쿼리에서는 평균 454배 빨라짐
- 프로덕션에서는 파서 캐시에 적중하지 않는 더 긴 SQL을 주로 처리해 노트북보다 성능 차이가 커짐
- 최종 결과물은 파서 코드 약 16K줄, 도구 약 5K줄, 테스트 수천 줄로 구성됨
- 현실적인 쿼리에서는 기존 파서와 동등하며, 차이는 인위적으로 구성한 극소수 쿼리에 한정됨
최종 파서 구조와 달라진 개발 방식
- 최종 파서는 다음 구조로 구성됨
- 대부분을 예측형 재귀 하강 파서로 구현함
- 표현식 처리에는 Pratt 파서 코어를 사용함
- 기본 커서는 LL(2)이며, 특정 지점에서만 입력을 소비하지 않는 제한적 미리보기 탐색으로 범위를 넓힘
- 반드시 필요한 소수의 판단에서만 순서가 있는 선택 기반의 국소적 추측 백트래킹을 적용함
- 전체 파서는 2026년 5월 Claude Opus 4.7이 Rust로 작성했으며, 코드를 직접 손으로 작성하지 않았음
- 문법으로 입력을 생성하고 코드 커버리지로 생성을 유도하는 PBT 구성은 파서 퍼징의 최신 방식에 가까우며, 단순한 “바이브 코딩”과 구별됨
- 특정 파서 지식이 있는 개발자에게도 수개월 걸릴 수 있는 작업을 며칠 만에 수행함
- 앞으로는 ANTLR 같은 파서 생성기가 기준 구현을 제공하고, LLM이 PBT와 퍼징으로 동등성을 맞추면서 더 빠른 직접 구현 파서를 작성하는 방식이 일반화될 수 있음