Codex와 REST API를 활용한 AI SQL 검증 에이전트 구축하기
요약
Codex와 REST API를 활용하여 SQL 문법 오류를 분석하고 개선 방향을 제안하는 AI 스타일의 SQL 검증 에이전트 구축 방법을 소개합니다. 데이터베이스에 직접 연결하지 않고도 규칙 기반 검증과 설명 모듈을 통해 엔진별 구문 오류를 효과적으로 잡아내는 프로젝트를 다룹니다.
핵심 포인트
- SQL 에러 메시지를 넘어 개발자에게 도움이 되는 구조화된 설명 제공
- Node.js, TypeScript, Zod를 활용한 REST API 기반 에이전트 구현
- ANSI, MySQL, PostgreSQL 등 다양한 DB 엔진별 구문 체크 지원
- 결정론적 규칙과 설명 모듈을 통한 AI 동작 시뮬레이션
대부분의 AI 데이터베이스 데모는 자연어(Natural Language)로 시작합니다.
사용자가 "지난달에 무엇을 구매한 고객이 누구인가요?"라고 물으면, 애플리케이션이 SQL을 생성하고 쿼리가 데이터베이스로 전송됩니다. 이는 유용하지만, 많은 개발자가 매일 직면하는 문제 하나를 간과하고 있습니다. 바로 이미 SQL을 가지고 있으며, 그것이 올바른지, 안전한지, 그리고 사용 중인 데이터베이스 엔진과 호환되는지 알아야 한다는 점입니다.
이 글에서는 두 번째 문제에 초점을 맞춘 작은 교육용 프로젝트를 구축합니다.
우리는 AI 스타일의 SQL 검증 에이전트(SQL validation agent)를 만들 것입니다. 이 에이전트는 SQL 문을 수신하고, 실행 전에 이를 분석하며, 일반적인 실수를 감지하고, 가공되지 않은 데이터베이스 에러보다는 도움이 되는 코드 리뷰에 더 가까운 구조화된 설명을 반환합니다.
이 프로젝트는 데이터베이스에 연결하지 않습니다. ORM을 사용하지 않습니다. OpenAI를 호출하지도 않습니다. "AI" 동작은 결정론적 검증 규칙(Deterministic validation rules)과 설명 모듈(Explanation module)로 시뮬레이션되며, 이를 통해 프로젝트를 쉽게 실행, 테스트 및 확장할 수 있습니다.
소스 코드:
마지막에 우리는 다음을 갖게 될 것입니다:
- Node.js 및 TypeScript 기반 REST API
POST /api/validate엔드포인트- Zod 요청 검증 (Request validation)
- 규칙 기반 SQL 검증 (Rule-based SQL validation)
- ANSI, MySQL, PostgreSQL, Oracle 및 SQL Server를 위한 엔진별 체크
- AI 설명 모듈
- Vitest 테스트
- 검증기를 개선하기 위한 재사용 가능한 Codex Skill
SQL 에러 메시지의 문제점
SQL 에러는 정확하지만, 항상 도움이 되는 것은 아닙니다.
다음과 같은 쿼리를 작성한다고 가정해 봅시다:
SELECT * FORM users;
데이터베이스 파서(Parser)는 FORM 근처에서 에러를 반환할 수 있습니다. 이는 기술적으로는 정확하지만, 개발자는 여전히 실제 문제를 이해해야 합니다:
FORM은 여기서 유효한 키워드가 아닙니다. 의도한 키워드는 아마도 FROM일 것입니다.
이것은 아주 작은 오타입니다. 쉼표 누락, 닫히지 않은 따옴표, 다중 문장, 안전하지 않은 명령 또는 방언별 구문(Dialect-specific syntax)이 있는 경우 경험은 더 나빠집니다.
예를 들어:
SELECT TOP 5 * FROM users;
이는 SQL Server에서는 의미가 통할 수 있지만, 일반적인 PostgreSQL 구문은 아닙니다. PostgreSQL은 다음과 같은 형식을 기대합니다:
SELECT * FROM users LIMIT 5;
전통적인 검증(Validation)은 주로 다음과 같은 질문에 답합니다:
데이터베이스가 이를 파싱(Parse)할 수 있는가?
반면 AI 스타일의 검증 에이전트(Validation agent)는 다음과 같이 답할 수 있습니다:
무엇이 잘못되었는가, 왜 잘못되었는가, 그리고 개발자가 다음에 무엇을 시도해야 하는가?
이러한 차이가 바로 이 프로젝트의 핵심입니다.
SQL 검증에 AI 에이전트가 유용한 이유
AI 에이전트는 단계들을 조정하고, 컨텍스트(Context)를 조사하며, 결과를 설명하고, 조치를 제안할 때 유용합니다. SQL 검증은 이러한 패턴에 매우 잘 부합합니다.
검증 에이전트는 다음과 같은 역할을 수행할 수 있습니다:
- 데이터베이스에 도달하기 전에 SQL 조사
- 읽기 전용 워크플로우(Read-only workflows)에서 안전하지 않은 문장 차단
- 단순 구문 오류(Syntax mistakes) 탐지
- 방언 불일치(Dialect mismatches)에 대한 경고
- 개발자 친화적인 언어로 오류 설명
- IDE, 풀 리퀘스트(Pull request) 봇 또는 CI 파이프라인(CI pipeline)과 통합
중요한 경계는 검증이 실행(Execution) 전에 이루어져야 한다는 점입니다.
이 프로젝트에서 SQL은 신뢰할 수 없는 입력(Untrusted input)으로 취급됩니다. API는 쿼리를 수신하고, 요청 형태를 검증하며, 규칙 기반 체크(Rule-based checks)를 실행하고, 설명을 생성하여 JSON을 반환합니다. 데이터베이스 연결은 필요하지 않습니다.
아키텍처는 다음과 같습니다:
Developer
|
v
...
이는 튜토리얼로 진행하기에 충분히 작지만, 구조는 더 큰 규모의 SQL 리뷰 에이전트가 사용할 수 있는 것과 유사합니다.
프로젝트 아키텍처
리포지토리(Repository)는 몇 가지 명확한 책임(Responsibilities)을 중심으로 구성되어 있습니다:
sql-ai-validator-agent/
src/
agent/
...
요청 흐름(Request flow)은 다음과 같습니다:
HTTP request
|
v
...
이러한 분리는 프로젝트를 유연하게 유지해 줍니다. API 레이어는 SQL 검증의 세부 사항을 알지 못합니다. 규칙 엔진(Rule engine)은 Express에 대해 신경 쓰지 않습니다. 설명 에이전트(Explanation agent)는 구조화된 오류를 수신하여 읽기 쉬운 피드백으로 변환합니다.
이를 통해 프로젝트 전체를 다시 작성하지 않고도 한 부분만을 개선할 수 있는 여유를 가질 수 있습니다.
프로젝트 로컬 실행하기
리포지토리를 클론(Clone)하고 의존성(Dependencies)을 설치하세요:
git clone https://github.com/cs2026086510-a11y/sql-ai-validator-agent.git
cd sql-ai-validator-agent
npm install
개발 서버를 실행하세요:
npm run dev
API는 다음에서 시작됩니다:
다음 명령어로 서버가 살아있는지 확인할 수 있습니다:
curl http://localhost:3000/api/health
예상 응답:
{
"status": "ok"
}
REST API 설계 (REST API Design)
주요 엔드포인트(Endpoint)는 다음과 같습니다:
POST /api/validate
Content-Type: application/json
요청 본문(Request body)은 두 개의 필드를 가집니다:
{
"engine": "postgresql",
"query": "SELECT * FORM users;"
...
engine 필드는 검증기(Validator)에게 어떤 SQL 방언 (SQL dialect)을 사용할지 알려줍니다. 지원되는 값은 다음과 같습니다:
ansi
mysql
postgresql
...
응답은 의도적으로 구조화되어 있습니다:
{
"valid": false,
"engine": "postgresql",
...
이러한 형태는 사람이 읽을 수 있으면서도 도구가 소비할 수 있기 때문에 유용합니다. IDE 확장 프로그램은 errors[0].message를 표시할 수 있습니다. CI 작업은 valid가 false일 때 실패할 수 있습니다. Pull Request 봇은 explanation을 리뷰 댓글로 게시할 수 있습니다.
요청은 Zod를 통해 검증됩니다:
import { z } from "zod";
import { sqlEngines } from "../types/validation.js";
...
Express 라우트는 간결하게 유지됩니다:
apiRouter.post("/validate", (request, response) => {
const parsed = validateSqlSchema.safeParse(request.body);
...
이것이 HTTP 경계(Boundary)의 전부입니다. 그 외의 모든 것은 검증기(Validator)의 영역입니다.
검증 엔진 구축하기 (Building the Validation Engine)
검증기는 다음 사항을 감지합니다:
FROM대신 사용된FORM- 단순
SELECT목록에서 누락된 쉼표 (Commas) - 닫히지 않은 따옴표 (Unclosed quotes)
- 닫히지 않은 괄호 (Unclosed parentheses)
- 여러 개의 SQL 문 (Multiple SQL statements)
- 금지된 쓰기(Write) 또는 스키마(Schema) 문구
- 컬럼이 없는
SELECT - 엔진별 특정 구문 문제
오케스트레이션(Orchestration) 함수는 짧습니다:
export function validateSql(request: ValidationRequest): ValidationResult {
const query = normalizeSql(request.query);
const errors = runValidationRules(query, request.engine);
...
각 규칙은 구조화된 에러를 생성합니다:
if (/FORM/i.test(literalSafeSql)) {
errors.push({
code: "TYPO_FORM",
...
한 가지 중요한 세부 사항은, 검증기(validator)가 키워드를 확인하기 전에 문자열 리터럴 (string literals)을 제거한다는 점입니다. 이는 다음과 같은 쿼리에서 오탐 (false positives)이 발생하는 것을 방지합니다:
SELECT 'FORM is mentioned inside a string' AS note;
또한 이 프로젝트는 다중 문장 (multiple statements)을 차단합니다:
SELECT * FROM users; DROP TABLE users;
해당 쿼리는 데이터베이스가 실행되기도 전에 에러를 반환합니다. 동일한 읽기 전용 (read-only) 정책에 의해 DROP, DELETE, UPDATE, INSERT, ALTER, TRUNCATE와 같은 문장들도 차단됩니다.
이는 해당 명령들이 항상 틀렸기 때문이 아닙니다. 이 검증기가 안전한 교육용 검토 레이어 (educational review layer)로 설계되었기 때문에 차단되는 것입니다.
AI 설명 에이전트 (AI Explanation Agent) 생성하기
설명 에이전트는 언어 모델 (language model)을 호출하지 않습니다. 대신, 검증 코드 (validation codes)를 사용하여 유용한 설명을 생성합니다.
function explainError(error: ValidationError, engine: SqlEngine): string {
switch (error.code) {
case "TYPO_FORM":
...
이를 통해 실제 LLM (Large Language Model)의 비용이나 예측 불가능성 없이 에이전트와 같은 응답을 얻을 수 있습니다. 이는 결정론적 (deterministic)이며, 테스트 가능하고, 검토하기 쉽습니다.
수정의 경우, 에이전트는 수정 사항이 명확할 때만 SQL을 변경합니다:
if (error.code === "TYPO_FORM") {
corrected = replaceWord(corrected, "FORM", "FROM");
}
이러한 절제력이 중요합니다. 검증 에이전트는 테이블 이름을 지어내거나, 누락된 컬럼을 추측하거나, 비즈니스 로직을 다시 작성해서는 안 됩니다. 대신 감지된 문제를 설명하고 안전한 다음 단계를 제안해야 합니다.
실제 API 예시
API가 어떻게 동작하는지 보여주는 예시들입니다.
오타: FROM 대신 FORM 사용
요청 (Request):
curl -s http://localhost:3000/api/validate \
-H "Content-Type: application/json" \
-d '{"engine":"postgresql","query":"SELECT * FORM users;"}'
응답 (Response):
{
"valid": false,
"engine": "postgresql",
...
쉼표 누락
요청 (Request):
쉼표 누락
요청 (Request):
curl -s http://localhost:3000/api/validate \
-H "Content-Type: application/json" \
-d '{"engine":"ansi","query":"SELECT id name email FROM users;"}'
응답 (Response):
{
"valid": false,
"engine": "ansi",
...
Forbidden statement
요청 (Request):
curl -s http://localhost:3000/api/validate \
-H "Content-Type: application/json" \
-d '{"engine":"postgresql","query":"DROP TABLE users;"}'
응답 (Response):
{
"valid": false,
"engine": "postgresql",
...
PostgreSQL dialect issue
요청 (Request):
curl -s http://localhost:3000/api/validate \
-H "Content-Type: application/json" \
-d '{"engine":"postgresql","query":"SELECT TOP 5 * FROM users;"}'
응답 (Response):
{
"valid": false,
"engine": "postgresql",
...
유효한 쿼리 (Valid query)
요청 (Request):
curl -s http://localhost:3000/api/validate \
-H "Content-Type: application/json" \
-d '{"engine":"postgresql","query":"SELECT id, name FROM users WHERE active = true;"}'
응답 (Response):
{
"valid": true,
"engine": "postgresql",
...
재사용 가능한 Codex Skill 생성
이 저장소에는 다음 위치에 재사용 가능한 Codex Skill이 포함되어 있습니다:
skill/SKILL.md
이 스킬은 Codex가 검증기(validator)와 어떻게 작동해야 하는지 설명합니다:
- 검증기의 목적
- 수용하는 입력값
- 반환해야 할 출력값
- 따라야 할 검증 워크플로우
- 중요한 보안 제약 조건
- 새로운 규칙을 안전하게 추가하는 방법
워크플로우 섹션은 다음과 같습니다:
1. SQL 의미를 변경하지 않으면서 공백 정규화(Normalize whitespace).
2. SQL 분석 전에 요청 구조 유효성 검사.
3. 실행 가능한 다중 구문 거부.
...
이것은 향후 개선 사항들이 동일한 운영 지침에 의해 안내될 수 있다는 점에서 유용합니다. 예를 들어, 개발자는 Codex에게 Oracle 문법에 대한 새로운 규칙을 추가하고, 테스트를 업데이트하며, API 응답 계약(API response contract)을 변경하지 않도록 요청할 수 있습니다.
이 스킬은 프로젝트를 일회성 튜토리얼보다 더 재사용 가능한 무언가로 만듭니다.
테스트 (Testing)
이 프로젝트는 Vitest를 사용합니다:
npm test
하나의 테스트는 오타 흐름(typo flow)을 확인합니다:
it("FORM 대신 FROM을 감지하고 수정된 SQL을 제안합니다", () => {
const result = validateSql({
engine: "postgresql",
...
검증기(validator)의 경우 모든 규칙이 오탐(false positives)을 발생시킬 수 있기 때문에 테스트가 특히 중요합니다. 권장되는 워크플로우는 다음과 같습니다:
- SQL 문제에 대해 실패하는 테스트를 추가합니다.
- 해당 문제를 잡아낼 수 있는 가장 작은 규칙을 구현합니다.
- 플래그(flag)가 지정되지 않아야 하는 테스트를 최소 하나 이상 추가합니다.
- API 응답을 안정적으로 유지합니다.
이렇게 해야 검증기가 노이즈(noisy)가 되지 않고 유용하게 유지될 수 있습니다.
실제 프로젝트에서의 활용 방법
이 교육용 API는 여러 워크플로우에 적용될 수 있습니다.
IDE에서는 개발자가 .sql 파일을 저장할 때 확장 프로그램이 /api/validate를 호출할 수 있습니다. 응답은 짧은 설명과 함께 에디터 진단(diagnostics) 정보가 될 수 있습니다.
CI(지속적 통합)에서는 스크립트가 SQL 파일들을 스캔하고, 응답에 차단(blocking) 오류가 포함되어 있으면 빌드를 실패시킬 수 있습니다.
풀 리퀘스트(pull requests)에서는 리뷰 봇이 다음과 같은 댓글을 게시할 수 있습니다:
SQL 검증 실패.
문제: TOP은 유효한 PostgreSQL SELECT 구문이 아닙니다.
...
더 큰 AI 시스템에서는 모델이 이 검증기를 도구(tool)로 사용할 수 있습니다. 모델은 쿼리가 안전한지 추측할 필요가 없습니다. API를 호출하고, 구조화된 응답을 읽은 뒤, 그 결과를 개발자에게 설명할 수 있습니다.
향후 개선 사항
이 프로젝트는 의도적으로 작게 설계되었지만, 아키텍처는 확장 가능성을 열어두고 있습니다.
다음 단계는 더 깊은 방언(dialect) 지원을 위해 실제 SQL 파서(parser)를 추가하는 것입니다. 정규 표현식(Regular expressions)은 튜토리얼용으로는 괜찮지만, 중첩된 쿼리(nested queries), 함수, 조인(joins), 그리고 복잡한 표현식에 대해서는 파서 기반의 검증이 더 신뢰할 수 있습니다.
또 다른 개선 사항은 줄(line) 및 열(column) 위치 정보입니다. IDE와 풀 리퀘스트 댓글은 문제의 정확한 위치를 가리킬 수 있을 때 훨씬 더 유용합니다.
정책 설정(Policy configuration) 또한 도움이 될 것입니다. 어떤 팀은 대시보드에서는 모든 쓰기 문(write statements)을 차단하고 싶어 하지만, 마이그레이션(migration) 폴더에서는 INSERT나 ALTER를 허용하고 싶어 할 수도 있습니다.
마지막으로, 설명(explanation) 모듈은 진정한 LLM 기반 에이전트(agent)가 될 수 있습니다. 그렇게 된다면, 결정론적 규칙(deterministic rules)이 여전히 먼저 실행되어야 합니다. 모델은 구조화된 결과(structured findings)를 설명해야 하며, 검증 결과(validation results)를 자유롭게 지어내서는 안 됩니다. 또한 프롬프트 인젝션(prompt-injection) 방어 수칙을 따라야 합니다: SQL을 신뢰할 수 없는 입력(untrusted input)으로 취급하고, 지시 사항(instructions)과 사용자 콘텐츠(user content)를 분리하며, 모델의 출력을 반환하기 전에 검증해야 합니다.
결론
Text-to-SQL은 인기가 높지만, SQL 검증(SQL validation) 또한 그만큼 중요합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기