AI를 활용한 안전한 SQL: 실행 전 쿼리를 검증하는 어시스턴트 구축 방법
요약
LLM이 생성한 SQL의 문법적 오류와 보안 취약점을 해결하기 위해, 실행 전 쿼리를 검증하는 AI 어시스턴트 구축 방법을 소개합니다. Hugging Face의 smolagents를 활용하여 추론 계층을 구성하고, 자체 보안 계층인 SQL Guard를 통해 안전한 데이터 접근을 보장합니다.
핵심 포인트
- LLM의 Text-to-SQL 생성 시 발생할 수 있는 의미론적 오류와 보안 위험 분석
- smolagents의 CodeAgent를 활용한 반복적 추론 및 코드 실행 사이클 구현
- 데이터베이스 실행 전 결정론적 검증을 위한 SQL Guard 보안 계층 도입
서론
SQL을 직접 작성하는 것은 의도치 않게 많은 사람을 데이터의 세계에서 소외시키는 기술 중 하나입니다. 프로덕트 매니저(Product Manager), 지원 에이전트, 심지어 주니어 개발자들조차 데이터베이스에 무엇을 묻고 싶은지는 정확히 알지만, 이를 SQL 문법으로 어떻게 표현해야 하는지는 항상 알지 못합니다.
Text-to-SQL 시스템은 바로 이 문제를 해결하려고 시도합니다. 즉, 자연어로 된 질문을 실제 쿼리로 변환하여 마찰 없이 데이터를 반환하는 것입니다.
하지만 SQL을 자동으로 생성하는 것은 문제의 끝이 아니라 시작일 뿐입니다. 거대 언어 모델 (LLM)은 문법적으로는 올바르지만 의미론적으로는 완전히 잘못된 쿼리를 생성할 수 있습니다. 잘못된 조인 (join), 부정확한 필터, 또는 최악의 경우 읽기 전용이어야 하는 데이터를 수정하려는 시도 등이 발생할 수 있습니다.
이 글에서 저는 단순히 쿼리를 생성하는 것뿐만 아니라, 실행하기 전에 결정론적인 방식으로 쿼리를 검증하는 SQL AI 어시스턴트를 구축합니다. 이를 위해 추론 계층으로 Hugging Face의 smolagents를 사용하고, 어떤 쿼리도 데이터베이스에 닿기 전 마지막 필터 역할을 하는 자체 보안 계층 (SQL Guard)을 사용합니다.
이 프로젝트의 전체 소스 코드는 기사 끝에 링크된 공개 저장소에서 확인할 수 있습니다.
왜 LLM에게 SQL을 요청하는 것만으로는 부족할까요?
가장 단순한 Text-to-SQL 파이프라인 (pipeline)은 다음과 같습니다:
- 데이터베이스 스키마 (schema)와 사용자의 질문을 LLM에 전송합니다.
- LLM에게 SQL 쿼리를 반환하도록 요청합니다.
- 해당 쿼리를 맹목적으로 실행하고 결과를 보여줍니다.
이 방식은... 작동을 멈출 때까지는 잘 작동합니다. 문제는 LLM에 대한 단 한 번의 호출이 취약하다는 점에 있습니다. 모델은 문법적으로는 유효하지만 의미상으로는 틀린 쿼리를 생성할 수 있으며, 시스템은 오류를 감지할 메커니즘 없이 이를 그대로 실행하게 됩니다.
더 최악인 것은, 실제 환경에서 시스템 수준에서 명시적으로 금지하지 않는다면 모델이 DELETE나 UPDATE를 주입하려고 시도하는 것을 막을 수 있는 것이 아무것도 없다는 점입니다. 단순히 _프롬프트 (prompt)_를 통해 말하는 것은 보안을 보장하지 않으며, 모델이 무시할 수 있는 제안에 불과합니다. 이것이 바로 이 프로젝트가 메우고자 하는 아키텍처적 공백입니다.
smolagents란 무엇이며 왜 여기서 중요한가?
smolagents는 Hugging Face의 경량 에이전트 _프레임워크 (framework)_로, 제작자들은 이를 "코드로 생각하는 에이전트를 위한 미니멀리스트 라이브러리"라고 설명합니다. 모델에게 경직된 JSON 형식으로 도구 호출 (tool calls)을 생성하도록 강요하는 대신, CodeAgent는 반복적인 추론 사이클의 일부로서 실제 Python 코드를 작성하고 실행합니다:
- 생각하기 (Pensar): 모델이 다음에 무엇을 할지 추론합니다.
- 행동하기 (Actuar): 도구(이 경우 SQL 엔진)를 호출하는 Python 코드를 작성합니다.
- 관찰하기 (Observar): 해당 도구의 결과가 모델의 컨텍스트로 돌아옵니다.
- 반복 또는 응답하기 (Repetir o responder): 에이전트가 쿼리를 수정해야 할지(예: 구문 오류가 발생한 경우) 또는 최종 응답을 제공할 수 있는지 결정합니다.
이 사이클은 매우 유용하지만, 그 자체만으로는 충분하지 않습니다. 에이전트는 의심스러운 결과에 대해 스스로 수정할 수는 있지만, 설계상 어떤 작업이 비즈니스 로직에 파괴적인지를 알지는 못합니다. 그 책임은 추가적이고 결정론적인 계층인 **검증기 (validator)**가 맡아야 합니다.
솔루션 아키텍처
모델이 제안하는 모든 SQL을 직접 실행하는 대신, 우리는 다음과 같은 흐름을 가진 시스템을 설계했습니다:
- 사용자가 자연어로 질문을 합니다.
- AI 에이전트가 스키마 (schema)에 대해 추론한 후 SQL 쿼리를 제안합니다.
- 엄격한 검증기가 해당 쿼리를 가로채서 검토합니다.
- 안전하다고 분류된
SELECT쿼리만 실행됩니다. - 결과가 처리되어 사용자에게 반환됩니다.
이 추가적인 단계 —검증(validation)— 이야말로 단순한 기술적 데모를 실제 운영 환경의 데이터에 연결할 수 있는 진정한 어시스턴트로 격상시키는 요소입니다.
사용자 ──▶ 에이전트 (smolagents) ──▶ SQL Guard ──▶ 데이터베이스
│ │
추론 및 위험 시
요청 수행 거부
...
프로젝트 구축하기: 판매 데이터베이스
예제의 재현성을 유지하기 위해, customers와 sales라는 두 개의 간단한 테이블을 가진 SQLite 데이터베이스를 사용합니다.
1. 스키마 (Schema) 정의
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
...
2. 예시 데이터(Seed) 로드
INSERT INTO customers (id, name, city) VALUES
(1, 'Alan Payne', 'Lima'),
(2, 'Marta Lopez', 'Arequipa');
...
3. Python 연결
import sqlite3
def get_connection():
...
핵심 요소: SQL 검증기 (SQL Guard)
이것이 프로젝트의 근간이며, 기본적인 Text-to-SQL 방식과 차별화되는 지점입니다. 에이전트가 생성한 모든 쿼리를 실행하기 전에, 해당 명령문은 반드시 Python의 이 필터를 통과해야 합니다:
BLOCKED = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER"]
def is_safe_sql(query: str) -> bool:
...
이 검증기는 타협 불가능한 세 가지 결정론적(deterministic) 규칙을 적용합니다:
SELECT문만 허용됩니다.- 여러 개의 문장이 연결된 다중 문장(multiple statements)은 허용되지 않습니다.
- 서브쿼리(subqueries) 내부를 포함하여 문자열의 어느 곳에서든 파괴적인 단어는 차단됩니다.
에이전트와 검증기 통합
실행 흐름은 다음과 같이 에이전트의 요청을 가로챕니다:
from db import get_connection
from validator import is_safe_sql
...
여기에 아키텍처상의 차이가 존재합니다. LLM 모델은 환각(hallucination)을 일으킬 수 있고, 사용자는 프롬프트(prompt)를 통해 악성 코드를 주입하려고 시도할 수도 있지만, 애플리케이션이 해당 제안을 실행할지 여부를 결정하는 최종 권한을 보유합니다. 에이전트는 보조하며, 시스템이 통제합니다.
사용 예시
사용자가 선의로 다음과 같이 질문하는 경우:
월별 총 매출액은 얼마인가요?
에이전트는 다음과 같은 쿼리를 생성합니다:
SELECT strftime('%Y-%m', order_date) AS month,
SUM(total_amount) AS total
FROM sales
...
검증기 (Validator)는 이것이 안전한 SELECT 문인지 확인하고 승인하며, 시스템은 실제 결과를 반환합니다.
공격이나 모델의 환각 (Hallucination)이 발생하면 어떻게 될까요?
만약 에이전트가, 혹은 사용자가 프롬프트 (Prompt)를 통해 DELETE FROM sales WHERE total_amount > 100;를 실행하려고 시도한다면, 검증기는 밀리초 단위로 이를 거부하고 데이터베이스 엔진에 접근하기도 전에 보안 경고를 반환합니다.
LLM 직접 호출 vs. 검증 기능이 있는 에이전트
| 기준 | LLM 직접 호출 (Zero-shot) | 에이전트 + 검증기 |
|---|---|---|
| 생성 | 피드백 없는 단일 시도 | 수정 능력을 갖춘 반복적 시도 |
| ... |
결론은 명확합니다. 검증기가 AI 고유의 위험을 완전히 제거하지는 못하지만, 모델의 암묵적인 결정을 시스템의 명시적인 규칙으로 변환함으로써 위험을 완화합니다.
보안: 프로덕션 환경을 위한 추가 고려 사항
제안된 SQL Guard 외에도, 기업 수준의 실제 솔루션은 다음 사항을 고려해야 합니다:
- 최소 권한 원칙 (DB roles): 데이터베이스에 연결할 때 관리자 권한이 아닌, 물리적으로
SELECT권한만 가진 제한된 역할 (Role)을 사용합니다. - 원시 테이블 대신 뷰 (Views) 사용: 비밀번호나 개인정보 (PII)가 포함된 컬럼을 숨기고, 엄선된 뷰 (Views)만을 AI에 노출합니다.
- 강제된 제한 (
LIMIT): 모델이 실수로 수백만 개의 행을 스캔하는 것을 방지하기 위해, 각 유효한 쿼리 끝에 프로그래밍 방식으로LIMIT 50을 강제합니다. - 로깅 (Logging): 도구 사용 내역을 시간이 지남에 따라 감사 (Audit)할 수 있도록 모든 질문, 생성된 쿼리 및 지연 시간 (Latency)을 저장합니다.
실제 사용 사례
- 동적 대시보드 (Dashboards dinámicos) — 엔지니어링 팀에 의존하지 않고도 비즈니스 사용자가 즉석에서 안전한 집계 (Aggregation)를 생성할 수 있도록 합니다.
- 지원 플랫폼 (Plataformas de soporte) — 엄격한 읽기 정책 (Read policies) 하에 상담원이 자연어 (Natural language)를 사용하여 고객의 운영 데이터를 검색할 수 있도록 합니다.
- 학술 및 연구 프로젝트 (Proyectos académicos y de investigación) — 데이터 무결성 (Data integrity)을 해치지 않으면서 SQL 구문 (Syntax)의 마찰을 제거하여 관계형 데이터베이스 (Relational databases)의 사용을 장려합니다.
결론
Text-to-SQL 아키텍처의 사용은 파운데이션 모델 (Foundational models)에 작업을 위임할 때 방어적 소프트웨어 엔지니어링 (Defensive software engineering)이 필요함을 보여줍니다. 눈에 띄는 프롬프트 (Prompt)와 검증된 시스템 사이의 차이는 궁극적으로 데모 (Demo)와 프로덕션 시스템 (Production system)의 차이입니다.
smolagents의 코드 기반 접근 방식은 LLM의 추론 (Reasoning)을 투명하고 감사 가능하게 (Auditable) 만듭니다. 여기에 SQL 검증 레이어를 추가하면, 휘발성인 프롬프트의 보안을 모델이 잘 행동하기를 기대하는 방식이 아닌, 신뢰할 수 있는 인프라 규칙으로 전환할 수 있습니다.
공개 저장소 (Public Repository):
이 프로젝트의 전체 작동 코드 (데이터베이스, 검증기, 오케스트레이션 및 데모)는 제 GitHub 계정에서 확인할 수 있습니다:
🔗 https://github.com/KiaaraZM/safe-sql-ai
참고 문헌
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기