최근 발생한 보안 사고들을 보면 이와 같은 비식별화가 필요한가? 라는 생각을 하게됩니다. 물론 비식별화를 했기에 자주 발생하지 않는 것이긴한데… 비식별화는 서비스 속도에 영향이 없나? 이것도 하드웨어의 발전이 이루어낸 건가? 라는 생각을 하면서 작성해봅니다. 실무에서 꽤 사용하고 있는 기술이긴 한데 비지니스 개발부서에 지탄을 받는 기술이기도 하죠.

거대 언어 모델(LLM)과 기업 내부 데이터베이스를 결합하는 RAG(검색 증강 생성) 아키텍처 환경에서, 고유식별정보(PII)를 원본 그대로 벡터 데이터베이스에 적재하는 것은 규제 법률(개인정보보호법 PIPA, GDPR 등) 위반을 초개합니다. 그러나 단순히 모든 텍스트를 임의의 난수로 치환하거나 마스킹할 경우, 고차원 임베딩 공간에서 텍스트 간의 의미적 유사도 연산이 붕괴되어 RAG의 핵심 기능인 문서 검색 품질이 급격히 저하되는 트레이드오프가 발생합니다.

Bastion-RAG 프레임워크의 데이터 보호 모듈인 Vault(internal/anonymizer/, internal/kms/)는 이 문제를 해결하기 위해 결정적 비식별화(Deterministic De-identification) 기술 스펙을 채택하여 구현되었습니다. 동일한 키 메티리얼을 사용하는 환경에서 동일한 평문 입력값은 언제나 매칭 가능한 고유의 일관된 토큰을 생성하므로, 벡터 공간 내에서 의미적 Join 연산과 컨텍스트 무결성을 유지할 수 있습니다.

본 포스트에서는 Vault 모듈의 두 단계(Phase 1, Phase 2) 처리 파이프라인 구조와 8대 비식별화 전략 인터페이스의 세부 소스 코드 구현 명세를 담백하게 분석합니다.


URL Site > https://github.com/zafrem/bastion-vault

시리즈명 : Bastion-RAG – Project 보안 RAG



1. RAG 시스템에서 결정적(Deterministic) 가공이 필수적인 기술적 이유

비식별화 과정에서 일관성(Determinism)을 결여한 무작위(Random) 토큰화 기법을 적용할 경우, RAG 시스템의 데이터 흐름 관점에서 아래와 같은 치명적인 아키텍처 결함이 발생합니다.

결정적 비식별화
  • 임베딩 및 매칭 스페이스 파쇄: 데이터 적재 시점(Ingress)에 청크 내 “홍길동”을 TOK_A로 바꾸고, 실시간 질의 시점(Query)에 유저 질문 속 “홍길동”을 TOK_B로 무작위 치환한다면, 밀집 벡터(Dense Vector) 공간 내에서 두 토큰의 코사인 유사도는 무관하게 계산되어 관련 문서를 검색(Retrieval)해 오지 못하는 인덱스 무력화가 발생합니다.
  • 관계형 데이터 조인(Join) 유실: 토큰화된 사용자 ID를 기반으로 주문 테이블과 서비스 티켓 테이블의 분산 문서 자산을 교차 참조하여 콘텍스트를 확장해야 하는 시나리오에서, 테넌트 간 식별 토큰이 일치하지 않으면 의미론적 링크 관계가 단절됩니다.
  • Phase-2 역복원 연산 오버헤드 통제: LLM 연산 통과 후 안전하게 원본 토큰을 역복원(Selective Detokenization)해야 하는 시점에서 비결정적 방식은 레지스트리 전역을 완전 탐색해야 하므로 $O(N)$ 이상의 성능 병목을 유발합니다. 결정적 구조는 고유 해시 키 기반의 $O(1)$ 룩업을 보장합니다.

다만, 결정적 비식별화는 카디널리티(Cardinality)가 낮은 필드(예: 성별, 혈액형 등)에 대해 빈도 분석 공격(Frequency Analysis Attack)에 노출될 위험이 존재합니다. 따라서 Vault 모듈은 필드의 민감도 레벨(L1~L5)에 따라 단방향 HMAC-SHA256, 대칭 키 암호화, 가변 복원 토큰화를 정밀 분기하여 적용하는 결합 아키텍처를 가집니다.

2. 비식별화 전략 인터페이스 및 주입 파라미터 구조

Vault에 탑재된 모든 비식별화 기술 아키텍처는 코드 유지보수성과 Stand-alone 가치를 보장하기 위해 단일 Strategy 인터페이스를 강제 구현하도록 빌드되었습니다.

Go

// internal/anonymizer/strategies/strategy.go

type Strategy interface {
    // Apply는 평문 문자열을 정의된 아키텍처 규칙에 따라 비식별화 토큰으로 변환합니다.
    Apply(ctx context.Context, value string, params Params) (string, error)

    // Reverse는 Reversible()이 true인 전략에 한해 토큰을 다시 평문으로 역복원합니다.
    Reverse(ctx context.Context, value string, params Params) (string, error)

    // Name은 감사 로그(Audit Trail)에 기록될 정적 전략 식별자를 반환합니다.
    Name() string

    // Reversible은 해당 전략이 물리적/논리적으로 역복원 가능한지 여부를 명시합니다.
    Reversible() bool
}

각 전략의 ApplyReverse 메서드가 실행될 때, 외부 KMS(Key Management Service) 매니저를 통해 테넌트 및 데이터 카테고리별로 안전하게 유도된 독립 키 메티리얼과 컨텍스트 필드 힌트 정보들이 Params 구조체 묶음 형태로 동적 주입됩니다.

Go

// internal/anonymizer/strategies/strategy.go

type Params struct {
    HMACKey             []byte // 결정적 토큰화 및 단방향 해시에 사용되는 32바이트 독립 키
    EncryptionKey       []byte // AES-256-GCM 대칭 키 암호화에 사용되는 32바이트 DEK
    MaskPattern         string // 부분 마스킹 필드 포맷 제어용 오버라이드 문자열
    GeneralizationLevel int    // 일반화 깊이 지표 (0=최상위 평문, 단계가 높을수록 추상화)
    Domain              string // 이메일 도메인 보존 등 특수 도메인 바인딩 문자열
    FieldHint           string // 범위 일반화 분기 처리를 위한 구조적 필드 명칭 식별자
    TenantID            string // 토큰 데이터베이스(TokenDB) 레지스트리 검색 범위를 제한하는 ID
}

3. Vault 8대 비식별화 가공 전략 세부 구현 스펙

Vault 모듈(internal/anonymizer/strategies/) 내부 핵심 연산 파이프라인에 정적 바인딩되어 가동되는 8대 핵심 처리 엔진의 세부 기술 스펙은 다음과 같습니다.

3.1 결정적 토큰화 (deterministic_tokenization)

  • 가변 복원 여부: 가능 (tokendb.Store 레지스트리 연동 필요)
  • 주요 할당 필드: 한국어 인명(korean_name), 이메일 주소(email), 사번(employee_id)

테넌트 및 카테고리별 고유 HMAC-SHA256 연산 결과물인 32바이트 바이너리 다이제스트를 URL-Safe Base64 정규화 처리를 거쳐 22자로 단축 슬라이싱합니다. 외부 시스템과의 식별자 오인 방지를 위해 TOK: 접두사를 강제 박제합니다.

Go

// internal/anonymizer/strategies/tokenization.go

func (s *TokenizationStrategy) Apply(ctx context.Context, value string, p Params) (string, error) {
    if len(p.HMACKey) == 0 {
        return "", fmt.Errorf("tokenization: HMAC key required")
    }

    mac := hmac.New(sha256.New, p.HMACKey)
    mac.Write([]byte(value))
    sum := mac.Sum(nil)

    token := base64.RawURLEncoding.EncodeToString(sum)
    if len(token) > 22 {
        token = token[:22]
    }
    tok := "TOK:" + token

    // 매핑 레지스트리 데이터베이스에 토큰-평문 쌍을 동기 적재 (Phase 2 역복원용)
    if s.store != nil {
        entry := tokendb.Entry{
            Token:     token,
            Original:  value,
            TenantID:  p.TenantID,
            PIIType:   p.FieldHint,
            CreatedAt: time.Now().UTC(),
        }
        _ = s.store.Put(ctx, entry)
    }
    return tok, nil
}

3.2 HMAC 단방향 암호화 (hmac_sha256)

  • 가변 복원 여부: 불가능 (One-Way)
  • 주요 할당 필드: 주민등록번호(korean_rrn), 외국인등록번호

가장 높은 보안 등급인 L1 Direct Identifier 필드를 파쇄하기 위해 가동됩니다. plain 무방비 해시는 주민등록번호 규격 특성상 오프라인 무차별 레인보우 테이블 공격에 취약하므로, 테넌트 격리 마스터 키에서 분리된 HMACKey를 합성하고 결과물 유실 방지를 위해 64자 전량 해시 밸류에 HMAC: 접두사를 결합합니다.

Go

// internal/anonymizer/strategies/hmac.go

func (s *HMACStrategy) Apply(_ context.Context, value string, p Params) (string, error) {
    if len(p.HMACKey) == 0 {
        return "", fmt.Errorf("hmac_sha256: HMAC key required")
    }
    mac := hmac.New(sha256.New, p.HMACKey)
    mac.Write([]byte(value))
    return "HMAC:" + hex.EncodeToString(mac.Sum(nil)), nil
}

3.3 부분 마스킹 (partial_masking)

  • 가변 복원 여부: 불가능
  • 주요 할당 필드: 휴대전화 번호(korean_mobile), 신용카드 번호(credit_card)

정적 필드 유효 가독성을 보존하기 위해 중간 문자열 영역을 와일드카드(*) 기호로 영구 치환합니다.

Go

// internal/anonymizer/strategies/masking.go

func maskMobile(v string) string {
    digits := stripNonDigits(v) // 숫자 기호만 정제 추출
    if len(digits) < 10 {
        return strings.Repeat("*", utf8.RuneCountInString(v))
    }
    return digits[:3] + "-****-" + digits[len(digits)-4:]
}

특히 휴대전화 번호 필드의 경우, 화면 표시용 문자는 010-****-5678로 마스킹하되, 내부 인프라 엔진이 일치 조건 조회를 수행할 수 있도록 뒤에서 다룰 maskForSearch 플래그 연동을 통해 고속 전용 탐색 토큰(_search_token)을 병렬로 추가 생성해 내는 분기 아키텍처를 가집니다.

3.4 AES-256-GCM 대칭 키 암호화 (encryption)

  • 가변 복원 여부: 가능 (KMS 마스터 키 복호화 권한 필수)
  • 주요 할당 필드: 급여 데이터(salary), 금융 자산 평가액

데이터의 복호화 가치와 무결성(Integrity) 검증을 동시에 달성하기 위해 인증된 대칭 암호화 모델인 AES-256-GCM 규칙을 강제 적용합니다. 연산 시 암호학적 재생 공격을 방어하기 위해 암호화 실행 시마다 고유 무작위 12바이트 논스(Nonce)를 새로 생성하여 최종 바이너리 페이로드의 헤더 영역에 강제 결합(Prepend) 적재합니다.

Go

// internal/anonymizer/strategies/encryption.go

func gcmEncrypt(key, plaintext []byte) ([]byte, error) {
    block, _ := aes.NewCipher(key)
    gcm, _   := cipher.NewGCM(block)

    nonce := make([]byte, gcm.NonceSize()) // 12바이트 암호학적 난수 생성
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return nil, err
    }
    // 논스 버퍼 자리에 직접 데이터를 누적하여 [Nonce 12B][Ciphertext][Tag 16B] 토폴로지 구축
    return gcm.Seal(nonce, nonce, plaintext, nil), nil
}

Reverse 복호화 엔진은 유입된 암호화 텍스트의 ENC: 접두사를 제거한 후 헤더에서 12바이트 논스를 슬라이싱하여 복호화를 집행하며, 만약 1비트의 무단 변조 및 데이터 truncated 손상이 적발될 경우 GCM 인증 태그 불일치 에러를 반환하고 데이터 진출을 전면 차단합니다.

3.5 주소 일반화 (generalization)

  • 가변 복원 여부: 불가능
  • 주요 할당 필드: 주소 시/도/군/구 기호(address_city)

텍스트의 고유 구체성을 거세하고 상위 개념으로 추상화 붕괴를 유도합니다. 대한민국 행정구역 트리 체계 사양에 맞춰 레벨 지표에 따라 하위 단위 토큰을 문자열 배열에서 영구 탈락시킵니다.

Go

// internal/anonymizer/strategies/generalization.go

func generalizeRegion(value string, level int) string {
    parts := strings.Fields(value)
    if len(parts) == 0 { return "" }
    
    // Level 1: "동/로/길" 최하위 상세 주소 세그먼트 영구 삭제
    if level >= 1 {
        last := parts[len(parts)-1]
        if strings.HasSuffix(last, "동") || strings.HasSuffix(last, "로") || strings.HasSuffix(last, "길") {
            parts = parts[:len(parts)-1]
        }
    }
    // Level 2: "구/군" 세그먼트 순차 삭제
    if level >= 2 && len(parts) > 0 {
        last := parts[len(parts)-1]
        if strings.HasSuffix(last, "구") || strings.HasSuffix(last, "군") {
            parts = parts[:len(parts)-1]
        }
    }
    return strings.Join(parts, " ")
}

이 처리를 통해 "서울특별시 강남구 역삼동" 원본 데이터는 Level 1 스캔 시 "서울특별시 강남구", Level 2 도과 시 "서울특별시"로 정밀 변환되어, RAG 검색 공간 내에서 광역 통계 단위의 클러스터 결합 요건을 충족하게 됩니다.

3.6 수치/날짜 범위 일반화 (range_generalization)

  • 가변 복원 여부: 불가능
  • 주요 할당 필드: 생년월일(birth_date), 나이(age), 급여 액수 범주

FieldHint 식별자를 룩업 기호로 삼아 대상 데이터 구조의 의미론적 범주 브래킷(Bracket) 구간을 동적 연산 조율합니다.

Go

// internal/anonymizer/strategies/generalization.go

func generalizeDateToDecade(value string) (string, error) {
    parts := strings.Split(strings.ReplaceAll(value, "/", "-"), "-")
    year, _ := strconv.Atoi(parts[0][:4])
    decade := (year / 10) * 10 // 10 단위 정수 연산 붕괴를 통해 10년 단위 세대로 통합
    return fmt.Sprintf("%ds", decade), nil
}

생년월일 질의 문자열인 "1985-03-15" 페이로드는 본 가동 레이어를 통과하며 고유 일자가 삭제된 "1980s" 문자열로 수렴되어 재식별 가능성을 원천 차단합니다.

3.7 값 삭제 처리 (suppression)

  • 가변 복원 여부: 불가능
  • 주요 할당 필드: 상세 주소 번지수(address_detail), 빌딩 동/호수 명칭

데이터 미니마이제이션(최소화 원칙)에 입각하여, 단 1바이트의 단서라도 유출될 경우 조합 공격에 의한 재식별 리스크가 극대화되는 상세 텍스트 자산을 완전히 파쇄합니다. 입력값 문자열 자체를 참조하지 않으며, 메모리 읽기 연산 없이 정적 상수 플레이스홀더인 [REMOVED] 데이터로 치환을 집행합니다.

Go

// internal/anonymizer/strategies/suppression.go

const suppressionPlaceholder = "[REMOVED]"

func (s *SuppressionStrategy) Apply(_ context.Context, _ string, _ Params) (string, error) {
    return suppressionPlaceholder, nil
}

4. 배치 비식별화 연산 파이프라인 아키텍처 및 처리 성능 최적화

실시간 대규모 트래픽 분산 처리를 수행하는 RAG 게이트웨이 환경에서 모든 레코드마다 머신러닝 분류기(Classifier)를 연속 구동하는 행위는 심각한 레이턴시 인프라 오버헤드를 야기합니다. Vault 엔진은 이를 최적화하기 위해 1바이트 배치 전수 추론 및 2페이즈 변환 파이프라인을 구현했습니다.

Raw Batch Records Collection (Max 1,000 units)
    │
    ▼
[Phase 1: Sampling & Detection Indexing]  ← 첫 번째 레코드만 고속 Classifier 추론 (O(1) 정적 최적화)
    │
    ▼
[KMS Token Infrastructure Mapping]         ← 테넌트 별 hmacKey, encKey 단 1회 로드 및 메모리 바인딩
    │
    ▼
[Phase 2: Matrix Parallel Transformation]  ← 정립된 탐지 맵 사양을 기반으로 루프 연산 7대 가공 전개
    │
    ├───► maskForSearch == true ? ───────► {field}_search_token 고속 검색 필드 복제 주입
    │
[Audit Governance Signing Logs]            ← 영구 저장용 JSONL 감사 레코드 HMAC 누적 생성
    │
    ▼
Anonymized Response Package Payload

4.1 오버헤드 통제를 위한 정적 탐지 인덱싱 모델

배치 내 유입된 모든 레코드가 동일한 JSON 구조적 스키마 동질성을 지닌다는 전제하에, buildDetectionIndex() 메커니즘은 오직 배치의 첫 번째 인덱스 레코드(Index 0)에 한해서만 PII 분류 추론을 실행하여 Field → PII_Type → Strategy 관계 맵을 정적 빌드합니다.

Go

// internal/anonymizer/engine.go

func (e *Engine) buildDetectionIndex(records []map[string]any, policies []fieldPolicy) map[string]fieldPolicy {
    index := make(map[string]fieldPolicy)
    if len(records) == 0 { return index }

    // 첫 번째 레코드만 Classifier를 통과시켜 필드 매핑 관계를 인덱싱 스캔
    detections := e.classifier.DetectPII(records[0])

    for _, det := range detections {
        for _, pol := range policies {
            if pol.piiType == det.PIIType {
                index[det.Field] = pol // 필드명을 해시 키로 삼아 O(1) 정적 룩업 맵 구성
                break
            }
        }
    }
    return index
}

후속 유입되는 최대 1,000개의 배치 레코드들은 분류기를 재구동하지 않고 본 정적 인덱스 구조체 명세를 그대로 상속받아 평문 필드 매핑 연산만 수행하므로, 전체 배치 변조 연산 비용을 $O(1)$의 분류기 실행 비용으로 극단적으로 억제하는 고속 연산 성과를 달성했습니다.

4.2 maskForSearch 바인딩 기법을 통한 하이브리드 검색 정합성 확보

Vault 모듈이 지닌 영리한 RAG 데이터 흐름 제어 스펙은 바로 maskForSearch 조건의 유연한 처리입니다. 휴대전화 번호나 주민등록번호와 같이 완전 가려진 마스킹 처리가 집행된 필드는 훗날 정확한 키워드 매칭 조회(Exact-match String Query) 시 검색 인덱스 탐색이 완전히 불가능해집니다.

이를 우회하기 위해 transformRecord 루틴은 정책 카탈로그 사양에 maskForSearch: true 지표가 마킹된 필드를 포착하면, 화면 표시용 데이터 구조(mobile: "010-****-5678") 외에 내부 임베딩 및 인덱스 조건 매칭용 필드 데이터인 {field}_search_token 필드를 페이로드 내에 강제 복제 생성하여 주입합니다.

이 기법을 통해 RAG 파이프라인의 데이터 미니마이제이션 무결성을 완벽하게 달성하면서도, 백엔드 검색 엔진이 고유 식별 토큰을 추적 및 비교 연산할 수 있는 엔터프라이즈 급 하이브리드 검색 정합성을 동시에 만족하게 됩니다.

5. 페이커 재작성(Faker Rewrite) 아키텍처와 임베딩 모델의 시너지 관계

Vault 모듈의 Phase 1 비식별화 처리가 완료되어 출력된 가공 문자열 페이로드는 TOK:RjIzbTYxdmVBcFVNVVhi와 같이 고차원 난수 텍스트 토큰 구조를 띄게 됩니다.

그러나 이 불투명한 암호화 문자열을 그대로 후속 하위 검색 엔진 레이어에 전달하여 사내 다국어 전용 고성능 임베딩 모델(예: BGE-M3 등)에 인젝션하면 심각한 인프라 오염이 발생합니다. 오픈소스 임베딩 모델은 문맥적 의미와 자연어 토큰 토폴로지를 학습했기 때문에, 의미가 거세된 특수 문자 토큰 문자열이 유입되는 순간 벡터 인덱스 공간 내에서 해당 임베딩 벡터 가중치를 사방으로 비산시켜 벡터 검색 품질을 엉망으로 파괴합니다.

이를 차단하기 위해 Vault 파이프라인 통과 직후 다운스트림 Navigator 인터페이스로 진입하는 경계선상에서 페이커 재작성(Faker Rewrite) 가드레일이 연동 구동됩니다.

  1. 의미론적 가짜 자연어 합성: TokenRewriter 컴포넌트는 암호화 텍스트 토큰인 TOK:RjIzbTYxdmVBcFVNVVhi 구문을 감지하면, Faker 라이브러리를 동적 호출하여 해당 도메인에 부합하는 가장 자연스러운 가짜 영미권 인명 구조인 "James Anderson" 문자열을 실시간 생성합니다.
  2. 복원용 접미사 앵커 박제: 합성된 가짜 인명 문자열 끝에 원본 암호화 토큰의 최전방 6자리 고유 해시 식별 문자를 앵커 기호로 결합하여 최종 문자열 포맷을 "James Anderson_RjIzbT"로 재작성(Rewrite)합니다.

이 아키텍처적 시너지를 통해 임베딩 모델은 의미론적으로 완벽한 클린 자연어 명세 데이터를 입력받아 고차원 벡터 인덱스 공간 내에서 최적의 밀집 임베딩 가중치 좌표를 산출해 낼 수 있게 되며, 훗날 Phase 2 역복원 시점에는 문자열 끝에 박제된 6자리 헥사 접미사 앵커를 정적 룩업 해시 키로 삼아 레지스트리에서 원래의 평문 데이터를 오차 없이 추적 및 역복원해 내는 초고속 거버넌스를 완성하게 됩니다.

6. 결론: 보안 무결성과 모델 성능의 완벽한 기술적 균형점

[Bastion-RAG 2 – Vault]의 결정적 비식별화 파이프라인을 온전하게 아키텍처화하는 엔지니어링 여정은 RAG 보안 거버넌스의 황금률을 세우는 과정이었습니다. 단순 마스킹이나 전역 단방향 일방 해시 적용이라는 1차원적 접근을 배격하고, 데이터의 속성과 다운스트림 ML 인프라의 거동 특성을 정밀 계산하여 설계된 8대 가공 전략은 시스템의 성능 마찰을 완벽하게 통제합니다.

배치 처리 시 첫 번째 인덱스 정보만 정적 스캔하는 고속 인덱싱 모델과 maskForSearch 하이브리드 필드 복제 기법, 그리고 임베딩 모델의 수치 연산 파괴를 예방하는 페이커 재작성 가드레일 결합을 통해, Vault 모듈은 p95 레이턴시 목표 임계치인 Pseudonymization 처리 < 5ms 이내의 고성능 연산 지표를 프로덕션 환경에서 완벽히 달성해 냈습니다.

데이터 유출 위험을 물리적으로 차단하면서도 인공지능 지식 검색 엔진의 정확도를 소수점 첫째 자리까지 무결하게 보존하려는 최고정보보호책임자(CISO)와 인공지능 파이프라인 리드 엔지니어들에게 본 결정적 가공 스펙 사양은 엔터프라이즈 RAG 인프라 보호를 달성할 수 있는 가장 명확하고 고도화된 기술적 해법이 될 것입니다.

By Mark