12.12.1 실행 불가능한 환경을 위한 대안: SQLGlot 등 AST 파서를 이용한 쿼리 정규화

12.12.1 실행 불가능한 환경을 위한 대안: SQLGlot 등 AST 파서를 이용한 쿼리 정규화

물리적 샌드박스 데이터베이스를 호출하지 않고 MLOps 평가 스레드 내림차순(Standalone)만으로 채점을 수행해야 하는 혹독한 인프라 제약 상황을 극복하기 위해, 우리는 쿼리를 ’데이터의 컨테이너’가 아닌 **‘소프트웨어 소스 코드(Source Code)’**로 간주하고 접근해야 한다.

만약 가장 어리석고 원시적인 구석기 시대의 텍스트 교집합 방식으로 정답 쿼리와 AI 모델의 예측 쿼리를 파이썬 == 연산자로 단순 비교 연산하게 되면 어떤 참극이 벌어질까? AI가 똑똑하게도 예약어를 소문자로 무심코 작성해 버리거나(select id), 테이블 이름 식별자에 백틱을 감싸거나(`users`), 혹은 쿼리의 논리 흐름과는 아무 관계도 맺지 않는 불필요한 공백과 줄바꿈(Indent)을 넣거나, WHERE A = 1 AND B = 2의 순서를 대수적으로 뒤바꾸는 그 순간, 실제로는 데이터베이스 구조상 완벽한 100점짜리 정답이었음에도 불구하고 문자열이 틀렸다는 이유로 채점 오라클로부터 억울하게 0점(EX=0) 판정을 받고 인공지능이 폐기 처분되는 끔찍한 오답의 살육전이 벌어지고 말 것이다.

이 비극적인 1차원적 블랙박스를 온전히 구원하고 실행 불가능한 환경에서 진리(Truth)를 세울 수 있는 데이터 엔지니어링의 유일한 구명줄이자 최첨단 기술이, 바로 쿼리 문자열을 컴퓨터 언어학(Computational Linguistics) 파서(Parser)의 칼날로 도려내고 분해하여 완전한 논리적 수학 계층 구조인 **추상 구문 트리(AST, Abstract Syntax Tree)**로 입체적 렌더링(Rendering)을 해내는 것이다.

1. 파이썬 sqlglot 라이브러리를 활용한 문법적 방언(Dialect) 중립화 무력화

오라클이 분석해야 할 가장 첫 번째 장벽은 ’벤더사(Vendor) 고유 방언(Dialect)의 파편화’다. 인간이 벤치마크에 적어둔 골든 참고 쿼리는 MySQL의 네이티브 DATE_ADD() 연산자로 작성되었는데, AI 에이전트는 PostgreSQL의 우아한 INTERVAL 문법이나 Oracle의 거친 SYSDATE 문법으로 적절히 영리하게 응답했다면, 이 쿼리는 문자열 비교 기반에서는 영원히 사형 판정을 받게 된다.

우리는 파이썬(Python) 생태계에서 가장 우수하고 압도적인 퍼포먼스를 자랑하는 범용 SQL 파서 라이브러리인 **sqlglot**을 활용하여, 쌍방의 이질적인 벤더사 방언 트리를 단 하나의 유니버설(Universal) 문법 트리 구조로 강제 번역하고 컴파일해 내는 트랜스파일링(Transpiling) 엔진 파이프라인을 구축해야 한다.

import sqlglot

def sanitize_and_normalize_ast_syntax(sql_query: str, source_dialect: str = "mysql") -> str:
    """ 
    더러운 방언의 차이와 AI가 남긴 포맷팅 잉여 찌꺼기를 날려버리고,
    가장 순수한 논리적 뼈대만 남기는 범용 트랜스파일 파이프라인 엔진 
    """
    try:
        # 1. 쿼리 텍스트를 완전히 분해하여 순수 트리 노드(Tree Node) 구조체로 메모리에 로드
        ast_tree = sqlglot.parse_one(sql_query, read=source_dialect)
        
        # 2. 철저한 정규화(Normalization): 모든 테이블/컬럼의 따옴표(")나 백틱(`) 식별자 포맷팅 파괴
        for identifier_node in ast_tree.find_all(sqlglot.exp.Identifier):
            identifier_node.set("quoted", False)
            
        # 3. [핵심] 범용 ANSI SQL 방언(ex. Spark, Presto 등 가장 표준화된 방언)으로 
        # AST를 거꾸로 재조립(Generator)하여 완벽하게 포맷팅이 정규화된 텍스트로 탈바꿈시켜 반환
        return ast_tree.sql(dialect="spark") 
        
    except sqlglot.errors.ParseError:
        # 모델 능력이 처참하여 아예 트리가 성립조차 안 되는 Syntax 붕괴 시 즉각 빈 슬롯 반환
        return "" 

2. 구문 트리 노드(AST Node)의 위상 정렬(Topological Sort) 정규화

방언의 중립화 단계가 평정되고 식별자 대소문자가 통일되었다면, 그 다음으로 무서운 적은 연산자의 **‘논리적 배치 순서(Logical Order) 비결정성’**이다.
앞서 언급한 WHERE user_age > 20 AND is_active = True 구문과 WHERE is_active = True AND user_age > 20 구문은 AST 계층 상에서 양쪽 다 조건식 자식 노드(Child Node)로 깔려있기 때문에 아무리 ANSI 표준으로 거꾸로 뽑아내더라도 텐서 텍스트의 결과물은 끝까지 불일치하게 된다.

이 치명적인 한계를 극복하기 위해, 우리는 한 단계 더 깊이 파고들어 파이썬 스크립트가 AST 트리를 바닥부터 최고차원까지 재귀적으로 순회(Recursive Traversal) 하도록 코어 로직을 작성해야 한다. 이 순회 과정에서 순서와 무관하게 완전히 멱등한 결과 집합을 내는 노드 브랜치(Branch, 예를 들어 논리 교환 법칙이 성립하는 AND / OR 체인 데이터, 혹은 동일 깊이의 무결점 INNER JOIN)들을 강제로 타겟팅한 뒤, 이것들의 하위 자식 노드를 알파벳 식별자 순이나 문자열 해시순으로 사정없이 위상 정렬(Topological Sort) 시켜버리는 가지치기 정렬(Pruning & Sorting) 파이프라인을 구축해야만 한다.

이 궁극적인 위상 기하학적 다듬기 파이프라인을 통과시키게 되면, 육안으로는 완전히 다른 형태의 텍스트로 보이던 엉망진창인 AI의 난해한 쿼리문조차 엔진 컴파일 모듈을 거치고 나면 가장 수학적이고 대수적으로 완벽하게 통일된 1차원 **‘유니버설 정규화 쿼리 문자열(Universal Normalized AST Token String)’**로 매끄럽게 탈바꿈하게 된다. 비로소 인프라의 거대한 데이터베이스 엔진 서버 전원을 켜지도 않은 채, 가장 경제적이고 영리한 오프라인 정적 구문 분석 스택만으로 쿼리의 의미론적 동등성을 측정해 내는 막강한 화이트박스(White-box) 오라클이 화려하게 탄생하게 되는 것이다.