AI를 사용하여 데이터베이스와 대화하는 방법: 자연어 기반 SQL 생성기
요약
LLM을 활용하여 자연어를 SQL 쿼리로 변환하는 Text-to-SQL 구현 방법을 소개합니다. Hugging Face의 T5 모델을 사용해 SQLite 데이터베이스와 연동하는 아키텍처와 실제 구현 시 발생하는 스키마 불일치 문제를 다룹니다.
핵심 포인트
- Hugging Face의 WikiSQL 파인튜닝된 T5 모델 활용
- 자연어 질문을 SQL 쿼리로 변환하는 Text-to-SQL 패턴 구현
- 모델이 실제 DB 스키마를 인지하지 못하는 한계점 분석
- 집계 함수 문법 오류 및 대소문자 구분 문제 해결 필요성
기술 문서 — Unit 3, 데이터베이스 II
서론
비즈니스 담당자와 데이터 사이의 가장 흔한 장벽 중 하나는 바로 SQL 그 자체입니다. 정보가 존재한다는 사실을 아는 것만으로는, 그 정보에 접근하기 위해 JOIN을 직접 작성해야 한다면 큰 도움이 되지 않습니다. 최근 몇 년 동안, 언어 모델 (LLMs)은 Text-to-SQL이라고 알려진 작업을 통해 이러한 격차를 줄이기 시작했습니다. 사용자가 자연어로 질문을 작성하면 모델이 그에 상응하는 SQL 쿼리를 생성하는 방식입니다.
이 글에서는 Hugging Face의 공개 모델, 예시 SQLite 데이터베이스, 그리고 AI가 위험한 명령을 실행하는 것을 방지하는 검증 레이어를 사용하여 이 패턴의 작지만 완전한 구현 사례를 보여줍니다. 전체 코드는 글 마지막에 링크된 공개 저장소(repository)에서 확인할 수 있습니다.
아키텍처
질문 (영어)
-> WikiSQL로 파인튜닝(fine-tuned)된 T5 모델 (Hugging Face)
-> 모델이 생성한 "가공되지 않은 (raw)" SQL
...
예시 데이터베이스는 기술 상점의 판매를 시뮬레이션하며, 제품, 카테고리, 가격, 수량, 도시, 고객 및 날짜를 포함하는 ventas 테이블을 가지고 있습니다:
CREATE TABLE ventas (
id INTEGER PRIMARY KEY,
product TEXT NOT NULL,
...
모델: Hugging Face + WikiSQL로 파인튜닝된 T5
SQL을 생성하기 위해 저는 Hugging Face에서 사용할 수 있고 WikiSQL 데이터셋으로 학습된 T5 모델인 mrm8488/t5-base-finetuned-wikiSQL을 사용했습니다. 사용법은 다음과 같습니다:
from transformers import T5ForConditionalGeneration, T5Tokenizer
MODEL_NAME = "mrm8488/t5-base-finetuned-wikiSQL"
...
how many products were sold in Madrid라는 질문에 대해 모델은 다음과 같은 결과를 내놓습니다:
SELECT COUNT(product) FROM table WHERE city = Madrid
이를 실행하기 전에 해결해야 할 두 가지 세부 사항이 있습니다. 바로 일반적인 테이블 명칭인 table (WikiSQL이 학습용 테이블에 이름을 붙이는 방식임)과 따옴표가 없는 Madrid 값입니다.
거의 아무도 보여주지 않는 부분: 모델은 당신의 스키마를 알지 못한다
실제 질문으로 모델을 테스트했을 때, 튜토리얼에서 흔히 볼 수 있는 "장난감 (toy)" 예제들에서는 보이지 않는 세 가지 문제가 나타났습니다:
- 때때로 집계 함수 (aggregation functions)의 괄호를 누락합니다:
SELECT COUNT(Product) FROM table WHERE City = 'Madrid'대신SELECT COUNT Product FROM table WHERE City = Madrid와 같이 출력됩니다. - 모델은 우리의 테이블 이름이
ventas라는 것이나, 컬럼 (columns)이product,category,price,quantity,city,customer,sale_date라는 사실을 알지 못합니다. WikiSQL은 수천 개의 서로 다른 테이블로 학습되었기 때문에, 모델은 질문의 단어로부터 그럴듯한 컬럼 이름을 "지어냅니다" (city가 때로는City로, 때로는Location으로 나타남). - 데이터의 실제 대소문자 구분을 항상 준계하지는 않습니다: 데이터베이스에 저장된 값이
Barcelona임에도 불구하고City = barcelona(소문자)를 생성했습니다. 이대로 실행하면 쿼리가 실패하지는 않지만, 조용히 0개의 행을 반환합니다 — 이는 작동하는 것처럼 보이기 때문에 가장 최악의 유형의 오류입니다.
모델이 반환한 문자열을 그대로 cursor.execute()에 전달하고 싶은 유혹이 생길 수 있습니다. 하지만 이는 SQL 인젝션 (SQL injection)의 문을 열거나, 모델이 DROP TABLE을 "환각 (hallucinate)" 하거나, 조용한 거짓 음성 (false negative)이 올바른 결과처럼 통과하게 만드는 가장 빠른 방법이기도 합니다. 따라서 textsql/sanitize.py는 어떤 SQL이 데이터베이스에 닿기 전에 네 가지 작업을 수행합니다:
1. 집계 함수에서 누락된 괄호를 복구합니다:
_AGG_NO_PARENS = re.compile(
r"\b(COUNT|SUM|AVG|MIN|MAX)\s+(?!\)([A-Za-z_]\w*)(?:\s+[A-Za-z_]\w*)*\s+FROM",
re.IGNORECASE,
...
2. 일반적인 테이블 이름을 변경하고, 텍스트 값에 따옴표를 붙입니다 (첫 번째 버전과 동일).
3. 유의어 사전을 사용하여 컬럼을 정규화합니다. 모델이 실제 스키마 (schema)를 볼 수 없기 때문입니다:
COLUMN_SYNONYMS = {
"product": "product", "item": "product",
"category": "category", "type": "category",
...
이를 통해, COUNT Sale (존재하지 않는 컬럼)은 결국 COUNT(*)로 번역되며, 이는 누군가가 "판매가 얼마나 있었나요"라고 말할 때 의도하는 바와 정확히 일치합니다.
4. 조용한 거짓 음성(false negatives)을 방지하기 위해 대소문자를 구분하지 않고 비교하기:
def _case_insensitive_compare(sql: str) -> str:
return _STRING_EQUALITY.sub(
lambda m: f"LOWER({m.group(1)}) = LOWER('{m.group(2)}')", sql
...
마지막으로, 체이닝된 명령어나 파괴적인 키워드 없이 오직 SELECT만 실행되는지 **검증(validate)**합니다:
FORBIDDEN_KEYWORDS = re.compile(
r"\b(insert|update|delete|drop|alter|attach|detach|pragma|create|replace|vacuum)\b",
re.IGNORECASE,
...
추가적인 방어책으로, SQLite 연결을 읽기 전용 모드 (read-only mode) (file:ventas.db?mode=ro)로 열어, 검증을 통과한 무언가가 유출되더라도 데이터베이스를 수정할 수 없도록 합니다.
터미널에서 Streamlit을 이용한 웹 데모까지
엔진이 작동하므로, Streamlit을 사용하여 웹 인터페이스로 감싸는 데는 30줄도 채 걸리지 않습니다:
import streamlit as st
from textsql.db import TABLE_NAME, run_query
from textsql.model import TextToSQL
...
결과물은 예를 들어 what is the total price where city is Barcelona라고 입력하면, 생성된 SQL과 결과 테이블을 실시간으로 볼 수 있는 페이지입니다.
결과 및 한계
예시 ventas 테이블(120개의 합성 데이터 행)과 정규화(normalization) 레이어가 갖춰진 상태에서, 다음과 같은 질문들은 처음부터 끝까지 올바르게 실행됩니다:
"how many products were sold in city Madrid"
-> SELECT COUNT(product) FROM ventas WHERE LOWER(city) = LOWER('Madrid')
...
그럼에도 불구하고, 이 프로토타입에는 명확한 한계가 있습니다:
- 질문에
COLUMN_SYNONYMS에 포함되지 않은 단어가 사용된 경우, 쿼리 실행 시 오류가 발생할 수 있습니다 (잘못된 쿼리를 실행할 위험을 피하기 위해 화면에 오류를 표시합니다). - 여러 테이블이나 서브쿼리 (subqueries)가 필요한 질문 (WikiSQL은 한 번에 하나의 테이블만 사용하는 예제로 학습되었습니다).
- 스페인어 질문: 모델이 영어로 학습되었기 때문에 질문을 보내기 전에 번역해야 합니다 (프로젝트의 자연스러운 확장 방향은 자동 번역 단계를 추가하는 것입니다).
이러한 한계점들은 그 자체로 좋은 교훈을 줍니다. 오늘날의 Text-to-SQL 모델은 간단한 탐색적 쿼리를 위한 훌륭한 보조 도구이지만, 프로덕션 (production) 환경에서 신뢰하기 위해서는 여전히 인간 또는 자동화된 검증 레이어가 필요합니다.
결론
SQL 데이터베이스와 AI 모델을 결합하는 데 복잡한 인프라가 필요하지는 않습니다. Hugging Face의 공개 모델, 약 100줄의 Python 코드, 그리고 주의 깊은 데이터 정제 (sanitization) 레이어만 있다면, 단 하루 만에 기능적인 "데이터베이스와의 대화" 프로토타입을 구축할 수 있습니다. 흥미로운 도전 과제는 SQL을 생성하는 것이 아니라, 실행하기 전에 이를 얼마나 신뢰할지 결정하는 데 있습니다.
참고 문헌
- AI를 사용하여 모든 데이터베이스와 통신하는 방법: SQL 쿼리를 사용하여 자신만의 데이터 추출기를 만드세요.
- Text to SQL — Hugging Face.
- Streamlit과 Hugging Face를 사용하여 텍스트로부터 SQL 쿼리 생성기 만들기 (Medium), 작성자: Kuhelidey.
- 전체 코드: https://github.com/JhonyVargas/text-to-sql-ia
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기