[Elasticsearch] Efficient Data Enrichment Using Enrich Processor

Elasticsearch를 쓰다 보면 Index document에 대량으로 새로운 필드를 추가해야 하는 상황이 생긴다.
update-by-query나 bulk update를 시도하기엔 문서 수가 너무 많거나, 문서 하나하나를 업데이트해야 해서 작업 비용이 커지는 경우도 있다.

이런 문제를 해결하기 위해 선택한 기능이 바로 Enrich Processor이다.

Enrich를 사용하면 문서를 인덱싱하는 시점에 다른 인덱스의 참조 데이터를 자동으로 병합할 수 있어서, 복잡한 업데이트 작업 없이 깔끔하게 데이터를 보강할 수 있다.

Enrich란?

Enrich Processor는 인덱싱 과정에서 들어오는 문서를 대상으로, 미리 준비된 참조 데이터(reference data) 를 자동으로 붙이는 기능이다. 한마디로 “문서 저장 전에 실시간 lookup + 자동 join” 을 수행하는 구조이다.

Enrich 기능은 다음 3가지 요소로 구성된다.

Source Index참조 데이터가 들어 있는 인덱스 (예: 제품 정보, 사용자 정보)
Enrich Policy어떤 필드로 매칭하고 어떤 필드를 보강할지 정의
Enrich IndexPolicy 실행 시 생성되는 최적화된 시스템 인덱스 (.enrich-*)

Enrich Processor는 매번 검색을 수행하지 않고, 정적·최적화된 enrich index를 조회하기 때문에 성능 손실 없이 빠르게 보강 작업을 수행한다.

동작 예시

사용자의 이메일을 기반으로 해당 사용자의 기본 정보를 문서에 자동 보강하는 상황을 살펴본다.

1) Source Index 준비

PUT users/_doc/1?refresh=wait_for
{
  "email": "alice@example.com",
  "first_name": "Alice",
  "city": "Seoul"
}

2) Enrich Policy 생성

PUT /_enrich/policy/user-policy
{
  "match": {
    "indices": "users",
    "match_field": "email",
    "enrich_fields": ["first_name", "city"]
  }
}

3) Enrich Policy 실행 → enrich index 생성

POST /_enrich/policy/user-policy/_execute

여기까지 수행하면 .enrich-user-policy-* 형태의 전용 인덱스가 생성된다.

4) Ingest Pipeline 생성

PUT /_ingest/pipeline/user_lookup
{
  "processors": [
    {
      "enrich": {
        "policy_name": "user-policy",
        "field": "email",
        "target_field": "user_info",
        "max_matches": 1
      }
    }
  ]
}

5) 문서 인덱싱

PUT /logs/_doc/1?pipeline=user_lookup
{
  "email": "alice@example.com"
}

// 결과
{
  "email": "alice@example.com",
  "user_info": {
    "first_name": "Alice",
    "city": "Seoul"
  }
}

문서가 인덱싱될 때 자동으로 참조 데이터가 붙는 구조다.
update-by-query 없이 “인덱싱 순간에 데이터가 완성되는” 형태라고 보면 된다.

제한사항

❗ 자주 변경되는 참조 데이터를 Enrich로 처리하는 경우

Enrich Processor는 source index를 직접 조회하지 않고 Enrich Index를 조회하는 구조이다. 따라서 참조 데이터가 변경되면 enrich index를 재생성하는 작업을 반복하게 된다.

참조 데이터가 자주 바뀐다면 재생성 작업을 반복해야 하고, 그만큼 시스템 부하도 증가한다.
특히 대규모 인덱스에서는 재생성 비용이 커져 전체 ingest 성능에 영향을 줄 수 있다.

따라서 Enrich는 자주 변하지 않는 데이터인 경우에 사용하는 것이 적합하다.

[LLM] Parameter-Efficient Fine-Tuning: LoRA and QLoRA

LLM은 수십억 개의 파라미터를 가지고 있기 때문에, 모든 파라미터를 학습시키는 Full Fine-Tuning 방식은 막대한 GPU 메모리와 시간이 필요하다. 특히 제한된 환경에서는 학습이 더 어려운데 이를 해결하기 위해 등장한 기법이 이를 해결하기 위해 등장한 기법이 PEFT(Parameter-Efficient Fine-Tuning)이다.

PEFT는 전체 파라미터를 학습하지 않고, 일부 파라미터만 학습하도록 하여 메모리 사용량과 연산량을 크게 줄인다. 그 결과, 학습 데이터가 적어도 효과적인 성능 향상을 기대할 수 있다.

LoRA (Low-Rank Adaptation)

LoRA는 모델의 기존 파라미터를 그대로 두고, 작은 보조 파라미터만 추가로 학습하는 방법이다. 기존의 Weight 행렬을 직접 업데이트하지 않고, 작은 두 개의 행렬 A, B를 학습하여 모델을 조정한다.

동작 원리

일반적인 선형 변환:

LoRA 적용 후:

  • W: 기존 모델 파라미터(freeze)
  • A, B: 학습할 작은 행렬
  • r: 행렬의 랭크(rank), 매우 작게 설정 (low-rank)

이 방식을 사용하면 내부 구조 rank가 낮아, 학습해야 하는 파라미터 수가 매우 적어진다.

이해하기 쉽게 예를 들어 보면, 전체 파라미터가 1024 * 1024로 총 1,048,576개인 모델이 있다고 가정해보자. r을 8로 해서 LoRA를 적용하면, A와 B행렬 각각이 8,192개의 파라미터만 가지게 되고 두 개를 합쳐도 16,384개의 파라미터만 학습하면 된다. 이는 전체 파라미터의 약 1.6%에 불과하다. 100만개가 넘는 파라미터 전부를 학습하는 대신 극히 일부만 학습해도 전부를 학습한것과 비슷한 학습 효과를 볼 수 있다.

QLoRA (Quantized LoRA)

QLoRA는 LoRA에 양자화(Quantization) 를 적용한 방식이다. 기존의 float32 파라미터를 int4와 같이 작은 비트수로 압축하여 GPU 메모리를 크게 절약한다.

특징

  • 최대 4배 이상 GPU 메모리 사용량을 감소할 수 있음.
  • 모델 본체는 작아지지만, optimizer state는 여전히 저장해야 하므로 경우에 따라 더 많은 메모리가 필요할 수 있음.
  • optimizer state는 전부 GPU에 올리지 않고, 필요할 때만 GPU로 이동하여 학습 하고 사용하지 않을 때는 CPU 메모리에 올려두도록 해야함.

[Elasticsearch] Understanding the Difference Between Fielddata and Keyword Fields

Elasticsearch를 사용하다 보면 집계나 정렬을 위해 텍스트 데이터를 처리해야 할 때가 많다. 이때 자주 등장하는 개념이 fielddatakeyword가 있는데 이 둘은 비슷한 목적(정렬, 집계)을 위해 사용되지만, 내부 동작 방식과 성능, 메모리 사용 면에서는 큰 차이가 있다.

fielddata (text 필드)

  • 기본적으로 text 타입은 검색에 적합하도록 분석(analyzed)된 문자열이다
  • 정렬이나 집계를 하기 위해선 fielddata를 사용하여 해당 데이터를 JVM 힙 메모리에 적재 한다.
  • 매우 메모리 집약적이며, 특히 대규모 데이터셋에서는 성능에 부담을 줄 수 있다.
  • 처음 한 번은 데이터를 읽어서 메모리에 올려야 하는데, 이때 리소스 사용량이 많다.

keyword type

    • keyword 타입은 분석되지 않은 raw 문자열로, 주로 정렬, 집계, 필터링에 사용된다.
    • doc values라는 구조로 디스크에 저장된다. 이 값은 인덱싱 시점에 미리 계산(precomputed) 되어 저장되며, 검색 시에는 디스크에서 읽어온다.
    • JVM 힙 메모리를 사용하지 않고, 일반적으로 fielddata보다 성능과 안정성 측면에서 유리하다.

    [LLM] What is layer normalization?

    딥러닝 모델을 학습할 때, 정규화(Normalization)는 빠르고 안정적인 학습을 위한 핵심 기법 중 하나다. 특히 자연어 처리(NLP)에서 사용되는 트랜스포머 기반 LLM(Large Language Model)에서는 층 정규화(Layer Normalization)가 중요한 역할을 한다.

    정규화는 딥러닝 모델에서 입력 데이터가 일정한 분포(평균과 분산)를 갖도록 조정해주는 기법이다. 정규화를 통해 모델은 다음과 같은 이점을 얻을 수 있다

    • 학습이 더 안정적이고 빠르게 진행 됨.
    • 과적합(Overfitting)방지에 도움 됨.
    • DNN(Deep neural network) architecture에서도 정보 흐름이 원활 함.

    Batch Normalization VS Layer Normalization

    Batch Normalization

    배치 정규화는 입력 데이터를 배치 단위로 묶어서 평균과 분산을 계산하고 정규화한다. 주로 이미지 처리 분야에서 사용되며, 자연어 처리에서는 아래 이미지 처럼 문장마다 길이가 다르더라도 일정한 길이로 맞추기 위해 PAD 토큰이 삽입된다.

    배치 마다 구성데이터가 달라서 정규화 효과가 일정하지 않게 되서 LLM에서는 일반적으로 Bath Norm을 사용하지 않는다.

    Layer Normalization

    층 정규화는 각 샘플(토큰 임베딩)마다 독립적으로 정규화한다. 같은 문장 안의 각 단어 벡터를 기준으로 평균과 분산을 계산한다.

    토큰 임베딩별 정규화를 수행하기 때문에 문장의 길이에 영향을 받지 않고 정규화를 동일하게 적용할 수 있다.

    LLM과 같은 트랜스포머 기반 자연어 처리 모델에서는, 입력의 다양성과 길이의 불균형을 고려해 배치 정규화 대신 층 정규화를 사용한다. 이는 모델이 더 빠르고 안정적으로 학습될 수 있도록 돕고, 문맥 이해 능력을 향상시키는 중요한 기법이다.

    [LLM] Self-Attention and Multi-head Attention

    자연어처리(NLP)에서 트랜스포머 모델은 언어의 문맥과 의미를 깊이 이해할 수 있게 해주는 혁신적인 구조이다. 그 중심에는 어텐션 있으며, 특히 Self-AttentionMulti-Head Attention이 있다.

    Attention이란?

    Aattention은 말 그대로 어디에 주의를 기울일 것인가를 계산하는 것이다.
    쉽게 설명하면, 우리는 평범한 글을 읽을 때는 왼쪽에서 오른쪽으로 자연스럽게 읽어 내려가지만, 어려운 글을 읽을 때는 특정 단어나 문장에 집중하며 앞뒤 내용을 다시 확인하곤 한다. 이러한 집중과 상호참조의 과정을 딥러닝 모델에 반영하기 위한 연산 방식이 바로 어텐션이다.

    Query, Key, Value

    Attention은 Q(Query), K(Key), V(Value)라는 세 가지 개념을 사용한다.

    • Query – 찾고자 하는 정보, 즉 ‘검색어’ 역할을 한다.
    • Key – 데이터가 어떤 쿼리와 관련이 있는지를 판단할 수 있게 해주는 특징 값. 예를 들어 문서 검색에서는 키가 문서의 제목, 본문, 저자일 수 있다.
    • Value – 관련 있는 데이터를 실제로 가져오는 값. 키를 통해 필터링된 후, 관련도에 따라 가중합되어 출력으로 사용됨.

    Self-Attention

    입력된 문장의 각 단어가 문장 내의 다른 단어들과 얼마나 관련이 있는지 계산하는 방식입니다. 입력된 문장의 모든 토큰을 Q, K, V로 변환한 뒤, 아래 그림과 같은 순서로 연산이 진행된다.

    Self-Attention
    1. Matrix product – Query와 Key의 내적해서 관련도를 측정한다.
    2. Sacling – 안정적인 학습을 위해 임베딩 차원수로 나눈다.
    3. Mask(Option) – Decoder 에서 미래의 토큰을 마스킹 할 것인지 선택하고 마스킹 적용.
    4. Softmax – 각 토큰 간 관련도 점수를 확률로 변환.
    5. Output – Value와 내적해서 최종 출력 생성.

    Multi-head Attention

    하나의 어텐션만 사용하는 것보다 여러 개의 어텐션을 병렬로 사용하면 더 다양한 관계를 동시에 파악할 수 있다.

    단순히 하나의 Attention으로만 문장을 이해하면 놓치는 정보가 생길 수 있다.
    그래서 여러 개의 head를 만들어서 서로 다른 방식으로 문장을 분석한 후, 그걸 합쳐서 더 풍부하고 정확한 표현을 얻는 방식이다.

    Multi-head Attention
    1. 입력을 여러 개의 Query, Key, Value를 head 크기로 분리하고 선형 변환을 한다.
    2. 각각의 헤드에서 Scaled Dot-Product Attention을 수행.
    3. 각 Attention 결과를 연결한다.
    4. 최종 선형 변환을 거쳐 결과 출력 한다.

    셀프 어텐션과 멀티헤드 어텐션은 트랜스포머가 자연어의 의미를 효과적으로 이해하게 해주는 핵심 기술이다. 문장 내에서 중요한 단어들 간의 관계를 파악하고, 다양한 시각에서 이를 종합하는 능력을 부여한다.

    [LLM] Model Parallelism and Data Parallelism

    대규모 언어 모델(LLM, Large Language Model)의 학습은 수십억 개 이상의 파라미터를 다루기 때문에, 단일 GPU의 메모리나 연산 능력으로는 처리하기 어렵다. 이러한 문제를 해결하기 위해 병렬화(parallelism) 전략이 도입되며, 주로 두 가지 방식인 모델 병렬화(Model Parallelism)데이터 병렬화(Data Parallelism)가 사용된다.

    Model Parallelism

    모델 병렬화는 하나의 모델을 여러 GPU에 분산시켜 학습하는 방식입니다. 모델이 너무 커서 단일 GPU 메모리에 올릴 수 없을 때 사용된다. 대표적인 두 가지 방식이 있다.

    1. Pipeline Parallelism
      • 딥러닝 모델의 층(Layer) 단위로 모델을 분할하고, 각 분할을 다른 GPU에 할당한다.
      • 입력 데이터가 GPU를 순차적으로 통과하며 forward/backward 연산이 수행된다.
      • 모델 크기를 효과 적으로 분산 할 수 있지만, 각 GPU가 순차적으로 동작하게 되어서 파이프라인 버블 문제가 생길 수 있다.
      • 그림에서 머신을 1, 2와 3, 4로 나누면 파이프라인 병렬화이다.
    2. Tensor Parallelism
      • 하나의 층 내부에서 계산되는 텐서 연산을 여러 GPU에 나누어 수행한다.
      • 행렬 곱 연산에서 행 또는 열을 나눠 병렬 연산 후 결과를 합치는 연산을 하면서 수행된다.
      • 레이어 수준보다 더 미세하게 병렬화할 수 있어서 계산량을 세밀하게 분산이 가능하지만, GPU간 통신이 빈번하게 발생하게 되어 통신 오버헤드가 커질 수 있음.
      • 그림에서 머신을 1, 3과 2, 4로 나누면 텐서 병렬화이다.
    Model Parallelism

    Data Parallelism

    모델 크기가 하나의 GPU에 올라가기 적당하고 데이터가 많을 경우 사용할 수 있는 방식으로 데 모델을 그대로 복제한 후, 서로 다른 GPU에서 다른 데이터를 학습시키는 방식이다.

    그림 처럼 각 GPU는 동일한 모델을 가지고 있으며, 각기 다른 미니배치 데이터를 학습한다.

    Data Parallelism

    forward/backward 연산 후, 파라미터의 그레이디언트(gradient) 를 모든 GPU가 서로 동기화하여 업데이트한다.

    구현이 간단하고 대부분의 프레임워크에서 지원이 되지만, 모델 크기가 GPU 메모리에 들어가야 하고, 모든 GPU 간의 gradient 통신 비용이 크다.

    LLM 모델 학습은 단순히 모델을 만드는 것 이상으로 효율적인 리소스 분산이 핵심이다. 모델 크기, 하드웨어 환경, 통신 병목 등을 고려하여 파이프라인 병렬화, 텐서 병렬화, 데이터 병렬화를 적절히 조합여 사용해야 한다.

    [LLM] Strategies to Reduce GPU Memory Usage During LLM Training

    대규모 언어 모델(LLM)을 학습하거나 미세 조정할 때 가장 큰 제약 중 하나는 GPU 메모리 부족이다. 학습 과정에서 어떤 데이터가 메모리에 올라가는지, 이를 어떻게 줄일 수 있는지 정리해 보자.

    GPU 메모리에 저장되는 주요 데이터

    1. Model Parameters
      • 모델 가중치(Weight)와 편향(Bias) 등의 학습 가능한 값.
      • 예를 들어 7B (7 Billion) 파라미터 모델의 경우 FP32 기준 약 28GB 메모리가 필요함.
    2. Gradient
      • 각 파라미터에 대해 손실 함수의 기울기 값
      • 역전파(Back propagation) 과정에서 계산되며, 파라미터와 같은 크기의 메모리를 필요로 함.
    3. Optimizer State
      • Optimizer는 모델을 학습시키기 위해 파라미터를 업데이트 하는 알고리즘임. 이때 대부분의 고급 옵티마이저는 각 파라미터마다 추가적인 정보를 저장하는데 이 정보들이 Optimizer State임.
      • 예를 들어 Adam 옵티마이저는 각 파라미터에 대해 1st moment (이전 기울기의 평균), 2nd moment (기울기 제곱의 평균) 추적 값을 저장함.
      • 파라미터 크기의 2~3배에 달하는 추가 메모리 소모가 발생할 수 있음.
    4. Forward Activation
      • 순전파(Forward pass)에서 중간 레이어의 출력값.
      • 역전파 시 Gradient를 계산하기 위해 반드시 필요한 값.
      • 가장 많은 메모리를 차지할 수 있음.

    메모리 절약을 위한 전략

    1. Gradient Accumulation
      • GPU 메모리 한계를 넘지 않기 위해 작은 배치(batch) 크기로 여러 번 계산하고 그래디언트를 누적(accumulate).
      • 일정 스텝마다 누적된 그래디언트를 이용해 파라미터를 한 번 업데이트함.
      • Batch size 16에서 OOM이 발생할 경우
        -> Batch size 4 (gradient_accumulation_steps=4)로 설정.
        -> Loss를 4로 나누어 누적하면 batch size 16과 동일한 학습 효과를 얻을 수 있음.
        -> 실제 메모리 사용량은 Batch size 4로 계산 됨.
        -> 작은 Batch size로 큰 Batch size로 학습로 학습한 것과 동일한 효과를 얻을 수 있지만 누적 연산을 추가로 해야 해서 학습 시간은 증가됨.
    1. Checkpointing (Activation Checkpointing)
      • 순전파 시 전체 activation을 저장하지 않고, 마지막 출력값만 저장.
      • 역전파 시 필요한 이전 상태는 다시 계산하여 사용.
      • 순전파 상태 값을 모두 저장하지 않아서 메모리 사용량은 대폭 감소되지만, 역전파 시 순전파를 매번 반복 계산하므로 연산량 증가하여 학습 시간은 증가됨.
    2. Gradient Checkpointing
      • 순전파의 일부 중간 지점(checkpoint)만 저장하고 나머지는 역전파 시 재계산.
      • 전체를 저장하는 일반 방식과, 끝만 저장하는 기본 checkpointing의 중간 전략.
      • 메모리 효율과 연산 효율을 적절히 절충한 방식이라 약간의 오버헤드는 존재하지만 일반적인 Checkpointing 보다는 효율적임.

    GPU 메모리는 LLM 학습에서 가장 중요한 자원 중 하나이다. 위에서 소개한 전략들을 적절히 활용하면 제한된 자원 내에서도 효율적으로 모델을 학습시킬 수 있다.

    [LLM] Floating Point Formats

    AI 모델 학습과 추론에서 부동소수점(Floating Point) 형식은 성능과 정확도에 큰 영향을 준다. 특히, 연산 속도와 메모리 사용량을 줄이기 위해 다양한 정밀도의 부동소수점 형식이 사용되는데, 대표적으로 사용되는 BF16, FP32, FP16에 대해 간단히 정리해 본다.

    1. FP32 (Single Precision Floating Point)
      • Exponent – 8 Bit
      • Mantissa – 23 Bit
      • Sign – 1 Bit
      • Total 32 Bit
      • 높은 정밀도와 넓은 표현 범위를 모두 갖춘 표준 부동소수점 형식. 대부분의 AI 학습, 고정밀 연산이 필요한 경우 사용한다.
    2. FP16 (Half Precision Floating Point)
      • Exponent – 5 Bit
      • Mantissa – 10 Bit
      • Sign – 1 Bit
      • Total 16 Bit
      • 정밀도와 표현 범위 모두 FP32보다 줄어들지만, 메모리 절약과 연산 속도는 증가 시킬 수 있다.
    3. BF16 (bfloat16, Brain Floating Point Format)
      • Exponent – 8 Bit
      • Mantissa – 7 Bit
      • Sign – 1 Bit
      • Total 16 Bit
      • FP16과 마찬가지로 16 Bit를 사용하여 표현 범위가 줄어들었지만, FP32와 동일한 지수부를 가져서 표현 가능한 수의 범위가 넓다.

    [Elasticsearch] Collecting RSS information with Logstash

    RSS feed는 웹사이트의 최신 콘텐츠를 효율적으로 수집하고 분석할 수 있는 강력한 도구입니다. Logstash를 사용하면 RSS 데이터를 쉽게 수집하고 처리할 수 있습니다. 이 글에서는 Google News RSS를 Logstash로 수집하는 방법을 예제로 설명하겠습니다.

    Logstash config

    Logstash의 설정 파일은 input, filter, output 세 가지 섹션으로 구성됩니다. 아래는 Google News의 RSS 피드를 수집하기 위한 예제 설정입니다.

    Input

    http_poller 플러그인을 사용해 RSS 피드를 주기적으로 요청합니다.

    input {
      http_poller {
        urls => {
          google_news => {
            url => "https://news.google.com/rss"
            method => get
          }
        }
        request_timeout => 60
        schedule => { every => "10m" }
        codec => "plain"
        metadata_target => "http_metadata"
      }
    }
    

    urls: 수집할 RSS 피드 URL을 지정합니다.
    schedule: 데이터를 가져올 주기를 설정합니다. every => “5m”은 5분마다 데이터를 요청합니다. cron 표현식을 사용하여 보다 세부적인 설정이 가능합니다
    metadata_target: 요청 메타데이터를 저장할 필드를 지정합니다.

    Fiter

    수집된 RSS 데이터를 구조화된 형태로 변환합니다.

    filter {
      xml {
        source => "message"
        target => "rss"
        store_xml => true
        force_array => false
      }
      split {
        field => "[rss][channel][item]"
      }
      mutate {
        add_field => {
          "title" => "%{[rss][channel][item][title]}"
          "link" => "%{[rss][channel][item][link]}"
          "source" => "%{[rss][channel][item][source][content]}"
          "pubDate" => "%{[rss][channel][item][pubDate]}"
          "type" => "%{[http_metadata][input][http_poller][request][name]}"
        }
        remove_field => ["message", "rss", "event", "http_metadata"]
      }
    }
    

    xml: RSS 데이터를 XML 형태에서 파싱하여 rss 필드에 저장합니다.
    split: 각 기사(item)를 개별 이벤트로 분리합니다.
    mutate: 원하는 필드(title, link, source, pubDate)를 추가하고 불필요한 데이터를 제거합니다.

    Output

    결과 데이터를 Elasticsearch에 저장하거 콘솔에 출력합니다.

    output {
      elasticsearch {
        hosts => ["http://localhost:9200"]
        index => "rss-data-%{+YYYY.MM.dd}"
      }
      stdout { codec => rubydebug }  
    }
    

    이 예제를 사용하면 Logstash를 활용하면 RSS 데이터를 효율적으로 수집하고 처리할 수 있습니다.

    [Azure SQL]Using Always Encrypted with .NET (feat. Keyvault)(2)

    [Azure SQL]Using Always Encrypted with .NET (feat. Keyvault)(1)에서 SQL에 Always Encryted가를 설정하여 데이터를 암호화하고 쿼리하는 방법에 대해서 알아봤다. 이번 포스트에서는 Always Encryted가 설정된 SQL를 이용하여 .Net 서비스를 개발하는 방법에 대해서 알아보겠다.

    .Net 서비스 내에서 Always Encrypted가 설정된 SQL에 접근 하기 위해서는 Connection String에 아래 옵션이 설정 되어야 한다.

    • Encryption Setting=Enabled
    • Encrypt=True

    appsettings.json에서 ConnectionStrings에 다음과 같이 설정하면 된다.

    "ConnectionStrings": {
      "DefaultConnection": "Server=tcp:azure sql.database.windows.net,1433;Initial Catalog=DB;Persist Security Info=False;User ID={user id};Password={password};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;Column Encryption Setting=Enabled;"
    }
    

    여기까지 하면 db connection이 생성될 때 Always Encryed 설정이 활성화 된다. 하지만 CEK가 설정된 Table을 읽어 오지는 못한다. 제대로 값을 읽어 오기 위해서는 CEK Provider를 서비스 시작 시점에 설정을 해주어야 한다.

    CEK Provider는 Program.cs에 다음과 같이 Provider를 구성해주면 된다.

    var azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new DefaultAzureCredential());
    
    var dics = new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>();
    dics.Add(SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider);
    
    SqlConnection.RegisterColumnEncryptionKeyStoreProviders(dics);
    

    이제, 아래와 같이 간단히 앞서 암호화 했던 Users 테이블을 select해 보면 복호화 된 데이터를 확인 할 수 있다.

    await using var conn = (SqlConnection)_context.Database.GetDbConnection();
    
    await conn.OpenAsync();
    
    await using var cmd = new SqlCommand("select * from Users", conn);
    await using var reader = await cmd.ExecuteReaderAsync();
    
    await reader.ReadAsync();
    
    var result = $"{reader[0]}, {reader[1]}, {reader[2]}";
    //id, email, name, registered date
    //1, abc@gmail.com, abc, 2023-01-01 00:00.000