에이전트·RAG

AGENT / 57번째 글

한 색인에 여러 고객사를 담을 때

테넌트를 가르는 방법은 전용 색인, 색인 안의 칸, 값 하나로 구분하기 셋입니다. 격리 강도와 비용이 어떻게 맞바뀌는지, 조건 하나에 격리를 맡기면 왜 위험한지, 이탈과 삭제를 어떻게 준비할지 정리합니다.

PALDYN Team13 MIN READ

지난 글 끝에서 권한 값이 낡으면 필터가 소용없다는 이야기를 했다. 그 문제가 가장 커지는 자리가 여기다 — 한 시스템이 여러 고객사의 문서를 함께 다룰 때.

사내용 RAG에서는 검색이 조금 새도 대개 사고까지는 안 간다. 같은 회사 사람이 못 볼 문서를 본 것이다. 그런데 다른 회사의 문서가 검색되면 그것은 품질 문제가 아니라 계약 위반이고, 대개 한 건으로도 서비스가 끝난다. 그래서 이 설계는 성능보다 격리를 먼저 놓고 시작한다.

가르는 방법이 셋이다

여러 고객사를 담는 세 가지 모양

고객사마다 색인을 따로 두는 방식이 가장 세다. 파일이 아예 다르니 조건을 빼먹어도 남의 것이 나올 수 없다. 고객사별로 임베딩 모델이나 청크 크기를 다르게 가져갈 수도 있고, 이탈하면 색인 하나를 지우면 끝난다.

한계는 개수다. 근사 탐색 색인은 하나마다 고정 비용이 붙는다 — 그래프 구조, 메모리에 상주하는 부분, 연결 자원. 문서가 열 개뿐인 고객사도 그 비용을 그대로 쓴다. 수백 개까지는 버티지만 수천 개가 되면 대부분이 거의 빈 색인인 채로 메모리를 먹는 상황이 된다.

한 색인 안에 칸을 나누는 방식이 절충이다. 저장소마다 이름이 다르지만(네임스페이스, 파티션, 컬렉션 안의 구획) 하는 일은 같다. 검색할 때 칸을 지정하면 그 안에서만 찾고, 다른 칸의 벡터는 탐색 대상에 아예 안 들어간다. 조건으로 거르는 것이 아니라 찾는 범위 자체가 다르다는 점이 중요하다. 앞 글에서 본 선택도 문제가 생기지 않는다.

값 하나로 구분하는 방식은 모든 청크에 tenant 값을 넣고 검색할 때마다 조건을 거는 것이다. 비용이 거의 안 붙어 고객사가 수만이어도 된다. 대신 격리가 조건 하나에 달려 있다. 그 조건을 빠뜨린 코드 경로가 하나만 있어도 샌다.

전용 색인 색인 안의 칸 값으로 구분
격리 물리적 탐색 범위가 갈림 조건 하나
고객사당 고정 비용 크다 작다 거의 없다
현실적 상한 수백 수만 사실상 없음
고객사별 설정 자유롭다 일부 가능 어렵다
이탈 처리 색인을 지운다 칸을 지운다 조건으로 골라 지운다

하나를 고르는 문제가 아니다. 문서가 많고 요구가 까다로운 큰 고객사 몇 곳만 전용 색인으로 떼고, 나머지 대다수는 한 색인에 담는 구성이 흔하다. 요금제와 나란히 가는 경우도 많다.

조건 하나에 맡기지 않는다

값으로 구분하는 방식을 쓴다면 — 대부분 여기서 시작한다 — 격리를 코드 한 줄에 걸어 두지 않는 것이 핵심이다.

한 겹으로 막지 않는다

테넌트는 요청에서 받지 않는다. 세션이나 인증 토큰에서 서버가 정한다. 요청 본문에 tenant 값을 받아 그대로 쓰는 코드는 그 값을 바꾸면 남의 데이터가 나온다는 뜻이다. 모델이 질의에서 뽑아내게 하는 것은 더 나쁘다 — 프롬프트 주입의 표적이 되기 딱 좋다.

조건은 검색 계층 안쪽에서 붙인다. 호출하는 쪽이 조건을 넘기게 하지 말고, 검색 함수가 세션에서 읽어 무조건 덧붙이게 만든다. 그러면 새 기능을 만들다 빠뜨릴 여지가 없다.

def search(query, k=10, filters=None):
    f = dict(filters or {})
    f["tenant"] = current_session.tenant_id   # 넘겨받지 않고 여기서 정한다
    hits = store.query(embed(query), k=k, filter=f)
    return [h for h in hits if h.meta["tenant"] == f["tenant"]]  # 한 번 더 본다

마지막 줄이 세 번째 층이다. 결과에 다른 테넌트의 것이 섞여 있으면 버리고 경보를 올린다. 정상이라면 절대 걸리지 않는 검사이므로 평소에는 비용이 0에 가깝다. 걸린다면 조건이 안 먹었거나 적재 때 값이 잘못 들어갔다는 뜻이고, 그 사실을 아는 것 자체가 이 검사의 값어치다.

캐시와 로그도 함께 갈라야 한다. 격리를 꼼꼼히 해 놓고 질의 결과 캐시의 키를 질의 문자열로만 만들면, 같은 질문을 한 다른 고객사에게 앞 사람의 답이 그대로 간다. 임베딩 캐시는 질의 텍스트만 다루니 공유해도 되지만, 검색 결과와 생성된 답의 캐시에는 테넌트를 키에 넣는다. 로그도 마찬가지다 — 근거 문서 내용이 로그에 남는데 로그 색인이 하나면 운영 화면에서 다 보인다.

시끄러운 이웃

격리를 다 해도 자원은 공유한다. 한 고객사가 문서 백만 건을 한꺼번에 올리면 색인 작업이 그쪽으로 쏠리고, 그동안 다른 고객사의 갱신은 대기열 뒤에 선다. 앞 글에서 본 처리 지연이 남의 사정으로 몇 시간이 되는 것이다.

셋으로 나눠 막는다.

  • 대기열을 고객사별로 나눈다. 하나의 큰 대기열 대신 고객사마다 줄을 세우고 돌아가며 처리하면, 대량 적재가 남의 순서를 밀어내지 못한다
  • 적재 속도에 상한을 둔다. 시간당 처리할 문서 수를 고객사별로 정해 둔다. 급한 대량 이관은 별도 경로로 받는다
  • 검색 쪽에도 동시 실행 상한을 둔다. 속도 제한에서 다룬 그 장치가 여기서도 필요하다. 한 고객사의 트래픽 급증이 전체 지연으로 번지는 것을 막는다

이탈을 미리 설계해 둔다

계약이 끝나면 데이터를 지워야 하고, 대개 기한과 증빙이 함께 요구된다. 이것을 나중에 생각하면 곤란해지는 이유는 데이터가 색인 한 곳에만 있는 것이 아니어서다.

지울 자리를 세어 보면 보통 이만큼이다.

어디 흔히 빠뜨리는 이유
벡터 색인의 청크 여기는 안 빠뜨린다
원문 보관소 파싱 전 원본 파일을 따로 두는 경우가 많다
결과·응답 캐시 만료만 걸어 두고 삭제 대상으로 안 본다
질의 로그·대화 기록 보존 기간 정책과 삭제 요구가 서로 어긋난다
평가용 데이터셋 실제 질의를 골라 만들어 둔 것이 남는다
백업 지운 뒤 만든 백업에는 없지만 옛 백업에는 있다

전용 색인 방식이 여기서 값을 한다. 색인 하나와 그에 딸린 저장소를 지우면 끝나므로 증빙도 간단하다. 값으로 구분하는 방식은 위 목록을 하나씩 훑어야 하고, 그래서 삭제 절차를 코드로 만들어 두고 시험 고객사로 정기적으로 돌려 보는 편이 안전하다. 계약 종료 통보를 받고 처음 짜기 시작하면 반드시 뭔가 빠진다.

앞 글에서 다룬 지움 표시가 여기서도 쓸모 있다. 계약 종료 즉시 표시를 세워 검색에서 빼고, 유예 기간이 지난 뒤 실제로 지우면 노출은 곧바로 멎으면서 되돌릴 여지도 남는다.

평가도 고객사마다 다르다

마지막으로 자주 놓치는 것 하나. 전체 평균 검색 품질이 좋아도 특정 고객사에서는 나쁠 수 있다. 문서 종류가 다르고, 용어가 다르고, 질문 방식이 다르기 때문이다.

RAG 평가를 돌릴 때 결과를 고객사별로 나눠 보면 대개 편차가 꽤 크다. 문서가 많은 몇 곳이 평균을 끌어올리고, 문서가 적거나 형식이 특이한 곳은 평균 아래에 조용히 깔려 있다. 그리고 불만은 그 아래쪽에서 나온다.

작은 고객사에서 잘 되는지를 따로 본다. 문서가 스무 건뿐인 고객사에서는 검색이 뽑을 후보 자체가 적어, 어떤 질문에도 그 스무 건 중 열 개가 나온다. 관련 없는 것을 근거로 답을 만들지 않도록 최소 점수 기준을 두고, 못 미치면 「자료에 없습니다」라고 답하게 하는 편이 낫다. 이 판단은 고객사 규모에 따라 달라져야 한다.

정리

  • 가르는 방법은 전용 색인 · 색인 안의 칸 · 값으로 구분 셋이고, 격리와 비용이 맞바뀐다
  • 전용 색인은 개수가 벽이다. 색인마다 고정 비용이 붙어 빈 색인도 자원을 쓴다
  • 칸으로 나누면 탐색 범위 자체가 갈려 선택도 문제가 안 생긴다
  • 값으로 구분할 때는 층을 여러 겹 둔다 — 세션에서 확정, 계층에서 강제 주입, 결과 재확인
  • 캐시 키와 로그에도 테넌트를 넣는다. 격리를 다 해 놓고 캐시로 새는 일이 흔하다
  • 대기열과 속도 상한을 고객사별로 나눠 대량 적재가 남을 밀지 않게 한다
  • 이탈 삭제는 색인 밖에도 자리가 많다. 절차를 코드로 만들어 미리 돌려 본다
  • 평가는 평균이 아니라 고객사별로 나눠 본다. 작은 고객사가 평균 아래에 깔린다

읽어주셔서 감사합니다. 😊

LATEST

에이전트·RAG의 최신 글

에이전트·RAG2026.08.23

에이전트는 어디서 어긋나기 시작하는가

에이전트가 이상한 답을 낼 때 증상은 마지막에 보이지만 어긋난 자리는 그보다 앞입니다. 한 걸음이 무너지는 여섯 자리를 나누고, 증상에서 원인을 거슬러 찾는 방법과 자리별 처방을 정리합니다.

11 MIN
에이전트·RAG2026.08.23

에이전트가 무엇을 했는지 나중에 알 수 있게 만들기

에이전트는 같은 입력에도 다른 경로로 갑니다. 그래서 로그 몇 줄로는 왜 그렇게 됐는지 복원이 안 됩니다. 트레이스를 어떻게 나누고 구간마다 무엇을 붙이며 어떤 지표를 볼지 정리합니다.

11 MIN
에이전트·RAG2026.08.23

에이전트 비용은 걸음 수보다 빨리 늘어난다

걸음이 두 배면 비용은 두 배가 아니라 서너 배입니다. 왜 그렇게 되는지, 어디서 새는지, 상한을 몇 겹으로 어떻게 거는지와 실제로 효과가 큰 순서대로의 대응을 정리합니다.

13 MIN