Go에서 PostgreSQL 파서 구축: 오픈소스 공개 후 발생한 문제
Source: Dev.to

postgresparser 은 순수 Go 로 구현된 PostgreSQL SQL 파서입니다. SQL 텍스트를 실행하지 않고도 구조화된 메타데이터(테이블, 컬럼, 조인, 필터, DDL 액션, 파라미터)로 변환합니다.
우리는 이 파서가 견고하다고 생각했습니다. 오픈소스를 공개하고 보니 그 생각이 틀렸다는 것을 알게 되었습니다.
우리가 배운 점을 정리합니다.
가장 큰 변화: 사용 사례 확장
우리는 자체 워크플로우를 위해 파서를 만들었습니다. 공개 사용자들은 매우 다른 작업량을 가지고 나타났습니다. 출시 후 첫 주에 대부분의 피드백은 결정론적 배치 파싱에 초점이 맞춰졌습니다.
내부 가정이 즉시 깨졌다
단일 팀 내부에서는 모두가 규칙을 “알고” 있기 때문에 모호한 동작이 살아남습니다. 공개 사용자들은 그 맥락을 가지고 있지 않습니다.
첫 번째 압력점은 다중 문장 SQL이었습니다. 우리는 ParseSQL(단일 문장)을 가지고 있었고 배치 파싱이 “충분히 가깝다”고 가정했습니다. 그렇지 않았습니다.
사람들은 파서를 다음과 같이 사용하고 있었습니다:
- CI linting pipelines
- Production tools
- LLM wrappers
우리가 명확히 답변하지 못한 실용적인 질문들:
- 정확히 어떤 문장이 실패했나요?
- 이것이 경고인지 심각한 오류인지?
- 진단 정보를 원본 SQL 텍스트에 신뢰성 있게 매핑할 수 있나요?
그러한 질문들은 암시된 동작에 의존하는 대신 엄격한 계약을 정의하도록 강요했습니다. 도구가 SQL을 대량으로 소비한다면, 배치 연관성은 모든 것입니다.
깨진 동작 예시
SELECT 1;
SELECT FROM;
SELECT 2;초기 배치 동작은 결과가 압축되고 진단이 문장 우선이 아니었기 때문에 상관관계를 파악하기 어려웠습니다. CI 검사나 마이그레이션 도구에서는 “배치에서 무언가가 실패했습니다”라는 메시지가 실질적인 조치를 취하기 어렵게 만들었습니다.
이제 각 문장은 (Index, RawSQL, Query, Warnings)와 같은 결정적인 상관관계를 갖게 되어, 하위 코드가 정확한 원본 문장을 가리킬 수 있습니다.
전후 API 차이
- type ParseBatchResult struct {
- Queries []*ParsedQuery
- Warnings []ParseWarning
- TotalStatements int
- ParsedStatements int
- }
+ type StatementParseResult struct {
+ Index int
+ RawSQL string
+ Query *ParsedQuery // nil => IR conversion failure
+ Warnings []ParseWarning // statement‑scoped warnings
+ }
+
+ type ParseBatchResult struct {
+ Statements []StatementParseResult
+ TotalStatements int
+ ParsedStatements int
+ HasFailures bool
+ }그 형태는 빠른 데모에는 덜 편리하지만 실제 통합에는 훨씬 더 좋습니다.
실제 환경에서의 Real SQL은 테스트 픽스처보다 더 난잡합니다
오픈소스 사용으로 내부 테스트에서는 보지 못했던 SQL 형태가 등장했습니다:
- 뒤에 붙는 세미콜론과 이상한 공백
- 전체적으로는 유효하지만 중간에 있는 잘못된 구문
- DDL과 DML 스크립트가 혼합된 경우
- DDL 경로에서의
ONLY변형
파서는 모호해지지 않으면서 회복력을 가져야 했습니다. 이를 위해 다음이 필요했습니다:
- 더 나은 문장 수준 경고 할당
- 명시적인 실패 의미(
Query == nil) - DDL 관계 추출 경로 전반에 걸친 더 엄격한 처리
Concrete snippet (current behavior)
batch, err := postgresparser.ParseSQLAll(sql)
if err != nil {
log.Fatal(err)
}
fmt.Printf("total=%d parsed=%d has_failures=%t\n",
batch.TotalStatements, batch.ParsedStatements, batch.HasFailures)
for _, stmt := range batch.Statements {
fmt.Printf("idx=%d failed=%t warnings=%d raw=%q\n",
stmt.Index, stmt.Query == nil, len(stmt.Warnings), stmt.RawSQL)
}그것이 사람들이 요구한 통합 모델이다: 결정적이고, 검사 가능하며, 가장 좋은 의미에서 지루한 모델이다.
왜 이것이 중요한가
오픈소스는 우리가 모서리 사례를 대충 넘길 수 있는 능력을 없앴습니다. 개발 루프는 다음과 같이 바뀌었습니다:
- 구현
- 도전을 받음
- 단순화
- 테스트로 동작을 고정
- 계약을 문서화
그 루프 덕분에 postgresparser는 내부 전용 도구였을 때보다 더 나아졌습니다. 내부 도구는 모호함을 견딜 수 있지만, 공개 라이브러리는 그렇지 못합니다.
postgresparser 위에 뭔가를 구축하고 있다면 이슈를 열어 주세요. 실제 SQL은 계약을 지속적으로 개선합니다.