엔지니어링 노트DB 연결과 OwlDB

테이블 권한은 SQL 검사 대신 역할마다 둔 DB 계정이 지킨다

관리자가 영업팀 역할에 고객·주문 테이블만 열고, 같은 DB에 있는 연봉 테이블은 닫았다고 합시다. AI가 쓴 SQL에서 테이블 이름을 뽑아 허용 목록과 대조하는 검사는 SQL 글자만 봅니다. 엔진은 그 글자가 실제로 가리키는 객체를 읽습니다. 둘이 어긋나는 SQL은 검사를 지나 연봉 행을 돌려주고, 오류가 나지 않으니 그 행은 AI의 답에 그대로 섞입니다.

그래서 테이블 경계는 DB의 권한 검사에 맡깁니다. OwlDB 연결에서는 역할마다 DB 계정을 하나씩 두고, 관리자가 고른 테이블에만 그 계정의 SELECT 권한을 줍니다. 그런 계정을 둘 수 없는 직접 연결은 DB 단위로만 엽니다. 이 글은 OwlAI와 DB가 각각 무엇을 확인하는지, 그리고 테이블 선택을 OwlDB 연결에만 둔 이유를 설명합니다.

SQL에서 테이블 이름을 뽑는 검사는 엔진의 이름 해석과 어긋난다

SQL을 파싱해 테이블 이름을 뽑는 방법에는 분명한 장점이 있습니다. OwlAI 안에서 끝나므로 DB에 계정을 따로 만들 필요가 없고, 어떤 방식으로 연결한 DB에도 같은 화면을 줄 수 있습니다. 문제는 검사기가 읽는 이름과 엔진이 푸는 이름이 같다는 전제입니다. 이 전제가 깨지는 SQL은 흔합니다.

  • PostgreSQL의 query_to_xml('SELECT * FROM hr.salary', true, false, '')은 문자열로 받은 SQL을 실행합니다. 파서가 보는 것은 함수 호출과 문자열 하나이고, 테이블 이름은 없습니다.
  • 스키마를 생략한 salary가 어느 스키마의 테이블인지는 PostgreSQL에서는 접속 계정의 search_path, Oracle과 Tibero에서는 접속 계정의 기본 스키마가 정합니다. 검사기는 SQL만 보고 이 값을 알 수 없습니다.
  • Oracle과 Tibero의 동의어(synonym)는 다른 스키마의 객체를 다른 이름으로 가리킵니다. 허용 목록에 있는 이름과 같은 이름의 동의어가 다른 테이블을 가리키면, 검사기는 통과시키고 엔진은 다른 테이블을 읽습니다.
  • 따옴표로 감싼 이름의 대소문자 규칙, 테이블 함수, 엔진 고유 구문은 엔진마다 다릅니다. 지원 엔진이 하나 늘 때마다 파서도 그 문법을 따라가야 합니다.

엔진은 이 이름들을 모두 스스로 풀고, 권한 검사는 풀린 객체에 대해 합니다. 문자열 안의 SQL도 같은 계정의 권한으로 실행되므로, 그 계정에 연봉 테이블 권한이 없으면 query_to_xml도 권한 오류로 끝납니다. 엔진의 이름 해석을 OwlAI가 따라 만들 이유가 없습니다.

OwlAI는 모델이 볼 테이블을 고르고, DB는 돌려줄 행을 정한다

관리자는 역할마다 DB 연결을 허용하고 범위를 고릅니다. 범위는 DB 전체와 테이블 선택 둘 중 하나이고, 테이블 선택은 스키마 분석을 마친 테이블 가운데서 고릅니다. OwlAI는 이 범위로 모델이 보는 것을 거릅니다.

  • 테이블 검색과 테이블 카드 조회는 범위 안 테이블만 돌려줍니다. 범위 밖 테이블을 이름으로 집으면 권한 거부로 답합니다.
  • 테이블 메모도 범위 안 테이블의 것만 읽고 씁니다.
  • 대화 첫머리에 싣는 DB 업무 요약은 테이블 전부가 열린 DB에만 싣습니다. 요약이 범위 밖 테이블을 언급할 수 있기 때문입니다.

AI가 SQL을 실행하기 전에 OwlAI가 확인하는 것은 질문한 사람의 역할이 그 DB를 쓸 수 있는가입니다. 허용되지 않은 DB의 SQL은 이 확인에서 실행 전에 거절됩니다. 그 DB 안에서 어느 테이블을 읽을 수 있는지는 SQL을 실행하는 DB 계정의 권한이 정합니다.

두 확인은 맡는 일이 다릅니다. OwlAI의 범위는 모델이 처음부터 열린 테이블만 보고 SQL을 쓰게 합니다. 그래서 대부분의 SQL은 범위 안에서 끝납니다. 모델이 범위 밖 테이블 이름을 짐작해 쓰거나 위의 예처럼 이름이 다른 객체로 풀려도, 그 SQL은 DB의 권한 검사에서 멈춥니다.

영업팀 SQL이 거치는 확인: OwlAI의 DB 판정과 역할 계정의 테이블 권한
그림 1. 영업팀 SQL이 거치는 확인: OwlAI의 DB 판정과 역할 계정의 테이블 권한

OwlDB 연결에서는 역할마다 DB 계정 하나에 SELECT만 준다

OwlDB는 DB 접속과 실행을 맡는 DB 관리 솔루션이고, OwlDB 연결은 OwlDB가 관리하는 DB 목록에서 골라 만든 연결입니다(「OwlDB 연결은 접속 정보 대신 DB 식별자만 든다」). OwlDB 연결에서는 OwlAI 역할 하나에 DB 계정 하나가 대응합니다. 관리자가 역할에 테이블을 고르면 OwlDB가 그 계정에 GRANT SELECT를 내리고, 고른 테이블을 빼면 REVOKE를 내립니다. 역할을 지우면 계정도 거둡니다.

이 계정이 받는 권한은 고른 테이블의 SELECT뿐입니다. PostgreSQL 14 이하는 모든 계정에 public 스키마의 CREATE 권한이 기본으로 열려 있어서, 계정을 만들 때 이 기본 권한도 거둡니다. 엔진마다 다른 읽기 전용 설정의 한계는 「읽기 전용과 쿼리 취소는 DB 서버에 닿아야 한다」에서 다룹니다.

AI가 SQL을 실행하면 OwlAI는 질문한 사람의 역할에 대응하는 계정 식별자를 OwlDB에 넘깁니다. OwlAI는 OwlDB 연결의 접속 정보를 받지 않으므로 그 계정의 비밀번호도 모릅니다. 영업팀 사원과 인사팀 사원이 같은 질문을 해도 SQL은 서로 다른 계정으로 실행되고, 연봉 테이블을 읽는 SQL은 영업팀 계정에서 권한 오류로 끝납니다. AI는 그 권한 오류를 도구 결과로 받습니다.

부여와 회수의 결과는 테이블마다 화면에 남는다

테이블을 200개 고르면 GRANT도 200번 나갑니다. 그 사이 테이블이 지워졌거나 DB가 잠시 응답하지 않으면 일부만 실패할 수 있습니다. 화면은 저장됐다고 하는데 DB 권한이 다르면 두 방향으로 어긋납니다. 부여가 일부 실패하면 사용자는 열린 줄 알고 묻는데 SQL이 권한 오류로 끝납니다. 회수가 일부 실패하면 닫았다고 믿은 테이블이 열린 채 남습니다. 뒤쪽이 유출입니다.

그래서 OwlDB는 부여와 회수의 결과를 테이블마다 돌려줍니다. 실패한 테이블은 역할 화면에 고른 범위와 실제 권한이 다르다고 표시되고, 관리자는 그 테이블만 다시 시도합니다. 실제 경계는 DB 권한이므로, 화면이 보여 줄 값도 DB가 돌려준 결과입니다.

DB 감사 로그에는 역할이 남고, 사람은 SQL 실행 이력에 남는다

계정 하나로 모든 질문을 실행하면 DB 감사 로그에는 늘 같은 계정 이름만 남습니다. DB 관리자가 감사 로그를 열어도 그 조회가 어느 부서의 질문이었는지 가릴 수 없습니다. 역할마다 계정을 두면 DB 감사 로그만으로 어느 역할의 조회였는지가 구분됩니다.

사람 단위의 기록은 OwlAI의 SQL 실행 이력이 남깁니다. SQL이 실행될 때마다 시각, 실행한 사람, DB 연결, 결과, 돌려받은 행 수, SQL 원문이 한 줄씩 남습니다(「감사 기록에는 대화 대신 AI가 실행한 SQL과 도구 호출을 남긴다」). DB 감사 로그의 계정과 시각으로 SQL 실행 이력의 줄을 찾으면 그 조회를 한 사람까지 이어집니다.

직접 연결은 DB 단위로만 연다

직접 연결에도 같은 구조를 둘 수 있어 보입니다. OwlAI가 고객 DB에 역할별 계정을 만들고 GRANT를 내리면 됩니다. 그러려면 OwlAI가 고객 DB에서 계정을 만들고 권한을 줄 수 있는 관리자급 계정을 들고 있어야 합니다. 직접 연결의 쓰기 차단은 연결 계정에 조회 권한만 있다는 데 기대고 있어서(「읽기 전용과 쿼리 취소는 DB 서버에 닿아야 한다」), 관리자급 계정을 쥐는 순간 그 전제가 사라집니다. 조회만 하는 AI 제품에 DB 관리자 권한을 내주는 구성은 DB 관리자가 받아들이기 어렵습니다.

그래서 직접 연결은 DB 전체로만 허용하고, 그 안의 경계는 관리자가 만든 연결 계정의 권한이 정합니다. 테이블 단위로 나눠야 하면 DB 관리자가 용도별 조회 계정을 만들고 그 계정마다 연결을 하나씩 등록한 뒤, 역할마다 다른 연결을 허용합니다. Supabase나 Amazon RDS에서 가져온 연결도 직접 연결과 같습니다. 이런 연결에 테이블을 고르는 요청은 API가 RBAC_DB_TABLE_SCOPE_UNSUPPORTED로 요청 전체를 거절하고, 요청의 어느 항목도 저장하지 않습니다. 역할 화면에서도 테이블 선택은 OwlDB 연결에서만 고를 수 있습니다.

설계에서 고른 것

테이블 경계를 SQL 파서 대신 DB 계정의 권한에 맡겼습니다. 파서는 OwlAI 안에서 끝나고 연결 방식과 관계없이 같은 화면을 줄 수 있습니다. 하지만 파서와 엔진의 이름 해석이 어긋나는 자리는 엔진과 문법이 늘 때마다 생기고, 어긋난 SQL은 오류 없이 답에 섞입니다. DB의 권한 검사는 엔진이 이름을 푼 뒤의 객체에 걸리므로 그 어긋남이 생기지 않습니다.

테이블 선택은 DB 계정이 그 범위를 강제할 수 있는 연결에만 열었습니다. 직접 연결에서도 테이블 선택을 보여 주면 관리자는 막혔다고 믿지만, 그 SQL을 막을 장치는 OwlAI 안의 파서뿐입니다. 보이는 권한과 지켜지는 권한을 같게 두는 쪽을 골랐고, 그 대가로 직접 연결에서 테이블을 나누려면 DB 관리자가 계정과 연결을 여럿 만들어야 합니다.

계정은 사람마다가 아니라 역할마다 둡니다. 사람마다 계정을 두면 DB 감사 로그가 더 촘촘해지지만, 직원 수만큼 DB 계정이 생기고 입사·퇴사·부서 이동마다 DDL이 나갑니다. 역할은 권한이 실제로 갈리는 단위이고, 사람 단위의 추적은 SQL 실행 이력이 맡습니다. 그 대가로 같은 역할 안에서 누가 조회했는지는 DB 감사 로그만으로는 알 수 없고 SQL 실행 이력과 맞춰 봐야 합니다.

권한의 뜻도 DB의 규칙을 그대로 따릅니다. 역할에 뷰를 열면 PostgreSQL·Oracle·Tibero 모두 기본적으로 뷰 소유자의 권한으로 기반 테이블을 읽으므로, 그 뷰가 보여 주는 행이 열립니다. DB 관리자가 이미 아는 규칙이라 따로 배울 것이 없지만, 연봉을 조인한 뷰를 영업팀에 열면 연봉도 열린다는 뜻입니다.

같은 DB 안에 역할마다 다르게 열어야 하는 테이블이 섞여 있다면 OwlDB 연결에서 테이블 선택을 씁니다. DB가 이미 업무별로 나뉘어 있거나 용도별 조회 계정을 DB 관리자가 관리하고 있다면, 직접 연결의 DB 단위 권한으로 충분합니다.

어떤 문제를 해결하나요?

DB 연결과 OwlDB의 다른 글