SQL 실행기가 둘이어도 판정과 결과 형식은 한 벌만 둔다

OwlAI가 고객 DB에서 SQL을 실행하는 길은 둘입니다. 직접 연결(관리자가 접속 정보를 손으로 입력한 연결)에서는 OwlAI의 내장 실행기가 DB에 붙고, OwlDB 연결에서는 OwlDB가 실행합니다. 실행기는 SQL을 DB에 보내고 결과를 받아 오는 구성 요소입니다. 둘이 결과를 조금씩 다르게 돌려주면 모델은 그 차이를 데이터의 차이로 읽습니다. 2,000행이 있는 테이블에 LIMIT 200을 붙인 SQL을 보냈는데 한쪽만 「더 있음」을 알리면, 다른 쪽으로 연결한 DB에서 모델은 「총 200건」이라고 답합니다. 한쪽이 시각의 시간대 오프셋을 떼면 서울 오전 10시 주문이 「01:00」으로 답에 실립니다.
그래서 실행기가 몇 개든 한 벌만 두는 것이 셋 있습니다. 권한과 읽기 전용 판정은 실행기를 고르기 전에 한 번 돌고, 결과가 잘렸는지와 셀 값의 모양과 거절 사유는 실행기 밖의 한 정의를 따릅니다. 두 길이 일부러 다르게 동작하는 부분은 연결 출처의 정의 한 곳에 모았습니다. 이 글은 이 세 가지가 무엇을 정하는지, 그리고 그것을 실행기마다 두지 않은 이유를 설명합니다.
판정은 실행기를 고르기 전에 돈다
모델이 SQL 실행 도구를 부르면 다음 판정이 차례로 돕니다. 어느 실행기로 갈지는 그다음에 정해집니다.
- 사용자가 멘션으로 DB를 지정해 턴(질문 하나에 답 하나가 끝나기까지)을 그 DB로 묶었으면, 그 밖의 DB를 부르는 호출을 거절한다.
- 사용자의 역할이 그 DB를 쓸 수 있는지 본다. 판정 단위는 DB이고, 그 안에서 어느 테이블을 읽을 수 있는지는 DB 계정의 권한이 정한다(「테이블 권한은 SQL 검사 대신 역할마다 둔 DB 계정이 지킨다」).
- 문장이 하나이고
SELECT나WITH로 시작하는지 본다. - 이 호출이 받을 행 상한을 정한다. 모델이 값을 주지 않으면 500행이고, 설치 설정
dbConnection.maxRowLimit을 넘는 값은 그 값으로 낮춘다.
여기까지 통과한 호출만 실행기로 갑니다. 그 엔진의 문법으로 읽기 문장인지 다시 가리는 판정은 엔진 이름만 있으면 돌아가므로, 내장 실행기는 DB에 보내기 직전에, OwlDB 연결은 OwlDB에 요청을 보내기 전에 같은 판정을 거칩니다. 내장 실행기는 그 위에 읽기 전용 세션과 시간 예산을 겁니다(「읽기 전용과 쿼리 취소는 DB 서버에 닿아야 한다」). OwlDB 연결의 요청에는 DB 식별자, 역할에 대응하는 계정 식별자, SQL, 행 상한이 실립니다(「OwlDB 연결은 접속 정보 대신 DB 식별자만 든다」).
실행기마다 자기 판정을 두는 길도 있습니다. 실행기를 고치는 사람이 자기 경로만 보고 규칙을 바꿀 수 있습니다. 대신 멘션 범위나 역할 권한의 규칙이 바뀔 때마다 두 곳을 함께 고쳐야 하고, 한 곳을 빠뜨린 구멍은 그 출처로 연결한 DB에서만 드러납니다. 내장 실행기 안에서도 같은 판단을 했습니다. 내장 실행기는 PostgreSQL, MariaDB, MySQL, Oracle, Tibero 다섯 엔진을 돌리는데, 엔진마다 채우는 것은 연결, 문장 실행, 취소, 오류 분류뿐입니다. 문장 판정, 행 상한 붙이기, 셀 값 변환은 다섯 엔진이 함께 쓰는 한 곳에 있습니다.
결과가 잘렸는지는 도구가 한 번 판정한다
모델이 결과를 읽고 가장 크게 틀리는 경우는 잘린 결과를 전부로 읽을 때입니다. 그래서 「결과가 잘렸다」는 표시는 실행기가 아니라 SQL 실행 도구가 단독으로 정합니다. 근거는 둘이고, 하나라도 서면 잘린 결과입니다.
- 실행기가 행 상한보다 한 행을 더 읽어 보고, 그 한 행이 왔다. 남은 행이 있다는 증거이고, 그 행은 모델에게 넘기기 전에 버린다.
- SQL 자신의 상한절(
LIMIT,FETCH FIRST)에 닿았다. 이때는 DB가 그 상한에서 멈추므로 한 행 더 읽기로는 알 수 없다.
앞 문단의 LIMIT 200이 둘째 경우입니다. 실행기는 200행을 받고 더 읽을 행이 없으니 잘렸다고 말하지 못합니다. 도구가 SQL을 그 엔진의 문법으로 읽어 상한절이 200이고 받은 행도 200이라는 것을 보고 잘림을 세웁니다. 예외는 GROUP BY 없이 집계만 하는 SELECT입니다. SELECT COUNT(*) … FETCH FIRST 1 ROW ONLY는 상한 1에 닿아도 뒤에 행이 있을 수 없으므로 잘림으로 보지 않습니다. 결과의 누적 크기가 dbConnection.resultMaxBytes를 넘어 실행기가 중간에 받기를 멈춘 경우도 같은 표시로 알립니다.
모델에게 가는 것은 잘렸는지 하나입니다. 어느 상한에 걸렸는지, 상한이 몇이었는지는 싣지 않습니다. 행 옆에 숫자가 하나 더 있으면 모델은 그것을 총 건수로 읽기 쉽습니다. 잘린 결과를 받은 모델은 총 건수가 필요하면 COUNT(*)를 따로 실행합니다. 어느 상한이 걸렸는지는 운영자용 로그에 남습니다.
OwlDB 연결에서도 이 판정은 OwlAI에 있습니다. OwlDB는 요청에 실린 행 상한까지 결과를 돌려주며 남은 행이 있었는지를 함께 알리고, OwlAI는 그 신호와 SQL의 상한절을 같은 방식으로 합칩니다. 판정을 실행기에 두면 OwlDB가 상한절까지 읽어 내야 하고, 두 제품이 SQL을 읽는 방식이 갈리는 순간 같은 SQL의 잘림 표시가 출처마다 달라집니다.
셀 값은 실행기 밖의 한 규칙으로 바뀐다
드라이버가 주는 값은 엔진마다 타입이 다릅니다. 모델이 받는 값은 아래 규칙 하나로 바뀝니다.
| 값 | 모델이 받는 모양 |
|---|---|
| 날짜·시각 | 문자열. 시간대가 붙은 값은 UTC로 옮기고 오프셋(+00:00)을 남긴다 |
| 정수인 10진수 | 자릿수를 잃지 않는 정수 |
| 소수 | 부동소수 |
| 텍스트 LOB | 앞 4,000자와 전체 글자 수를 적은 잘림 표시 |
| 이진 LOB | 크기만 |
| 이진 값 | 64바이트까지는 16진수, 넘으면 크기만 |
| 규칙이 없는 타입 | 문자열. 타입마다 경고 로그를 한 번 남긴다 |
규칙마다 틀리는 방향이 분명합니다. 시간대가 붙은 값에서 오프셋을 떼면 모델과 결과 표는 UTC 시각을 사용자의 벽시계로 읽습니다. 오프셋이 남아 있으면 모델이 사용자의 시간대로 옮겨 말합니다. 정밀도 없는 Tibero NUMBER를 부동소수로 받으면 2^53을 넘는 값과 38자리 값의 끝자리가 바뀝니다. 주문 번호나 계좌 번호가 이런 값이면 모델은 존재하지 않는 번호로 다음 조회를 합니다. CLOB을 한쪽은 본문으로, 다른 쪽은 드라이버의 객체 표기로 주면 모델은 같은 컬럼을 두고 「내용이 있다」와 「비어 있다」를 오갑니다.
규칙이 없는 타입에 경고를 남기는 것은 조용한 변환을 찾기 위해서입니다. 문자열로 바꾼 값이 객체 주소였다면 화면에는 이상한 글자가 찍히고, 로그가 없으면 아무도 알아채지 못합니다. OwlDB 연결의 결과도 같은 규칙의 값으로 오므로, 모델은 어느 출처의 DB인지 모른 채 같은 모양을 읽습니다.
거절 사유와 실행 기록도 출처를 가리지 않는다
거절은 모델이 SQL을 고쳐 풀 수 있는지로 나뉩니다.
- DB가 문장을 거절하면(문법 오류, 없는 컬럼) DB의 문구를 그대로 돌려준다. 고칠 것이 SQL이기 때문이다.
- DB가 권한이 없다고 하면 테이블 접근 거절 사유로 돌려준다. 직접 연결의 계정이든 OwlDB의 역할 계정(「테이블 권한은 SQL 검사 대신 역할마다 둔 DB 계정이 지킨다」)이든 같은 사유다.
- 접속하지 못하면 어느 DB가 어떤 분류(접속 거절, 인증 실패 등)로 실패했는지만 싣는다. 드라이버 원문에는 내부망의 주소와 사정이 들어 있어 싣지 않는다. OwlDB 연결에서 노드 사정 같은 인프라 사유로 돌아온 요청은 모델에게 보이지 않고 OwlAI가 다시 보낸다(「AI 조회를 실행할 노드는 등록할 때가 아니라 실행할 때 고른다」).
- 시간 예산을 넘긴 실행과 사용자가 멈춘 실행은 각자 다른 사유로 돌아온다.
실행이 끝나면 성공이든 거절이든 SQL 실행 이력에 한 줄이 남습니다. 누가 어느 DB에 어떤 SQL을 보냈는지, SQL이 가리킨 테이블, 성공했다면 받은 행 수, 결과(성공·거절·실패)입니다. 조회 결과 값은 이 기록에 남지 않습니다. 기록이 또 하나의 유출 경로가 되지 않게 하려는 것입니다. 관리자는 연결 출처와 상관없이 같은 화면에서 같은 칸을 봅니다.
일부러 다르게 둔 것은 연결 출처의 정의에 있다
연결 출처는 연결을 어디서 만들었는지 적은 값입니다. 직접 입력, Supabase, Amazon RDS, OwlDB 넷이 있고, 연결을 등록할 때 정해진 뒤 바뀌지 않습니다. 출처마다 다른 규칙은 표 하나에 있습니다.
| 규칙 | 직접 입력 | Supabase | Amazon RDS | OwlDB |
|---|---|---|---|---|
| 역할에 테이블 단위 권한을 줄 수 있다 | 아니오 | 아니오 | 아니오 | 예 |
| SSH 터널을 거칠 수 있다 | 예 | 아니오 | 예 | 아니오 |
| 엔진·호스트·포트·TLS를 수정에서 바꿀 수 있다 | 예 | 아니오 | 아니오 | 아니오 |
| 실행하는 쪽 | 내장 실행기 | 내장 실행기 | 내장 실행기 | OwlDB |
등록 화면과 API, 실행기를 여는 자리, 역할 권한이 모두 이 표를 읽습니다. 그래서 같은 규칙이 화면과 API에서 다르게 걸리지 않습니다. 직접 입력 연결에 테이블을 골라 권한을 주려고 하면 API가 RBAC_DB_TABLE_SCOPE_UNSUPPORTED로 거절하고, Supabase 연결의 호스트를 바꾸려고 하면 DB_CONNECTION_SOURCE_LOCKED로 거절합니다. 새 출처를 더할 때는 이 표에 한 줄을 채우면 되고, 규칙을 하나 더할 때는 칸을 하나 더하고 네 출처의 값을 모두 정합니다.
테이블 단위 권한이 OwlDB 연결에만 있는 이유는 그 경계를 지킬 수단에 있습니다. OwlDB 연결에서는 역할마다 둔 DB 계정의 SELECT 권한이 테이블을 막습니다. 직접 연결에서 같은 화면을 보여 주려면 SQL에서 테이블 이름을 뽑아 대조하는 검사로 막아야 하는데, 그 검사는 엔진이 이름을 푸는 방식과 어긋납니다(「테이블 권한은 SQL 검사 대신 역할마다 둔 DB 계정이 지킨다」). 화면이 같으면 관리자는 테이블이 막혔다고 믿게 됩니다. 실행 전 무거운 SQL 판정(「무거운 SQL은 cost 문턱 대신 DB 상태를 아는 OwlDB가 판정한다」)과 실행 노드 선택(「AI 조회를 실행할 노드는 등록할 때가 아니라 실행할 때 고른다」)도 같은 이유로 OwlDB 연결에만 있습니다. 둘 다 연결 하나로는 알 수 없는 DB의 상태가 있어야 판정할 수 있습니다.
설계에서 고른 것
판정을 실행기마다 두는 대신 실행기 앞에 하나로 두었습니다. 실행기마다 두면 고친 규칙이 한쪽에서 빠지고, 그 구멍은 그 출처의 DB에서만 드러납니다. 대가로 한 실행기에만 필요한 규칙도 공통 판정의 자리에서 출처를 보고 갈라야 합니다.
잘림 판정을 실행기 대신 SQL 실행 도구에 두었습니다. SQL의 상한절은 실행기가 알 수 없는 근거이고, 이것을 실행기마다 읽게 하면 같은 SQL의 잘림 표시가 출처마다 달라집니다. 대가로 도구가 엔진마다 SQL의 상한절 문법을 알아야 합니다.
셀 값을 한 규칙으로 바꿉니다. 모델이 보는 것은 실행기가 아니라 결과이기 때문입니다. 대가로 엔진이 가진 정보 일부가 모델에게 가지 않습니다. 시간대가 붙은 값은 원래의 오프셋 대신 UTC로 옮긴 값으로 가고, 4,000자를 넘는 LOB은 앞부분만 갑니다.
출처마다 다른 것은 출처 정의 한 곳에 모았습니다. 직접 연결에 테이블 단위 권한이나 실행 전 비용 판정을 흉내 내 넣으면 화면은 같아도 지켜지는 정도가 다릅니다. 직접 연결에는 지킬 수 있는 것만 보입니다.
DB가 몇 개뿐이고 역할마다 열어 줄 범위가 DB 단위로 나뉜다면 직접 연결로 충분합니다. 같은 DB 안에서 테이블마다 권한을 나눠야 하거나 AI 조회가 운영 업무와 같은 DB를 쓴다면, 그 DB는 OwlDB 연결로 붙입니다. 두 방식을 한 설치 안에서 섞어 써도 모델이 받는 결과의 형식은 같습니다.
어떤 문제를 해결하나요?
DB 연결과 OwlDB의 다른 글

엔지니어링 노트 · DB 연결과 OwlDB
배스천에는 계정 대신 DB 포워딩만 여는 키 한 줄을 넣는다
운영 DB가 사설망 안에 있고 그 앞에 배스천 호스트(사설망으로 들어가는 SSH 서버)가 있으면, OwlAI는 배스천을 거치는 SSH 터널로 DB에 붙습니다.

엔지니어링 노트 · DB 연결과 OwlDB
인증서 오류는 검증을 끄지 않고, 빠진 CA와 틀린 이름을 고쳐서 푼다
기업망에 설치한 OwlAI가 클라우드 LLM이나 고객 DB에 TLS로 붙을 때, 인증서 오류가 나는 경우는 크게 둘입니다.

엔지니어링 노트 · DB 연결과 OwlDB
RDS 목록은 액세스 키 대신 60분 뒤 만료되는 역할로 읽는다
OwlAI SaaS에서 고객의 Amazon RDS·Aurora를 연결하려면 엔드포인트·포트·엔진과 함께 DB가 공개 서브넷에 있는지, 어느 VPC·보안 그룹에 속하는지 같은 망 정보가 필요합니다.
