졸업 프로젝트(하루멍록)에서 복잡한 SQL 쿼리 문제를 LangChain으로 해결해보려고 노력한 Text-to-SQL 시스템 개발 경험을 정리한 게시글입니다.
🚀 프로젝트 시작 동기
"백엔드 SQL 쿼리, 너무 복잡한데?"
하루멍록이라는 반려동물 관리 앱 졸업 프로젝트에서 백엔드를 맡았는데, 테이블이 점점 늘어나고 JOIN이 복잡해지면서 SQL 작성이 점점 힘들어졌다. 그래서 "자연어로 물어보면 SQL이 나오면 얼마나 편할까?"라는 생각으로 Text-to-SQL 시스템을 직접 만들어보기로 했다.

📊 하루멍록 데이터베이스 구조
하루멍록은 반려동물 관리 앱답게 다양한 도메인으로 구성되어 있다.
- 회원/인증: member, member_block
- 반려동물 관리: pet, pet_image, member_pet
- 산책 기능: walk, track, walk_position, member_walk, walk_pet
- 일정 관리: event, bath_event, hospital_event 등
- 커뮤니티: post, comment, post_like, comment_like
- 알림/설정: notice, setting, out_box
이 스키마 전체를 어떻게 LLM에게 효율적으로 보여줄지가 핵심 고민 포인트였다.
🛠️ 시도한 구현 방식들
1차 시도: 전체 테이블 한 번에 넣기
질문 → 전체 DDL → SQL 생성
가장 단순한 방식으로, 전체 CREATE TABLE 문을 LLM에게 그대로 던지고 SQL을 생성하게 했다. Context window 안에 모든 테이블의 DDL이 들어가기 때문에, 성능 자체는 꽤 잘 나왔다.
하지만 매 요청마다 전체 스키마를 프롬프트에 포함해야 해서 토큰 낭비와 비용 측면에서 아쉬움이 컸다.
2차 시도: 2단계 접근 – 최종적으로 가장 만족스러웠던 방식 ✅
질문 → 관련 테이블 추출 (gpt-5-nano) → SQL 생성 (gpt-5)
이 방식이 이번 실험에서 가장 균형이 좋고 실제로 쓰기 좋았던 접근이었다.
- 1단계: gpt-5-nano로 전체 DDL을 보고 질문에 필요한 테이블을 상위 5개 정도 추출
- 2단계: 추출된 테이블의 DDL만 컨텍스트로 넣고 gpt-5로 최종 SQL 생성

여기서 전략은 “싸고 작은 모델로 필터링, 큰 모델은 진짜 중요한 데만 쓰자”였다. 전체 스키마를 매번 다 넣는 1차 시도보다 토큰 비용이 줄어들면서도, 필요한 테이블을 꽤 안정적으로 골라줘서 실용적인 타협점이 되었다.
3차 시도: RAG + 청킹
질문 → 벡터 검색 (청크 단위) → SQL 생성
CREATE TABLE 문을 일정 길이로 청킹해서 벡터 DB에 저장하고, 질문과 유사한 청크를 검색해서 SQL을 만드는 방식도 시도했다. 하지만 Retriever만으로는 유사한 테이블을 찾는데 한계가 있었고 CREATE TABLE이 중간에서 잘리면 컬럼 정의나 제약 조건이 끊겨서 스키마를 온전히 이해하기 어려운 문제가 컸다.
결론적으로 “일반 텍스트 문서처럼 자르는 청킹 방식은 DB 스키마에는 맞지 않다”는 걸 확인했다.
4차 시도: RAG + 테이블 단위
질문 → 키워드 추출 → 테이블 단위 벡터 검색 → SQL 생성
이번에는 테이블 하나를 하나의 문서로 보고, CREATE TABLE 전체를 통째로 임베딩해 테이블 단위로 벡터 검색을 했다.
- 임베딩: text-embedding-3-large
- 단위: CREATE TABLE 한 문서 전체
- 메타데이터: 테이블명
- 도메인 키워드 맵: "나" → member, "게시물" → post 같은 매핑을 추가

테이블이 잘리는 문제는 해결됐지만, 코사인 유사도만으로는 어떤 테이블이 진짜로 필요한지 정확하게 고르기가 어려웠다. 특히 JOIN이 필요한 상황에서, post는 잘 잡지만 member까지 항상 같이 잡히지는 않는 식의 아쉬움이 있었다.
🎯 사용 예시 (4차 시도)
4차 시도의 경우 조금 복잡하니 예를 들어 설명하자면 이런 식으로 작동 되었다.
- 질문: "내가 작성한 게시물을 찾아주는 쿼리가 필요해"
- 테이블 생성 : member, post, member_post
- 테이블 생성 바탕으로 SQL 생성 : SELECT * FROM post WHERE member_id = ?




여러 번 확인해보니 RAG 사용 시 "나", "내가 쓴 글" 같은 표현을 member, post와 연결하는 도메인 키워드 맵은 기존 RAG 시스템 대비 정확도를 끌어올리는 데 꽤 도움이 되었다.
🥳 최종 결론 & 얻은 인사이트
정리해보면 다음과 같다.
- 단순 1차 방식(전체 DDL 통째로 넣기)은 정확도는 좋지만 토큰 낭비가 심했다. (1차 시도)
- RAG + 청킹 방식은 스키마가 잘리는 문제가 있어서 DB에는 적합하지 않았다. (3차 시도)
- RAG + 테이블 단위 방식은 구조적으로는 깔끔하지만, 코사인 유사도만으로 필요한 모든 테이블을 고르는 데 한계가 있었다. (4차 시도)
- 테이블 추출(nano 모델) → SQL 생성(일반 모델)이 정확도, 비용, 구현 난이도 측면에서 가장 만족스러웠다. (2차 시도)



이번 시도를 통해 "유행하는 기술(RAG)을 무조건 쓰는 것보다, 내 문제에 맞는 구조를 찾는 게 더 중요하다"는 걸 체감했고, LLM을 백엔드 실무 문제(SQL 복잡도)에 직접 적용해본 좋은 실험이 되었다.
물론 현재 상황에서는 졸업 프로젝트 정도라 LLM에게 모두 맡기는 것이 가장 성능이 좋았지만 실제 회사 프로젝트라면 테이블 숫자가 훨씬 많을 것이고 DDL의 크기가 Context-Window를 넘을 것이기에 그렇게되면 RAG 방식에 비해 부정확해질 것으로 예상된다. 그렇기에 테이블이 더 복잡해질 때를 가정해서도 한번 더 실험해보고 개선점을 찾아봐야겠다는 생각을 했다.
읽어주셔서 감사합니다! 궁금한 점은 댓글로 남겨주세요. 👋
'개발기' 카테고리의 다른 글
| 서버비 0원으로 월 8,000 트래픽 감당하기: Cloudflare Workers + Notion 도입기 (0) | 2025.12.03 |
|---|