에이전트 메모리에서 '포스트 필터 테넌트 스코핑'을 사용하면 데이터가 유출됩니다
You're leaking data if your agent memory uses post filter tenant scoping
핵심 요약
에이전트 메모리 설계 시 검색, 캐싱, 엔티티 해결 단계에서 테넌트 격리가 제대로 이루어지지 않으면 데이터 유출이 발생할 수 있습니다.
- 검색 단계 격리 — 프롬프트 필터링 이전에 벡터 검색 단계에서부터 테넌트별로 데이터를 분리해야 함
- 그래프 경계 관리 — 노드뿐만 아니라 엣지 순회 시에도 테넌트 경계를 확인하여 교차 유출을 방지해야 함
- 캐시 키 설계 — 캐시 키에 테넌트 정보를 포함하지 않으면 타 테넌트의 데이터가 노출될 위험이 있음
- 데이터 삭제 검증 — 단순 플래그 업데이트가 아닌 파티션 삭제 등 물리적 삭제를 통해 데이터 완전 제거를 보장해야 함
처음에는 공유 인덱스를 쓰고 시스템 프롬프트에 "고객 X의 메모리만 사용해"라고 적어놨는데, 이건 안 먹힘. 왜냐면 모델이 뭘 하기도 전에 검색 단계에서 이미 데이터가 다 새버리거든.
데이터가 새는 지점
프롬프트는 마지막 단계임. 즉, 범위 밖의 레코드가 컨텍스트 윈도우에 들어왔다는 건 이미 읽히고 쿼리까지 됐다는 뜻이라 토큰만 날린 거임. 프롬프트 규칙 하나만 믿는 건 사실상 아무것도 안 하는 거나 다름없음.
실패 경로는 크게 세 가지인데, 전부 검색 단계에서 발생함.
1. 벡터 검색에서의 사후 필터링(Post filtering)
메타데이터에 tenant_id를 넣고 사후 필터링으로 범위를 제한하는 단일 인덱스 방식임. ANN 검색이 전체 인덱스를 다 뒤져서 상위 k개를 뽑은 다음, 테넌트랑 안 맞는 행을 버리는 구조거든. 이러면 결과값은 줄어들고 소규모 테넌트는 재현율(recall)이 떡락함. 게다가 검색 과정에서 다른 테넌트의 벡터까지 다 훑게 됨.
사전 필터링이 도움은 되지만, 일부 ANN 구현체는 필터가 그래프 대부분을 가려버리면 성능이 개판이 됨. 메타데이터를 쓴 단일 인덱스는 존나 불안정해서 개발 단계에선 안 보이다가 실제 테넌트 수만큼 쌓이면 바로 문제 터짐.
우리는 파티션 방식으로 바꿨고, 모든 쓰기 작업에 파티션 키를 필수로 넣게 했음.
2. 그래프가 테넌트 경계를 무시함
스토어에 그래프가 있다면 시드 노드만 제한하는 걸로는 부족함. 엣지를 타고 넘어갈 때마다 범위를 체크해야 함.
두 테넌트가 같은 벤더를 공유한다고 치자. 그럼 하나의 글로벌 노드로 연결되는데, 테넌트 A에서 두 번만 건너뛰면 그 노드에 닿고 바로 테넌트 B의 서브 그래프로 넘어가 버림. 모든 레코드에 테넌트 ID가 제대로 박혀 있어도 엣지 자체는 경계라는 개념을 모르거든.
그래서 우린 노드가 아니라 서브 그래프 단위로 범위를 제한함. 현실 세계에선 같은 물건이라도 엔티티는 테넌트별로 따로 관리하는 거지. 그래, 데이터 중복 저장되는 거 맞는데 어쩌라고.
3. 캐시 키에 테넌트 정보가 없음
캐시가 쿼리 텍스트랑 임베딩 해시를 기준으로 키를 잡으면, 테넌트 A가 검색해서 따끈따끈하게 만들어둔 결과가 테넌트 B한테 그대로 나감. 임베딩 캐시, 중복 제거 테이블, 리랭크 캐시 전부 똑같은 버그 생기기 딱 좋음. 오늘 당장 grep 돌려봐라. 한 줄짜리 버그인데 터지면 진짜 걷잡을 수 없이 꼬임.
우리가 모델링하는 방식
범위 제한은 데이터를 쓸 때 바로 붙임. 만약 범위 정보가 없으면 쓰기 작업 자체가 실패함. '기본 버킷(default bucket)' 같은 건 절대 안 씀. 기본 버킷은 나중에 누가 탈퇴할 때까지 아무도 모르게 데이터가 줄줄 새는 주범이거든.
계층 구조는 사용자 -> 고객 -> 클라이언트 순이고, 이걸 머티리얼라이즈드 패스(materialised path)로 저장함. 검색할 때는 필터가 아니라 해결된 범위 경로를 인자로 넘김. 범위 정보 없이는 쿼리 자체가 안 돌아가게 막아놨음. 범위 밖의 레코드는 애초에 검색 대상이 아니니까 프롬프트 인젝션 같은 건 신경 쓸 필요도 없음. 데이터 자체가 프로세스로 넘어오질 않으니까.
엔티티 해석(Entity resolution)도 범위를 제한해야 함
중복 제거를 위한 유사도 검색에 범위 제한 안 걸어두면 테넌트끼리 엔티티가 섞여버림. 이건 읽기 단계에서 새는 것보다 훨씬 심각함. 데이터가 스토어에 잘못된 상태로 박제되고, 접근 제어로는 막을 방법도 없거든.
해석 작업은 철저히 범위 안에서만 돌아감. 매칭 확신도에 따라 전략 순서를 정하고, 임계값 미만이면 자동 병합 안 하고 검토 큐로 넘김. 처음엔 중복을 줄이려고 튜닝했는데, 그건 멍청한 짓이었음. 병합 안 된 중복은 그냥 귀찮은 정도지만, 잘못 병합된 건 바로 사고로 이어지니까.
삭제가 가장 확실한 테스트

