RAG 파이프라인은 발생하기를 기다리는 데이터 유출입니다
요약
멀티테넌트 애플리케이션에서 RAG 파이프라인을 구축할 때, 검색(retrieval) 단계가 테넌트 격리 규율을 무시하여 데이터 유출 위험을 초래할 수 있습니다. 이는 일반적인 데이터 유출보다 심각하며, 사용자가 본 적 없는 데이터를 기반으로 답변을 생성하기 때문입니다.
핵심 포인트
- RAG 파이프라인은 별도의 데이터 경로를 만들어 테넌트 격리 규율을 무시합니다.
- 벡터 스토어에 `tenant_id`를 직접 추가하여 필터링 누락을 방지해야 합니다 (Denormalization).
- 테넌트 ID 필터를 중앙에서 강제화하여 호출자가 실수할 여지를 없애야 합니다.
여러 테넌트(multi-tenant) 애플리케이션은 아마도 잘 격리되어 있을 것입니다. 모든 쿼리가 테넌트에 범위가 지정되고, 중앙에서 강제되며, 개별 개발자가 이를 기억할 필요가 없습니다.
그런 다음 검색(retrieval)을 추가하면, 결코 같은 규율을 받지 못하는 두 번째 데이터 경로를 조용히 만들게 됩니다.
실패 모드
테넌트 A가 질문합니다. 시맨틱 검색(Semantic search)이 가장 관련성 높은 청크(chunk)를 찾습니다. 그 청크 중 하나는 테넌트 B의 문서에서 왔습니다. 모델은 그것을 읽고, 사용자가 볼 수 없었던 데이터를 사용하여 유창하게 답변합니다.
오류가 없습니다. 예외도 없고. 경고도 없습니다. 단지 다른 사람의 데이터로 구성된 자신감 있고 정확하게 들리는 답변만 있습니다.
이것은 일반적인 데이터 유출보다 더 심각한데, 그 이유는 아티팩트(artifact)가 없기 때문입니다. 사용자는 테넌트 B의 문서를 본 적이 없습니다. 그들은 우연히 그 내용을 포함하고 있는 생성된 텍스트 단락을 보았을 뿐입니다. 당신의 로그는 성공적인 요청을 보여줍니다.
왜 이런 일이 발생하는가
대부분의 팀은 먼저 애플리케이션 계층(application layer)을 구축하고 그곳에서 격리를 올바르게 수행합니다. Laravel에서는 전역 범위(global scope)입니다. Django에서는 매니저(manager)입니다. 어딘가 중앙에서 모든 쿼리에 WHERE tenant_id = ?가 추가되고, 개발자들은 더 이상 신경 쓸 필요가 없기 때문에 생각하는 것을 멈춥니다.
그런 다음 검색이 별도의 기능으로 도착합니다. 문서들이 청크화(chunked)되고 임베딩되어 벡터 스토어(vector store)에 작성됩니다. 이를 수행하는 코드는 종종 백그라운드 작업이나 수집 스크립트(ingestion script)에 존재하며, 해당 스프린트에서 AI 기능 작업을 하던 사람이 작성합니다.
이제 동일한 데이터로 가는 두 개의 경로가 생겼고, 그중 오직 하나만이 규칙을 상속받았습니다.
구체적인 예시
다음은 괜찮아 보이는 수집(ingestion) 코드입니다:
async def ingest_document(doc: Document):
chunks = chunk_text(doc.content)
embeddings = await embed_batch(chunks)
...
문제를 찾아보세요. 행에 tenant_id가 없습니다. 문서에는 있지만, 청크 테이블에는 들어가지 못했습니다. 여전히 documents를 통해 조인함으로써 되찾을 수 있지만, 이는 누군가가 조인을 기억해야 함을 의미하며, 결국 아무도 기억하지 못하게 만듭니다.
그리고 이에 따른 검색 코드는 다음과 같습니다:
Clean하고 빠르며, 어떤 테넌트의 콘텐츠든 기꺼이 반환합니다.
실제로 문제를 해결하는 방법
1. 청크(chunk)에 테넌트 정보를 비정규화하여 추가하기 (Denormalise the tenant onto the chunk)
테넌트 정보(tenant_id)를 부모 문서(parent document)에도 이미 존재함에도 불구하고 벡터 행(vector rows)에 직접 넣으세요. 이것은 의도적인 중복입니다. 이렇게 하면 조인(join)이 제거되고, 필터링을 실수로 누락할 수 없게 됩니다.
ALTER TABLE doc_chunks ADD COLUMN tenant_id uuid NOT NULL;
CREATE INDEX ON doc_chunks (tenant_id);
2. 테넌트 필터를 생략하는 것을 불가능하게 만들기
호출자(caller)에게 맡기지 마세요. 중앙에서 스코프를 지정하세요. 이는 이미 ORM이 SQL에 대해 하는 방식과 동일합니다.
async def search(query: str, tenant_id: UUID, limit: int = 6):
if tenant_id is None:
raise ValueError("tenant_id is required for retrieval")
...
기본값이 없는 필수 인자(required argument)는 저렴하면서도 효과적입니다. 깜빡하는 개발자는 누출이 아니라 오류를 받게 됩니다.
만약 Postgres를 사용한다면, 행 수준 보안(row level security)이 더 강력합니다. 왜냐하면 누가 원시 쿼리(raw query)를 작성하더라도 유지되기 때문입니다:
ALTER TABLE doc_chunks ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON doc_chunks
...
3. 유사성 검색 이후가 아닌, 이전에 필터링하기
전역적으로 상위 20개의 청크를 검색한 다음 그중 다른 테넌트에 속하는 것을 폐기하는 것은 발생할 수 있는 정확성 버그(correctness bug)이며 품질을 저하시킵니다. 결과 예산(result budget)을 버릴 행에 쓰고 있으므로, 데이터가 적은 테넌트는 더 나쁜 답변을 받게 됩니다.
먼저 필터링하세요. 인덱스에게 작업을 맡기세요.
4. 실제 사례로 격리 테스트하기
대부분의 팀은 검색이 관련성 있는 결과를 반환하는지 여부만 테스트합니다. 다른 사람의 데이터를 반환하지 않는지 여부는 거의 테스트하지 않습니다.
async def test_tenant_cannot_retrieve_other_tenant_data():
await ingest_for_tenant(TENANT_B, "The launch date is March 14th.")
...
테넌트 A가 테넌트 B만 가진 것을 요청했을 때 테스트를 작성하고, 답변이 비어 있음을 단언해야 합니다. 만약 해당 테스트가 존재하지 않는다면, 귀하는 격리(isolation) 기능이 작동한다는 것을 모르는 것입니다. 귀하는 그것을 가정하고 있는 것입니다.
더 어려운 버전
위의 모든 것은 필터링이며, 이는 애플리케이션 레벨의 제어입니다. 이 제어는 방지하려는 버그와 같은 프로세스에서 실행됩니다.
만약 테넌트 수가 적고 데이터가 충분히 민감하다면, 물리적 분리(physical separation)가 더 강력합니다. 테넌트별로 별도의 인덱스(index), 컬렉션(collection) 또는 네임스페이스(namespace)를 갖는다는 것은 필터가 누락되더라도 아무것도 유출할 수 없음을 의미합니다. 왜냐하면 다른 테넌트의 벡터는 그 연결로부터 전혀 도달할 수 없기 때문입니다.
이것은 운영상의 트레이드오프(tradeoff)가 있습니다. 테넌트별 인덱스는 수천 개의 소규모 테넌트에게 비용이 많이 들고, 마이그레이션(migrations)을 테넌트별로 실행해야 하며, 크로스-테넌트 분석(cross tenant analytics)은 그 자체의 문제가 됩니다. 수십 또는 수백 개의 테넌트를 가진 엔터프라이즈 B2B 환경, 특히 규정 준수 압박 하에서는 보통 올바른 선택입니다. 하지만 대규모 무료 등급을 가진 제품에게는 보통 그렇지 않습니다.
필터(Filters)는 바닥(floor)입니다. 물리적 격리(Physical isolation)가 천장(ceiling)입니다. 대부분의 팀은 적어도 바닥에 서 있어야 합니다.
핵심적으로 가져갈 것
SQL 계층에서는 완벽한 격리를 구현했더라도, 검색(retrieval)을 통해 여전히 유출될 수 있습니다. 왜냐하면 이들은 두 개의 데이터 경로이며 오직 하나만이 귀하의 규칙을 상속받았기 때문입니다.
임베딩 파이프라인(embedding pipeline)을 데이터베이스와 동일한 요구 사항을 가진 데이터 경로로 취급해야 합니다. 왜냐하면 그것이 정확히 바로 그것이기 때문입니다.
저는 프로덕션 AI 시스템과 그 안에서 발생하는 문제들에 대해 씁니다. 더 많은 내용은 ikramulmustafa.com에서 확인할 수 있으며, 실패 처리(failure handling)가 문서화된 RAG 에이전트가 github.com/ikramulmustafa/n8n-rag-lead-agent에 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기