추천 키워드

K-Metrics 제작 후기 #2

검색해도 안 나오는 SQL을 새로 만들다

검색으로 못 찾은 쿼리는 새로 만들어야 했습니다
최고운
최고운 AX팀
AI로 더 나은 일하는 방식을 고민하는 AI 엔지니어

업무의 언어를 이해하고, 함께 쿼리를 고치기까지

지난 편에서는 동료들이 업무에서 사용하고 확인한 쿼리를 모아, 자연어로 찾아 재사용하는 이야기를 했어요. 등록된 쿼리로 답할 수 있는 질문이라면 새 SQL을 만들지 않고도 업무를 시작할 수 있죠.

그렇다면 검색이 “못 찾았어요”라고 답한 요청은 어떻게 처리할까요? 등록된 쿼리가 다루지 않는 대상을 조회하거나, 다른 기준으로 집계해야 한다면 새로운 SQL이 필요합니다.

이번 편은 그다음을 다룹니다. 검색으로 답하기 어려운 요청을 K-Metrics가 어떻게 새로운 SQL로 만드는지, 그 과정에서 업무 용어를 해석하고 필요한 조건을 사용자와 확인하는 방법을 살펴볼게요. 만들어진 쿼리를 업무 의도에 맞게 이어서 고치는 과정도 함께 다룹니다.

SQL 한 번을 생성하는 것만큼, 그 SQL을 사용자의 업무 의도에 맞게 다듬어 가는 과정에 집중했습니다.

질문이 SQL이 되기까지

“지난달 A상품 신규 가입 고객 수를 연령대별로 보여 줘”라는 요청을 예로 들어볼게요. 이 질문에 답하려면 무엇을 셀지, 어떤 데이터를 사용할지, 기간과 집계 기준을 어떻게 적용할지 정해야 합니다.

K-Metrics는 질문을 요구사항으로 나누고, 테이블·컬럼과 조건을 정해 SQL을 작성한 뒤 검증해요. 문제가 있으면 필요한 단계로 돌아가고, 재시도 한도 안에서 해결되지 않으면 남은 문제를 정리해 반환합니다.

이미지1
SQL 생성과 검증·재시도 흐름

이 과정을 모델에게 한 번에 맡기지 않고 여러 단계로 나눈 데에는 이유가 있습니다.

왜 자율 에이전트보다 단계별 워크플로우가 필요했을까?

검색이나 스키마 조회를 도구로 제공하고, 모델이 다음 행동을 선택하도록 만들 수도 있습니다. 판단하고 도구를 사용한 뒤 그 결과로 다음 행동을 정하는 ReAct 방식이라면, SQL 생성 외의 정보 탐색까지 유연하게 연결할 수 있죠. 다만 이 서비스에서는 그 자유도보다 운영 중 처리 과정을 추적하고 통제할 수 있는 구조가 더 중요하다고 봤어요.

그 출발점은 모델에게 한 번에 맡기는 판단의 범위를 정하는 일이었습니다. 온프렘 LLM의 제한된 컨텍스트를 효율적으로 쓰기 위해 작업을 작은 목표로 나눴어요. 테이블을 고를 때는 요구사항과 후보 설명을, SQL을 작성할 때는 선택한 스키마와 확정된 계획을 전달합니다. 각 단계가 해야 할 일과 필요한 정보를 함께 좁힌 것이죠.

작업을 나누고 나니, 단계 사이에서 반드시 지켜야 할 순서도 분명해졌습니다. 특히 생성한 SQL의 검증을 모델에게 도구로 주고 ‘필요하면 호출해’라고 맡기고 싶지는 않았어요. SQL 작성 뒤에는 구조 검증으로 이어지고, 결과에 따라 의도 검증이나 수정으로 넘어가도록 경로를 정했습니다. 모델이 복구 방향을 제안하더라도, 이동할 수 있는 단계와 재시도·종료 조건은 코드가 관리하도록 했어요.

이미지2
자율 에이전트와 단계별 워크플로우의 차이

이렇게 단계와 경로를 명시하면, 결과가 잘못됐을 때 되짚을 지점도 생깁니다. 최종 SQL만 보는 대신 요구사항 해석·테이블 선택·컬럼 매핑의 중간 결과를 따라가며 어느 판단에서 문제가 시작됐는지 확인할 수 있어요. 단계별 입출력을 기준으로 프롬프트 수정과 테스트의 출발점도 좁혀 볼 수 있습니다.

LangGraph는 이 설계를 실행 흐름으로 옮기는 데 사용했습니다. 단계별 결과는 상태로 전달하고, 검증 결과에 따라 앞 단계로 돌아가는 분기와 반복은 조건부 연결로 표현했어요. LangGraph로도 자율 에이전트를 만들 수 있고, ReAct에도 검증과 재시도 제한을 붙일 수 있습니다. 결국 선택의 핵심은 프레임워크의 우열이 아니라, 판단은 모델에 맡기되 반드시 거쳐야 할 검증과 재시도·종료 조건은 코드로 관리하겠다는 것이었어요.

사내에서 쓰는 용어를 실제 조건으로 만들기

생성 흐름을 나눴다고 해서 모델이 우리 행의 업무까지 이해하지는 못 합니다. 사용자가 평소 쓰는 줄임말도 알아들어야 하고, 짧은 요청에 생략된 업무 기준도 SQL에 반영할 수 있어야 해요.

그래서 도메인 지식은 크게 두 가지를 중심으로 정리했습니다. 하나는 현업에서 쓰는 용어와 별칭이고, 다른 하나는 그 요청을 실제 데이터로 옮길 때 따라야 하는 우리 행의 업무 기준입니다.

이미지3
도메인 지식을 SQL 조건으로 연결하는 흐름

첫 번째는 같은 뜻의 표현을 연결하는 일입니다. 예를 들면, ‘마동 고객만 보여줘’의 ‘마동’은 ‘마케팅 동의’로 해석합니다. 이런 줄임말과 별칭을 용어 정의에 함께 등록하고, 질문에 맞는 정의를 골라 전달해요. 사용자가 정식 명칭을 모두 풀어 쓰지 않아도 같은 업무 개념으로 이해하도록 한 것이죠.

두 번째는 그 뜻을 우리 행의 조회 기준으로 구체화하는 일입니다. ‘마케팅 동의’라는 말을 이해하는 것만으로는 어떤 동의 조건을 확인해야 하는지를 알 수 없으니까요.

일반적인 마케팅 동의 고객 조회에는 카탈로그에 정한 기본 동의 조건을 적용하고, ‘푸시 발송 대상 고객’을 요청하면 광고성 정보 수신 동의와 푸시 발송 동의까지 함께 확인하도록 합니다. 같은 동의 관련 요청이라도 어떤 대상을 찾는지에 따라 필요한 조건이 달라지는 것이죠.

이미지4
같은 동의 관련 요청도 대상에 따라 달라지는 조건

이 기준은 용어 설명에만 두지 않았습니다. 대상 조건 규칙에는 요청별로 적용할 동의 조합을 담고, 컬럼 설명에는 어떤 채널에서 어떤 동의와 함께 써야 하는지 지침을 붙였어요. 모델이 요청 맥락에 맞는 규칙을 선택하면, 코드는 카탈로그에 정의된 컬럼과 값을 실제 스키마와 대조한 뒤 생성 계획에 반영합니다.

SQL을 쓰기 전, 계획 먼저 세워야 해요

이렇게 해석한 업무 의미는 SQL을 작성할 구체적인 계획으로 이어져야 합니다. 평소 데이터 분석으로 쿼리를 자주 다루는 팀원분이 “사람이 SQL을 짤 때 거치는 판단 순서를 적용해 보자”는 아이디어를 주셨어요. 어떤 테이블과 컬럼을 쓰고, 무엇을 계산하고 집계하며, 어떤 조건으로 거르고 묶을지 먼저 정리하는 방식입니다.

이미지5
쿼리를 작성할 때 확인하는 여섯 가지 판단

앞의 요청은 ‘고객 수’라는 지표, ‘A상품’과 ‘신규 가입’이라는 조건, ‘연령대’라는 집계 기준, ‘지난달’이라는 시간 표현으로 나눕니다. 각 요구사항에 필요한 테이블은 등록·과거 쿼리와 테이블 설명에서 후보를 모아 선택해요. 카탈로그에 등록된 스키마를 확인한 뒤 요구사항을 컬럼에 연결하고, 조인 관계와 조건·집계 기준을 계획에 담습니다.

이때 중요한 것은 ‘연령대별로 나눈다’는 요구와 이를 구현할 컬럼·집계 기준을 연결해 두는 것입니다. 아직 해결하지 못한 요구사항도 함께 남겨요. 긴 판단 설명을 받기보다, 다음 단계가 사용할 결정과 미확정 항목을 구조화해 전달하는 데 초점을 맞췄습니다.

이미지6
요청을 요구사항으로 나누고 생성 계획에 담는 과정

필요한 날짜까지 확정되면 이 계획과 스키마, 도메인 지식, 참고 쿼리를 SQL 작성 단계에 전달합니다. 모델은 앞에서 정한 내용을 바탕으로 연령 구간 계산식과 집계 등을 실제 SQL로 옮겨요.

계획은 작성 후에도 쓰입니다. 상품과 기간은 반영됐는데 연령대별 집계가 빠졌다면, 요청 문장만 다시 읽는 대신 계획의 집계 기준과 SQL을 대조할 수 있죠. 이렇게 생성에 사용한 계획을 검증의 기준으로도 이어 씁니다.

검증 단계에서 발견한 문제, 어디서 고칠까?

계획을 SQL로 옮겼다고 요청이 모두 반영됐다고 볼 수는 없습니다. 생성 경로에서는 작성된 SQL을 두 단계로 검사해요. 구조 검증은 SQL 파서(sqlglot)로 문법과 컬럼의 소속 등을 확인하고, 의도 검증은 LLM이 요청·계획·SQL을 대조합니다. 의도 검증 단계에서는 필수 조건의 누락, 계획에 없는 코드 조건, 시간 조건의 위반도 코드로 별도 검사해요.

앞서 소개한 망분리 제약은 생성에도 동일하게 적용됩니다. 여기서 검증은 실제 DB에서 SQL을 실행하는 것이 아니라, SQL 구문과 등록된 스키마, 요청·계획 사이의 일관성을 확인하는 과정이에요. 따라서 검증을 통과했다고 실제 조회 결과의 정확성까지 확인된 것은 아닙니다.

이미지7
구조 검증과 의도 검증의 두 단계

여기서 중요한 것은 오류 원인에 따라 돌아갈 단계를 다르게 정하는 것입니다.

이미지8
검증 오류 원인에 따른 복구 경로

연령 정보가 필요한데 선택한 테이블 어디에도 없다면, SQL만 반복해서 수정해도 해결되지 않습니다. 반대로 필요한 컬럼이 있는데 소속을 잘못 연결했다면 매핑부터 재검토할 수 있죠. 같은 “검증 실패”라도 부족한 것이 데이터 근거인지, 매핑인지, SQL 표현인지에 따라 복구 경로가 달라집니다.

재탐색·매핑·수정에는 각각 횟수 제한을 두고, 수정해도 SQL이 바뀌지 않는 경우에는 같은 검증·수정을 계속 반복하지 않도록 종료 경로를 둡니다. 최종 단계에서는 남은 검증 결과를 바탕으로 SQL과 확인할 사항 또는 실패 사유를 정리해요.

또한 LLM에게 보여주는 스키마를 필요한 부분으로 줄이더라도, 구조 검증에서는 카탈로그에 등록된 해당 테이블의 전체 스키마를 사용합니다. 생성 단계에서 보여 주지 않은 컬럼을 실제로 존재하지 않는 컬럼으로 오판하지 않도록 한 것이죠.

필요한 정보는 사용자가 고르기 쉽게 묻는다

여기까지는 생성과 검증의 내부 흐름을 살펴봤습니다. 실제 사용 과정에서는 SQL을 완성하기 전에 사용자에게 확인해야 할 정보도 생겨요. 이를 매번 긴 문장으로 되묻고 답을 해석하는 방식은 번거롭습니다.

날짜가 한 예입니다. 모델은 ‘지난달’을 ‘월 단위, 한 달 이전’처럼 해석하고, 실제 날짜 계산은 코드가 기준일을 바탕으로 수행해요. 그래도 필요한 날짜를 확정할 수 없다면 사용자에게 입력을 요청합니다.

이미지9
조회 조건을 확인하고 수정하는 입력 카드 예시

K-Metrics는 조회 조건을 입력 카드로 보여 줍니다. 날짜나 선택 항목처럼 형식이 정해진 값은 사용자가 확인하고 수정할 수 있도록 하고, 이미 확정된 값은 카드에 남겨 둡니다. 제출값의 형식과 필수 입력 여부는 코드로 검사해요.

사용자가 선택한 테이블과 날짜는 각각 확정된 테이블 목록과 파라미터로 전달합니다. 선택 결과를 질문 문장에 덧붙여 다시 검색하면, 사용자가 골랐는데도 다른 후보로 바뀔 수 있기 때문이에요.

“여기서 조건 하나만 바꿔 주세요”

필요한 조건을 확인해 쿼리를 받았더라도 작업이 끝나는 것은 아닙니다. 업무에서는 결과를 살펴보거나 목적을 구체화하면서 조건을 조금씩 바꾸게 돼요.

“이 조건은 빼 주세요.”
“상위 20개만 보여 주세요.”
“이번에는 다른 상품도 포함해 주세요.”

이때 매번 처음부터 새 SQL을 만들면 사용자가 이미 확정한 조건까지 바뀔 수 있어요. 편집의 기준은 현재 SQL과 확정된 조건이어야 합니다.

그래서 편집 요청에서는 바꾸려는 부분을 따로 해석하고, 기존 테이블·컬럼·파라미터를 이어받습니다. 전체 생성 경로를 거치더라도 기존 SQL을 기준으로 충돌하지 않는 조건과 집계를 보존하며 수정하도록 해요.

편집 중 등장한 새로운 용어도 다시 조회합니다. 처음 질문에 없었던 상품의 줄임말이 후속 요청에 나왔다면, 이전에 전달한 용어 정의만으로는 해석할 수 없겠죠. 기존 지식을 이어받으면서 이번 지시에 필요한 지식을 보충하는 방식입니다.

이 흐름은 서비스에서 생성한 SQL뿐 아니라 사용자가 붙여 넣은 SQL의 편집에도 사용합니다. 붙여 넣은 SQL은 참조 테이블이 카탈로그에 등록되어 있는지 먼저 확인한 뒤 편집으로 연결해요.

정렬이나 조회 건수를 바꾸는 작은 수정은 별도의 편집 경로에서 처리합니다. 현재 SQL과 스키마 안에서 끝낼 수 있다면 테이블 검색부터 다시 할 필요는 없으니까요.

이 경로에서는 LLM이 한 번 수정안을 만들고, 코드가 결과를 검사합니다. 간단한 편집으로 끝낼 수 없다고 판단하거나 검사에 실패하면 전체 생성 경로로 넘깁니다.

이미지10
현재 SQL을 기준으로 한 쿼리 편집 흐름

간단한 편집에도 생성과 같은 구조 검증기를 사용합니다. 다만 전체 생성 경로의 의도 검증을 그대로 수행하는 것은 아니며, 편집에 필요한 별도 검사로 결과를 확인해요.

검사는 문법에만 머물지 않습니다. 테이블 구성이 달라졌는지, 카탈로그가 지정한 시간 컬럼의 조건 형태가 바뀌었는지 등을 코드로 확인해요. 예를 들어 기준 날짜의 값만 바꾸는 것과 한 시점의 조건을 기간 조건으로 바꾸는 것은 다른 판단이 필요합니다.

모델이 요청 중 반영하지 못한 항목을 보고한 경우에도 간단한 편집 결과를 채택하지 않고 전체 생성 경로로 넘깁니다.

“수정했습니다”라는 설명만 믿지도 않습니다. 편집 전후 SQL을 파싱한 구조로 비교해 실제 변경이 있는지 확인합니다. 들여쓰기만 달라진 SQL을 수정 결과로 받아들이지 않기 위해서예요.

이미지11
간단한 편집 결과를 채택하기 전에 확인하는 것

편집 도중 “이 조건은 왜 들어갔나요?”라고 물어도 현재 SQL은 유지합니다. 설명을 들은 뒤 수정을 이어갈 수 있도록 한 것이죠. 새 요청으로 판단한 경우에는 기존 작업을 초기화하기 전에 전환 여부를 확인합니다.

다음은 한 질문에서 이어지는 분석으로

현재 쿼리를 이어서 고치는 경험을 넘어, 다시 방문했을 때의 시작점도 개선하려고 합니다. 이용 이력을 바탕으로 자주 쓰는 쿼리를 제안해 익숙한 작업에서 바로 시작할 수 있도록, 개인화를 준비하고 있어요.

또한 하나의 세션에서 이전 요청과 SQL의 맥락을 유지하며 후속 질문에 필요한 쿼리를 이어서 만드는 기능도 추진할 예정입니다. “이 고객군을 다른 기준으로 나눠 보면?”, “이전 기간과 비교하면?”처럼 비교·세분화에 필요한 SQL을 만드는 과정으로 확장하려고 해요.

마치며 — 구현하면서 배운 것들

AI 엔지니어로 업무를 수행하면서, 이 서비스를 사용할 임직원들이 어떤 고민 속에서 쿼리를 작성하고 분석을 이어가는지 이해하는 과정이 필요했어요. 어떤 기준으로 데이터를 찾고, 어떤 조건을 반복해서 바꾸며, 어디에서 어려움을 느끼는지는 기술적인 설계만으로 알기 어려운 부분이었습니다.

쿼리를 잘 생성하도록 설계하는 것은 중요합니다. 하지만 실제 업무 효율에 도움이 되려면, 사용자가 어떤 문제를 풀고 있는지 이해하고 그들이 가진 도메인 지식을 서비스에 녹여 내는 일이 더 중요하다는 걸 느꼈어요. ‘푸시 발송 대상’이라는 짧은 표현에도 현업에서 적용하는 여러 동의 조건이 담겨 있었습니다. 사용자에게는 익숙해서 질문에 생략하는 기준까지 이해하고 반영해야 업무 의도에 맞는 SQL을 만들 수 있었어요.

이번 작업을 통해 좋은 서비스를 만드는 출발점은 결국 사용자를 이해하는 데 있다는 생각이 더 분명해졌습니다. 어떤 SQL을 만들어 낼 수 있는지만큼, 그 SQL이 누구의 어떤 수고를 덜어 줄 수 있는지를 함께 보게 됐어요.

앞으로도 실제 사용자의 이야기를 듣고, 업무에서 쌓인 지식과 경험을 서비스 개선에 반영하려고 해요. 임직원들이 데이터를 찾고 분석하는 과정에서 겪는 수고를 덜 수 있도록, 현업과 함께 다듬어 가겠습니다.