에이전트 메모리 어댑터에서 임의의 SQL RPC를 제거한 이유
요약
에이전트 메모리 어댑터 설계 시 보안 경계를 강화하기 위해 임의의 SQL RPC 대신 제한된 목적의 RPC와 PostgREST를 사용하는 방식을 설명합니다. 보안 취약점을 줄이기 위해 데이터베이스 인터페이스를 명확히 정의하고 권한을 최소화하는 설계 원칙을 다룹니다.
핵심 포인트
- 임의의 SQL 실행은 보안 경계를 넓혀 권한 오남용 위험을 초래함
- 쓰기/삭제는 PostgREST를 사용하고 유사도 검색에만 특화된 RPC 활용
- security invoker와 고정된 파라미터를 통해 실행 권한을 최소화
- 데이터베이스 인터페이스를 명확히 정의하여 애플리케이션의 요청 범위를 제한
첫 번째 버전은 잘 작동했습니다. 그것이 문제였습니다.
저는 에이전트 메모리 어댑터(agent memory adapter)에 Supabase가 호스팅하는 pgvector 지원을 추가하고 있었습니다. 가장 빠른 방법은 유혹적이었습니다. SQL을 데이터베이스 함수로 보내고, 함수가 이를 실행하게 하며, TypeScript 측의 코드를 작게 유지하는 것이었습니다.
그렇게 했다면 데모를 만들기는 쉬웠을 것입니다. 하지만 또한 하나의 편의 기능(convenience function)을 기능에 필요한 것보다 훨씬 더 큰 보안 경계(security boundary)로 만들어 버렸을 것입니다.
그래서 저는 그것을 제거했습니다.
대체된 방식은 쓰기(writes)와 삭제(deletes)에는 일반적인 PostgREST 작업을 사용하고, 유사도 검색(similarity search)을 위해서는 하나의 목적 특화된 RPC를 사용합니다. 이 글에서는 왜 그 경계가 더 좁아지는지, 구현 방식은 어떠한지, 그리고 실제 일회용 Supabase 프로젝트를 대상으로 테스트했을 때 어떤 일이 일어났는지 설명합니다.
왜 일반적인 SQL RPC가 잘못된 추상화인가
벡터 메모리(Vector memory)에는 세 가지 작업만 필요합니다:
- 문서와 임베딩(embeddings) 저장;
- 가장 유사한 문서 찾기;
- ID를 통한 문서 삭제.
임의의 SQL을 허용하는 RPC는 이 세 가지를 모두 수행할 수 있지만, 데이터베이스 역할(database role)에 의해 허용되는 거의 모든 다른 작업도 수행할 수 있습니다. 만약 이것이 security definer와 결합된다면, 애플리케이션 코드의 실수가 호출자의 일반적인 권한을 넘어설 수 있습니다.
이것은 나쁜 거래입니다. 작은 어댑터가 개방형 실행 프리미티브(execution primitive)를 갖게 되는 셈이니까요.
더 좁은 설계는 덜 영리합니다:
- 벡터를 저장하기 위해
supabase.from(table).upsert(...)사용; - 삭제를 위해
delete().in('id', ids)사용; - 오직 유사도 검색만이 유일한 임무인 RPC 하나를 노출;
- 해당 함수에 고정된 파라미터와 고정된 반환 형태(return shape) 부여;
- 서버에 service-role 자격 증명을 유지.
여기서는 덜 영리한 것이 유용합니다. 데이터베이스 인터페이스가 애플리케이션이 요청할 수 있는 것을 정확하게 명시하기 때문입니다.
제한된 검색 함수
전체 설정은 AgentsKit 문서에 나와 있지만, 함수의 중요한 형태는 다음과 같습니다:
create or replace function public.match_agentskit_vectors(
query_embedding extensions.vector(1536),
match_count integer default 10,
...
여기에는 몇 가지 의도적인 제약 사항이 있습니다:
security invoker를 통해 함수가 호출자의 권한 하에 유지됩니다;search_path가 세션으로부터 상속되는 대신 고정됩니다;- 호출자는 SQL이 아닌 임베딩 (embedding), 개수 (count), 임계값 (threshold), 그리고 JSON 필터를 제공합니다;
- 결과 개수는 1에서 100 사이로 제한됩니다;
- 함수는 어댑터가 이해하는 컬럼 (column)들만 반환합니다.
또한 public과 anon으로부터 실행 권한을 취소(revoke)한 다음, 통합에 사용되는 서버 측 역할 (server-side role)에만 권한을 부여(grant)합니다.
이것은 보편적인 권한 부여 모델이 아닙니다. 모든 애플리케이션은 여전히 자체적인 RLS (Row Level Security) 정책과 테넌시 (tenancy) 규칙이 필요합니다. 이는 단지 "SQL 문자열을 보내줘"라고 하는 것보다 더 나은 시작 경계일 뿐입니다.
TypeScript 측은 작게 유지됩니다
어댑터는 Supabase URL과 서버 전용 자격 증명 (credential)으로 구성됩니다:
import { supabaseVectorStore } from '@agentskit/memory'
const memory = supabaseVectorStore({
...
이는 다른 백엔드와 동일한 VectorMemory 인터페이스 (surface)를 구현합니다:
await memory.store(documents)
const matches = await memory.search(queryEmbedding, {
topK: 5,
...
이것이 제가 신경 쓰는 또 다른 경계입니다. 애플리케이션 코드는 에이전트 전반에 흩어져 있는 Supabase 전용 호출이 아니라, 기능(capability)—저장(store), 검색(search), 삭제(delete)—에 의존합니다.
Supabase는 일급 백엔드 (first-class backend)로 남습니다. 다만 영구적인 아키텍처 결정이 되지 않을 뿐입니다. 팀은 배포 요구 사항이 변경됨에 따라 동일한 계약 (contract) 하에서 로컬 pgvector, Supabase, 또는 다른 벡터 저장소 (vector store)를 사용할 수 있습니다.
그것이 바로 "락인 없음 (no lock-in)"이 실제로 의미해야 하는 것입니다. 제공업체들이 동일한 척하는 것이 아니라, 그 차이점을 검사하고 교체할 수 있는 어댑터 경계에 두는 것입니다.
모의 객체(Mock)만이 아닌 실제 통합을 테스트했습니다
모의 객체 (Mocks)는 어댑터가 예상된 메서드 (methods)를 호출한다는 것을 증명했습니다. 하지만 SQL 시그니처 (signature), PostgREST 페이로드 (payloads), pgvector 연산자 (operators) 및 권한이 함께 제대로 작동하는지는 증명하지 못했습니다.
이를 위해 저는 상파울루(São Paulo)에 일회용 Supabase 무료 프로젝트를 생성하고, 커밋 e47f30cf5a938dcf865e9342a41fcf9d7d378fd1의 고정된 구현(frozen implementation)을 실행했습니다.
검증에는 피스처(fixture)를 이해하기 쉽게 유지하기 위해 세 개의 합성 벡터(synthetic vectors)와 3차원 버전의 스키마(schema)를 사용했습니다. 프로덕션(production) 예제는 1536 차원을 사용하며, 선택된 임베딩 모델(embedding model)에 맞춰 변경되어야 합니다.
결과:
- 직접적인 업서트(upsert)를 통해 세 개의 레코드 모두 저장되었습니다.
- 유사도 검색(similarity search)이 첫 번째 시도에서 통과되었습니다.
- 예상된 레코드들이
1및0.993883748801337점수와 함께 순서대로 반환되었습니다. topK, 유사도 임계값(similarity threshold), 그리고 JSON 테넌트 필터(tenant filter)가 준수되었습니다.- 제외된 테넌트에 속한 레코드는 나타나지 않았습니다.
- 직접 삭제(direct deletion)를 통해 테스트 레코드들이 제거되었습니다.
- 최종 필터링된 검색 결과 레코드가 0개로 반환되었습니다.
테스트 후, 저는 로컬 자격 증명(credential)을 제거하고 일회용 프로젝트를 삭제했습니다.
이 수치들은 벤치마크(benchmark)가 아닙니다. 이는 통합 증거(integration evidence)입니다. 즉, 좁은 인터페이스(narrow interface)가 실제 Supabase 동작 환경에서 엔드 투 엔드(end to end)로 작동함을 보여줍니다.
이것이 해결하는 것과 해결하지 못하는 것
이 패턴은 불필요한 임의의 SQL 경계(arbitrary-SQL boundary)를 제거하고 벡터 메모리(vector memory)를 교체 가능한 상태로 유지합니다. 하지만 이것이 모든 데이터베이스 접근을 자동으로 안전하게 만들어주는 것은 아닙니다.
프로덕션 배포(production deployment) 시에는 여전히 다음 사항들이 필요합니다:
- 서버 전용 자격 증명 처리(server-only credential handling);
- 실제 테넌트(tenants)를 위해 설계된 RLS(Row Level Security) 및 권한 부여(grants);
- 모델과 일치하는 임베딩 차원(embedding dimension);
- 데이터 볼륨에 따른 인덱스(indexes) 및 성능 테스트;
- 복합 필터(compound filters)나 비교 연산자(comparison operators)가 필요한 경우, 더 구체적이고 제한된 RPC.
중요한 점은 이러한 결정 사항들이 가시적으로 유지된다는 것입니다. 어댑터(adapter)는 편리한 메서드 뒤에 범용 데이터베이스 탈출구(database escape hatch)를 숨기지 않습니다.
Supabase 커뮤니티가 이 설계를 검토해 주길 바랍니다
저는 Supabase GitHub Discussions에 이 내용이 AI 통합(AI integration)에 속하는지, pgvector 프레임워크 예시인지, 아니면 문서의 다른 곳에 위치해야 하는지 묻는 제안을 올렸습니다:
https://github.com/orgs/supabase/discussions/48752
만약 에이전트 메모리 (agent memory) 또는 RAG (Retrieval-Augmented Generation)를 위해 Supabase를 사용하고 있다면, 해당 제안을 검토하고 무엇이 누락되었는지—특히 RLS (Row Level Security), 테넌시 (tenancy) 또는 RPC 경계 (RPC boundary)와 관련하여—말씀해 주세요. 별(star)을 누르는 것보다 구체적인 반례 (counterexample)를 제시해 주시는 것이 더 유용하며, 저는 그 피드백을 사용하여 오픈 소스 어댑터와 그 문서를 개선할 것입니다.
공개 사항: 저는 AgentsKit을 만들고 유지 관리합니다. 저는 공개된 구현체와 실제 일회성 프로젝트 검증을 통해 이 글을 작성했습니다. 예시 링크들은 테스트된 정확한 커밋(commit)을 가리킵니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기