코드를 거의 보지 않고 만든 70배 빠른 SQL 파서

7 hours ago 3
  • 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% 정확도에 도달하는 데 필수는 아니었지만, 매우 미묘한 사례를 찾는 데 도움을 줌

실패를 찾고 고치는 자동 반복 과정

  • 최종 개발 루프는 다음 단계를 자율적으로 반복함
    1. PBT, 실제 쿼리 집합, 회귀 테스트, 경계 사례 탐색으로 새로운 실패를 생성함
    2. 실패를 축소해 계속 확장되는 회귀 테스트 목록에 추가함
    3. 일반적인 해결책을 우선하면서 문법과 C++ 기준 구현의 처리 방식을 확인함
    4. 수정한 뒤 사람이 읽을 수 있는 한 문단 요약을 출력함
    5. 전체 회귀 테스트를 실행해 모든 사례가 통과하는지 확인함
    6. 같은 과정을 다시 자율 실행함
  • 새 파서가 훨씬 빨랐기 때문에 프로덕션에서 기존 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와 퍼징으로 동등성을 맞추면서 더 빠른 직접 구현 파서를 작성하는 방식이 일반화될 수 있음
Read Entire Article