Trans-RAG: Query-Centric Vector Transformation으로 조직 간 RAG 보안 강화

·

서론

A제약사의 AI 연구팀이 신약 후보 물질을 검색하려 한다. 문제는 핵심 데이터가 파트너사 B의 서버에 있다는 점이다. 기존 방식이라면 B사의 데이터베이스에 질의를 보내고, 암호화된 인덱스에서 결과를 받아 복호화해야 한다. 그런데 이 복호화 순간, 보안의 사각지대가 열린다. 전송 계층에서는 완벽한 암호화가 유지되었지만, 검색 결과를 활용하는 지점에서 plaintext가 노출되는 것이다.

2024년 Healthcare 정보보호 동향 보고서에 따르면, 조직 간 데이터 협업 과정에서 발생하는 보안 사고의 34%가 바로 이 “복호화 이후 구간"에서 일어난다. Homomorphic Encryption(동형암호)이 이 문제의 해결책으로 떠올랐지만, 검색 시스템에 적용하면 연산 오버헤드가 100배 이상 증가하는 현실적인 한계가 있다.

바로 이 지점에서 Trans-RAG가 근본적으로 다른 접근을 제시한다. “암호화된 상태로 연산하자"는 기존의 패러다임을 벗어나, “처음부터 다른 언어(의미 공간)로 존재하게 하자"는 발상의 전환이다. 각 조직이 자신만의 고유한 vector space language를 가지면, 외부 쿼리는 해당 언어로 “번역"되어 들어오고 원본 데이터는 결코 원래 형태로 존재하지 않는다.

이 글에서는 2025년 4월 arXiv에 발표된 “Trans-RAG: Query-Centric Vector Transformation for Secure Cross-Organizational Retrieval” 논문을 중심으로, 이 혁신적인 접근법의 원리와 구현, 그리고 실무 적용 가능성을 깊이 있게 분석한다.

본론

문제 정의: Cross-Organizational RAG의 세 가지 딜레마

RAG(Retrieval-Augmented Generation) 시스템이 조직 경계를 넘어 배포될 때, 다음 세 가지 요구사항이 동시에 충족되어야 한다.

요구사항기존 Homomorphic EncryptionFederated LearningTrans-RAG
데이터 기밀성암호문 상태 연산로컬 데이터 유지수학적 의미 공간 격리
검색 정확도높음 (이론적)중간 (모델 의존)높음 (nDCG@10 3.5% 감소만)
연산 효율성낮음 (100x+ 오버헤드)중간 (통신 비용)높음 (Native retrieval 속도)
Plaintext 노출복호화 시 노출로컬에 존재원칙적 불가능
구현 복잡도매우 높음높음중간

핵심 통찰은 “데이터를 숨기는 것"이 아니라 “데이터를 이해할 수 없게 만드는 것"이다. 이를 위해 Trans-RAG는 vector space language라는 개념을 도입한다.

Vector Space Language: 핵심 개념

일반적인 embedding space에서 “apple"이라는 단어는 특정 좌표에 위치한다. Word2Vec이나 BERT, OpenAI의 text-embedding-ada-002 모두 이 좌표를 다르게 매핑하지만, 같은 모델을 사용하면 동일한 좌표를 공유한다. 즉, 누구나 이 좌표계를 알면 의미를 역추적할 수 있다.

Vector space language는 이 좌표계 자체를 조직 고유의 것으로 변환한다. 수학적으로 표현하면:

$$\text{Trans}(\mathbf{v}) = R \cdot S \cdot \mathbf{v} + \mathbf{b} + \epsilon$$

여기서:

  • $R$: 직교 회전 행렬 (Orthogonal Rotation Matrix)
  • $S$: 스케일링 행렬 (Scaling Matrix)
  • $\mathbf{b}$: 바이어스 벡터 (Bias Vector)
  • $\epsilon$: 노이즈 (Gaussian Noise)

이 변환의 아름다움은 “의미적 관계는 보존하되, 절대적 위치는 파괴"한다는 점이다. “왕 - 남자 + 여자 = 여왕"이라는 관계는 유지되지만, “왕"이 원래 어떤 단어였는지는 변환된 공간에서 알 수 없다.

Trans-RAG 아키텍처

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
graph TD
    A[User Query] --> B[Source Embedding]
    B --> C[vector2Trans Encoder]
    C --> D[Transformed Query]
    D --> E[Target Organization Vector DB]
    E --> F[Retrieved Documents]
    F --> G[LLM Generation]
    
    H[Organization A Corpus] --> I[vector2Trans Transform]
    I --> J[Vector DB A]
    
    K[Organization B Corpus] --> L[vector2Trans Transform]
    L --> M[Vector DB B]
    
    J --> E
    M --> E

vector2Trans: 다단계 변환 메커니즘

vector2Trans 알고리즘은 다음 5단계로 구성된다.

Step 1: Key Generation 각 조직은 고유한 변환 키 쌍을 생성한다. 이 키는 외부에 절대 노출되지 않는다.

Step 2: Space Rotation 직교 행렬(Orthogonal Matrix)을 사용하여 embedding space를 회전시킨다. 이 회전은 내적(dot product)의 결과를 보존하므로 코사인 유사도 계산이 가능하지만, 축의 의미를 완전히 재배치한다.

Step 3: Dimensional Scaling 각 차원에 독립적인 스케일링을 적용하여 거리 정보를 왜곡한다.

Step 4: Translation & Noise 바이어스 벡터를 더하고 미세한 가우시안 노이즈를 추가하여 원본 공간으로의 역변환을 수학적으로 불가능하게 만든다.

Step 5: Query Transformation 검색 시, 소스 조직의 쿼리를 타겟 조직의 vector space language로 변환하여 전송한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import numpy as np
from scipy.stats import ortho_group
from typing import Tuple

class Vector2Trans:
    """
    Trans-RAG의 vector2Trans 변환기 구현
    각 조직은 고유한 변환 파라미터를 보유
    """
    
    def __init__(self, dim: int, noise_scale: float = 0.01):
        self.dim = dim
        self.noise_scale = noise_scale
        
        # 직교 회전 행렬 생성 (Haar measure)
        self.rotation = ortho_group.rvs(dim)
        
        # 랜덤 스케일링 (각 차원에 대해)
        self.scaling = np.random.uniform(0.5, 2.0, size=dim)
        
        # 바이어스 벡터
        self.bias = np.random.randn(dim)
        
        # 노이즈 스케일
        self.noise_scale = noise_scale
    
    def transform(self, vectors: np.ndarray) -> np.ndarray:
        """
        원본 벡터를 조직 고유의 vector space로 변환
        
        Args:
            vectors: (N, dim) 형태의 원본 embedding 벡터
        
        Returns:
            변환된 벡터 (N, dim)
        """
        # Step 1: Rotation
        rotated = np.dot(vectors, self.rotation.T)
        
        # Step 2: Scaling
        scaled = rotated * self.scaling
        
        # Step 3: Translation + Noise
        noise = np.random.normal(0, self.noise_scale, size=scaled.shape)
        transformed = scaled + self.bias + noise
        
        return transformed
    
    def transform_query(
        self, 
        query_vector: np.ndarray, 
        target_key: Tuple
    ) -> np.ndarray:
        """
        쿼리를 타겟 조직의 vector space로 변환
        
        Args:
            query_vector: (dim,) 형태의 쿼리 embedding
            target_key: 타겟 조직의 공개 변환 키
        
        Returns:
            변환된 쿼리 벡터
        """
        target_rot, target_scale, target_bias = target_key
        
        # 소스 공간에서 역변환 후 타겟 공간으로
        intermediate = query_vector.reshape(1, -1)
        
        # 타겟 조직의 변환 적용
        rotated = np.dot(intermediate, target_rot.T)
        scaled = rotated * target_scale
        transformed = scaled + target_bias
        
        return transformed.flatten()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 사용 예시
if __name__ == "__main__":
    dim = 768  # BERT-base embedding dimension
    org_a = Vector2Trans(dim, noise_scale=0.01)
    org_b = Vector2Trans(dim, noise_scale=0.01)
    
    # 조직 A의 문서 embeddings
    doc_embeddings = np.random.randn(1000, dim)
    
    # 조직 A의 vector space로 변환하여 저장
    transformed_docs = org_a.transform(doc_embeddings)
    
    # 쿼리 처리 (조직 B에서 조직 A로 검색)
    query = np.random.randn(dim)
    
    # 조직 A의 공개 키 (rotation, scaling, bias만)
    org_a_public_key = (org_a.rotation, org_a.scaling, org_a.bias)
    
    # 쿼리를 조직 A의 공간으로 변환
    transformed_query = org_b.transform_query(query, org_a_public_key)
    
    # 검색 수행 (코사인 유사도)
    similarities = np.dot(
        transformed_docs, 
        transformed_query
    ) / (
        np.linalg.norm(transformed_docs, axis=1) * 
        np.linalg.norm(transformed_query)
    )
    
    top_k_idx = np.argsort(similarities)[-5:][::-1]
    print(f"Top-5 검색 결과 인덱스: {top_k_idx}")
    print(f"최고 유사도: {similarities[top_k_idx[0]]:.4f}")

보안 분석: 수학적 격리 보장

논문의 핵심 기여 중 하나는 변환된 vector space 간의 수학적 독립성을 증명한 것이다. 실험 결과는 인상적이다.

보안 메트릭측정값의미
Angular Separation89.90°두 조직의 공간이 거의 직교
Isolation Rate99.81%크로스 공간 검색 실패율
Reconstruction Error>95%원본 복원 시도의 오차율
Brute-force Resistance2^(dim/2)차원 공격의 복잡도

89.90°의 각도 분리가 의미하는 바는 직관적이다. 90°가 완전한 직교(어떤 정보도 공유하지 않음)이므로, 89.90°는 “사실상 독립적"임을 수학적으로 보여준다.

성능 평가: 8 × 3 × 3 실험 매트릭스

논문은 8개 retriever, 3개 dataset, 3개 LLM으로 구성된 포괄적인 실험을 수행했다.

Retriever 구성:

  • Sparse: BM25, SPLADE
  • Dense: DPR, ANCE, TAS-B, GenQ, Aggretriever, BGE

Dataset:

  • Natural Questions (NQ)
  • TriviaQA
  • MS MARCO

LLM:

  • GPT-3.5-Turbo
  • Llama-2-70B
  • Mistral-7B
메트릭Baseline (No Security)Homomorphic Enc.Trans-RAG차이
nDCG@100.6720.6580.649-3.5%
Recall@1000.8410.8230.830-1.3%
MRR0.6210.6090.604-2.7%
검색 지연 (ms)12.31,245.614.1+14.6%
吞吐量 (QPS)8138.2709-12.8%

nDCG@10 기준 3.5% 감소는 Homomorphic Encryption 대비 연산 효율성의 극적 개선(100배 이상)을 고려하면 실무적으로 충분히 수용 가능한 trade-off이다.

실무 적용 가이드

Trans-RAG를 프로덕션 환경에 적용하기 위한 단계별 가이드를 제공한다.

Phase 1: 기존 RAG 파이프라인 분석

현재 사용 중인 embedding model, vector DB, retriever 구성을 파악한다. Trans-RAG는 embedding model에 독립적이므로 기존 인프라 변경이 최소화된다.

Phase 2: 변환 키 관리 체계 구축

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 변환 키 관리를 위한 간단 예시
from dataclasses import dataclass
import hashlib
import os

@dataclass
class OrgKeyPair:
    """조직별 변환 키 쌍"""
    org_id: str
    private_rotation: np.ndarray
    private_scaling: np.ndarray
    private_bias: np.ndarray
    
    def get_public_key(self) -> bytes:
        """공개 키 해시 생성 (키 자체는 직접 교환)"""
        key_data = (
            self.private_rotation.tobytes() +
            self.private_scaling.tobytes() +
            self.private_bias.tobytes()
        )
        return hashlib.sha256(key_data).digest()
    
    def rotate_key(self):
        """주기적 키 로테이션"""
        dim = self.private_rotation.shape[0]
        self.private_rotation = ortho_group.rvs(dim)
        self.private_scaling = np.random.uniform(0.5, 2.0, size=dim)
        self.private

출처: http://arxiv.org/abs/2604.09541v1