🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Amazon Titan Embeddings 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 18. · 4 Views

Amazon Titan Embeddings 완벽 가이드

AWS의 Titan Embeddings 모델을 활용하여 텍스트를 벡터로 변환하고, 시맨틱 검색과 추천 시스템을 구축하는 방법을 실무 중심으로 알아봅니다. 초급 개발자도 쉽게 따라할 수 있도록 단계별로 설명합니다.


목차

  1. Titan_Embeddings_모델_소개
  2. Embeddings_API_호출
  3. 단일_텍스트_임베딩
  4. 배치_임베딩_처리
  5. 임베딩_차원과_특징
  6. 다국어_임베딩_지원
  7. 고객의 언어에 맞는 답변을 보여줍니다

1. Titan Embeddings 모델 소개

어느 날 김개발 씨는 회사의 고객 문의 시스템을 개선하는 프로젝트를 맡게 되었습니다. "비슷한 문의를 자동으로 찾아주는 기능을 만들어 보세요." 팀장님의 요청이었습니다.

단순 키워드 검색으로는 한계가 있다는 것을 알고 있던 김개발 씨는 고민에 빠졌습니다.

Amazon Titan Embeddings는 텍스트를 숫자 벡터로 변환해주는 AWS의 AI 모델입니다. 마치 단어의 의미를 좌표로 표현하는 것처럼, 비슷한 의미를 가진 텍스트는 가까운 위치에 배치됩니다.

이를 통해 의미 기반 검색, 추천 시스템, 문서 분류 등 다양한 AI 애플리케이션을 구축할 수 있습니다.

다음 코드를 살펴봅시다.

import boto3
import json

# Bedrock Runtime 클라이언트 생성
bedrock = boto3.client(
    service_name='bedrock-runtime',
    region_name='us-east-1'
)

# 모델 ID 지정 - Titan Embeddings G1 사용
model_id = 'amazon.titan-embed-text-v1'

# 텍스트를 벡터로 변환
text = "고객 서비스 문의입니다"
response = bedrock.invoke_model(
    modelId=model_id,
    body=json.dumps({"inputText": text})
)

# 결과에서 임베딩 벡터 추출
result = json.loads(response['body'].read())
embedding = result['embedding']  # 1536차원의 벡터
print(f"벡터 차원: {len(embedding)}")

김개발 씨는 입사 6개월 차 백엔드 개발자입니다. 최근 회사의 고객 상담 시스템에 새로운 기능을 추가하는 임무를 받았습니다.

고객이 문의를 등록하면, 과거의 비슷한 문의와 답변을 자동으로 추천해 주는 기능이었습니다. 처음에는 간단해 보였습니다.

"그냥 키워드로 검색하면 되지 않나?" 하지만 실제로 구현해 보니 문제가 많았습니다. "배송이 늦어요"와 "언제 도착하나요"는 같은 의미지만, 키워드가 달라서 검색이 되지 않았습니다.

임베딩이란 무엇일까요? 선배 개발자 박시니어 씨가 다가와 조언을 해주었습니다. "임베딩을 사용해 보는 건 어때요?

AWS에 Titan Embeddings라는 좋은 모델이 있어요." 임베딩이란 쉽게 말해서 텍스트를 숫자로 바꾸는 것입니다. 하지만 단순한 변환이 아닙니다.

마치 지도 위에 도시를 표시하는 것처럼, 의미가 비슷한 단어들을 가까운 위치에 배치합니다. 예를 들어 볼까요?

"개"와 "고양이"는 모두 동물이므로 가까운 위치에 놓입니다. 반면 "개"와 "컴퓨터"는 전혀 관련이 없으므로 멀리 떨어진 위치에 놓입니다.

이렇게 의미를 숫자 공간에 표현하는 것이 바로 임베딩입니다. 왜 Titan Embeddings를 사용할까요? 임베딩 모델은 여러 종류가 있습니다.

OpenAI의 임베딩 모델도 있고, 오픈소스 모델도 많습니다. 그렇다면 왜 Titan Embeddings를 선택할까요?

첫 번째 이유는 AWS 생태계와의 완벽한 통합입니다. 이미 AWS를 사용하고 있다면, 별도의 API 키 발급이나 결제 설정 없이 바로 사용할 수 있습니다.

IAM 권한만 설정하면 됩니다. 두 번째는 높은 품질입니다.

Titan Embeddings는 1536차원의 벡터를 생성하며, 25개 이상의 언어를 지원합니다. 영어는 물론이고 한국어도 잘 처리합니다.

세 번째는 합리적인 가격입니다. 텍스트 1000개당 약 $0.0001 정도로, 대규모 서비스에서도 부담 없이 사용할 수 있습니다.

어떻게 작동할까요? 위의 코드를 단계별로 살펴보겠습니다. 먼저 boto3 라이브러리로 AWS Bedrock 서비스에 접속합니다.

Bedrock은 AWS의 생성형 AI 서비스 플랫폼이며, Titan Embeddings는 이 플랫폼에서 제공하는 모델 중 하나입니다. 다음으로 모델 ID를 지정합니다.

amazon.titan-embed-text-v1은 현재 가장 많이 사용되는 Titan Embeddings 모델입니다. 앞으로 v2, v3 같은 더 강력한 버전이 나올 수도 있습니다.

그리고 invoke_model 메서드를 호출합니다. 여기에 변환하고 싶은 텍스트를 전달하면, 몇 초 안에 결과가 돌아옵니다.

결과는 1536개의 숫자로 이루어진 벡터입니다. 실무에서는 어떻게 활용할까요? 김개발 씨는 이 기능을 고객 문의 시스템에 적용하기로 했습니다.

구조는 이렇습니다. 먼저 과거의 모든 고객 문의를 Titan Embeddings로 변환하여 벡터 데이터베이스에 저장합니다.

새로운 문의가 들어오면, 그 문의도 벡터로 변환합니다. 그리고 벡터 간의 거리를 계산하여 가장 가까운 문의들을 찾아냅니다.

이렇게 하면 "배송이 늦어요"와 "언제 도착하나요"가 키워드는 다르지만, 의미가 비슷하기 때문에 같은 카테고리로 분류됩니다. 고객은 더 빠르게 답변을 받을 수 있고, 상담사의 업무 부담도 줄어듭니다.

주의할 점은 무엇일까요? 초보 개발자들이 자주 하는 실수가 있습니다. 임베딩을 일반 데이터베이스에 저장하려고 하는 것입니다.

1536개의 숫자로 이루어진 벡터를 MySQL이나 PostgreSQL의 일반 컬럼에 저장하면, 검색이 매우 느려집니다. 벡터 검색에는 특별한 인덱스가 필요하기 때문입니다.

따라서 Pinecone, Weaviate, OpenSearch 같은 벡터 데이터베이스를 사용하거나, PostgreSQL의 pgvector 확장을 활용해야 합니다. 이런 도구들은 벡터 검색에 최적화되어 있어서, 수백만 개의 벡터에서도 빠르게 검색할 수 있습니다.

처음 시작하는 분들을 위한 조언 박시니어 씨는 김개발 씨에게 이렇게 말했습니다. "처음에는 작은 데이터셋으로 테스트해 보세요.

100개 정도의 문장을 임베딩하고, 비슷한 문장끼리 잘 묶이는지 확인해 보는 거죠." 실제로 김개발 씨는 회사의 FAQ 50개로 먼저 실험했습니다. 결과는 놀라웠습니다.

"반품하고 싶어요"라는 새로운 질문이 들어왔을 때, "환불 절차가 궁금합니다"라는 기존 FAQ가 가장 유사한 항목으로 검색되었습니다. 정리하며 Titan Embeddings는 텍스트의 의미를 숫자로 표현하는 강력한 도구입니다.

검색 엔진을 만들 때, 추천 시스템을 구축할 때, 문서를 분류할 때 등 다양한 상황에서 활용할 수 있습니다. 여러분도 오늘 배운 내용을 토대로 간단한 예제부터 시작해 보세요.

AWS 계정만 있다면 누구나 바로 사용할 수 있습니다.

실전 팁

💡 - AWS 리전은 us-east-1을 사용하는 것이 가장 안정적입니다

  • 처음에는 작은 데이터셋(100개 이하)으로 테스트하여 동작을 확인하세요
  • 벡터는 반드시 벡터 데이터베이스나 pgvector에 저장하세요

2. Embeddings API 호출

김개발 씨는 Titan Embeddings의 개념을 이해했지만, 실제로 API를 호출하는 방법이 궁금했습니다. "boto3는 처음 써보는데, 설정이 복잡하지 않을까?" 박시니어 씨가 옆에서 말했습니다.

"생각보다 간단해요. AWS 인증만 제대로 되어 있으면 됩니다."

Bedrock Runtime API는 AWS에서 생성형 AI 모델을 호출하는 인터페이스입니다. boto3 라이브러리를 사용하여 Python에서 쉽게 접근할 수 있으며, IAM 권한만 제대로 설정하면 별도의 API 키 없이 안전하게 사용할 수 있습니다.

invoke_model 메서드 하나로 모든 작업이 이루어집니다.

다음 코드를 살펴봅시다.

import boto3
import json
from botocore.exceptions import ClientError

# AWS 자격 증명 설정 (환경 변수나 IAM Role 사용)
bedrock = boto3.client(
    service_name='bedrock-runtime',
    region_name='us-east-1',
    # aws_access_key_id='YOUR_KEY',  # 환경 변수 사용 권장
    # aws_secret_access_key='YOUR_SECRET'
)

try:
    # 요청 본문 구성
    request_body = {
        "inputText": "AWS Bedrock으로 AI 애플리케이션 개발하기"
    }

    # API 호출
    response = bedrock.invoke_model(
        modelId='amazon.titan-embed-text-v1',
        contentType='application/json',
        accept='application/json',
        body=json.dumps(request_body)
    )

    # 응답 파싱
    response_body = json.loads(response['body'].read())
    embedding = response_body['embedding']

    print(f"성공! 벡터 길이: {len(embedding)}")

except ClientError as e:
    print(f"오류 발생: {e}")

김개발 씨는 이제 실제로 코드를 작성해 볼 차례입니다. 하지만 새로운 AWS 서비스를 사용하는 것은 항상 긴장되는 일입니다.

"인증은 어떻게 하지? 요금은 얼마나 나올까?

잘못 호출하면 어떻게 되지?" 이런 걱정들이 머릿속을 스쳐 지나갔습니다. AWS 인증 방식 이해하기 박시니어 씨가 먼저 인증 방식을 설명해 주었습니다.

"AWS는 API 키를 직접 코드에 넣는 방식을 권장하지 않아요. 대신 IAM 역할이나 환경 변수를 사용하죠." AWS 인증은 크게 세 가지 방법이 있습니다.

첫 번째는 IAM 역할입니다. EC2나 Lambda에서 코드를 실행한다면, 해당 서비스에 IAM 역할을 부여하는 것이 가장 안전합니다.

코드에 자격 증명을 전혀 넣지 않아도 자동으로 인증됩니다. 두 번째는 환경 변수입니다.

로컬 개발 환경에서는 AWS CLI로 자격 증명을 설정하면, boto3가 자동으로 읽어옵니다. aws configure 명령어로 한 번만 설정하면 됩니다.

세 번째는 직접 입력입니다. 하지만 이 방법은 보안상 권장하지 않습니다.

코드가 GitHub에 올라가면 자격 증명이 노출될 수 있기 때문입니다. boto3 클라이언트 생성하기 위의 코드에서 가장 먼저 하는 일은 bedrock-runtime 클라이언트를 생성하는 것입니다.

여기서 주의할 점이 있습니다. 단순히 'bedrock'이 아니라 'bedrock-runtime'이어야 합니다.

왜 그럴까요? AWS Bedrock은 두 가지 API를 제공합니다.

하나는 모델을 관리하는 **제어 평면(Control Plane)**이고, 다른 하나는 실제로 모델을 실행하는 **데이터 평면(Data Plane)**입니다. 우리가 사용하는 것은 데이터 평면이므로 'bedrock-runtime'을 지정해야 합니다.

리전은 'us-east-1'을 사용하는 것이 좋습니다. Titan Embeddings는 모든 리전에서 제공되는 것이 아니며, us-east-1이 가장 먼저 새로운 모델을 지원하고 가격도 합리적입니다.

요청 본문 구성하기 Bedrock API는 JSON 형식으로 데이터를 주고받습니다. Titan Embeddings의 경우, 요청 본문은 매우 간단합니다.

inputText 필드에 변환하고 싶은 텍스트를 넣기만 하면 됩니다. 텍스트의 길이는 최대 8192 토큰까지 가능합니다.

토큰이란 단어보다 작은 단위로, 대략 한글 기준으로 4-5글자 정도가 1토큰입니다. 따라서 약 3만 글자 정도의 긴 문서도 한 번에 처리할 수 있습니다.

API 호출과 응답 처리 invoke_model 메서드는 동기식으로 작동합니다. 호출하면 몇 초 안에 결과가 돌아오며, 그동안 코드는 대기 상태가 됩니다.

응답은 바이너리 스트림 형태로 오기 때문에, response['body'].read()로 읽은 후 JSON으로 파싱해야 합니다. 최종적으로 얻게 되는 embedding 필드가 바로 우리가 원하는 1536차원 벡터입니다.

에러 처리는 필수입니다 실무에서는 항상 예외 처리를 해야 합니다. 네트워크 오류가 날 수도 있고, IAM 권한이 없을 수도 있으며, 서비스 할당량을 초과할 수도 있습니다.

김개발 씨는 처음에 예외 처리를 하지 않았다가 큰 낭패를 봤습니다. 테스트 중에는 잘 작동하다가, 실제 배포 후에 IAM 권한 오류가 발생했는데, 에러 메시지를 확인할 방법이 없었던 것입니다.

ClientError 예외를 잡아서 오류 메시지를 로깅하는 것만으로도 문제 해결 시간을 크게 단축할 수 있습니다. 실무에서의 활용 패턴 박시니어 씨는 실무 팁을 하나 더 알려주었습니다.

"API 호출을 직접 하는 것보다, 래퍼 함수를 만드는 게 좋아요." 예를 들어 get_embedding(text) 같은 함수를 만들어서, 내부적으로 boto3를 호출하고 에러 처리와 재시도 로직을 담는 것입니다. 이렇게 하면 코드가 깔끔해지고, 나중에 모델을 변경하거나 다른 서비스로 바꾸기도 쉬워집니다.

비용은 얼마나 나올까요? 김개발 씨가 가장 걱정했던 부분은 비용이었습니다. "혹시 실수로 수천 번 호출하면 어떡하지?" 다행히 Titan Embeddings는 매우 저렴합니다.

텍스트 1000개를 처리하는 데 약 0.01원 정도입니다. 하루에 100만 개의 텍스트를 처리해도 10원 정도밖에 나오지 않습니다.

물론 AWS는 사용량 기반 과금이므로, 프로덕션에서는 CloudWatch로 사용량을 모니터링하는 것이 좋습니다. 예상치 못한 버그로 무한 루프가 돌면 비용이 증가할 수 있습니다.

정리하며 김개발 씨는 이제 자신 있게 Bedrock API를 호출할 수 있게 되었습니다. IAM 권한을 설정하고, boto3 클라이언트를 생성하고, invoke_model로 호출하는 간단한 세 단계만 기억하면 됩니다.

여러분도 오늘 배운 내용을 토대로 직접 API를 호출해 보세요. AWS 프리 티어에서도 충분히 테스트할 수 있습니다.

실전 팁

💡 - IAM 역할에 'bedrock:InvokeModel' 권한을 반드시 추가하세요

  • us-east-1 리전을 사용하면 안정성과 가격 면에서 유리합니다
  • 에러 처리를 반드시 포함하여 운영 환경에서 문제를 빠르게 파악하세요

3. 단일 텍스트 임베딩

API 호출 방법을 익힌 김개발 씨는 본격적으로 문장을 벡터로 변환하는 작업을 시작했습니다. "이 문장이 정말 의미 있는 벡터로 변환될까?" 첫 번째 테스트 결과를 보는 순간, 김개발 씨는 놀라움을 감추지 못했습니다.

단일 텍스트 임베딩은 하나의 문장이나 문단을 1536차원의 벡터로 변환하는 과정입니다. 이 벡터는 텍스트의 의미를 수치화한 것으로, 코사인 유사도를 계산하여 다른 텍스트와의 유사성을 측정할 수 있습니다.

검색, 분류, 클러스터링 등 다양한 NLP 작업의 기초가 됩니다.

다음 코드를 살펴봅시다.

import boto3
import json
import numpy as np

bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')

def get_embedding(text):
    """텍스트를 벡터로 변환하는 함수"""
    response = bedrock.invoke_model(
        modelId='amazon.titan-embed-text-v1',
        body=json.dumps({"inputText": text})
    )
    result = json.loads(response['body'].read())
    return np.array(result['embedding'])

# 두 문장의 유사도 계산
text1 = "강아지가 공원에서 뛰어놀고 있습니다"
text2 = "개가 야외에서 놀고 있어요"
text3 = "프로그래밍은 재미있습니다"

embedding1 = get_embedding(text1)
embedding2 = get_embedding(text2)
embedding3 = get_embedding(text3)

# 코사인 유사도 계산
def cosine_similarity(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

print(f"문장1 vs 문장2: {cosine_similarity(embedding1, embedding2):.4f}")  # 높은 유사도
print(f"문장1 vs 문장3: {cosine_similarity(embedding1, embedding3):.4f}")  # 낮은 유사도

김개발 씨는 드디어 실전 테스트를 할 준비가 되었습니다. 간단한 문장 몇 개로 임베딩이 실제로 의미를 잘 포착하는지 확인해 보기로 했습니다.

첫 번째 실험 김개발 씨는 세 개의 문장을 준비했습니다. "강아지가 공원에서 뛰어놀고 있습니다", "개가 야외에서 놀고 있어요", 그리고 "프로그래밍은 재미있습니다"입니다.

첫 두 문장은 표현은 다르지만 의미가 비슷합니다. 세 번째 문장은 완전히 다른 주제입니다.

과연 임베딩이 이 차이를 구분할 수 있을까요? 벡터로 변환하기 위의 코드에서 get_embedding 함수를 만들었습니다.

이 함수는 텍스트를 받아서 1536개의 숫자로 이루어진 벡터를 반환합니다. 여기서 numpy 배열로 변환하는 이유는 나중에 벡터 연산을 쉽게 하기 위함입니다.

코사인 유사도를 계산하거나, 벡터 간 거리를 구하거나, 정규화를 하는 등의 작업이 numpy로 훨씬 간편합니다. 각 문장을 임베딩하는 데는 약 0.5초에서 1초 정도 걸립니다.

네트워크 상태에 따라 조금씩 달라질 수 있지만, 일반적으로 매우 빠른 편입니다. 코사인 유사도란 무엇일까요? 벡터를 얻었으니, 이제 유사도를 계산할 차례입니다.

가장 많이 사용하는 방법이 코사인 유사도입니다. 코사인 유사도는 두 벡터 사이의 각도를 측정합니다.

마치 손가락 두 개를 펼쳤을 때, 각도가 작으면 방향이 비슷하고, 각도가 크면 방향이 다른 것과 같습니다. 값은 -1에서 1 사이이며, 1에 가까울수록 매우 유사하고, 0에 가까우면 관련이 없으며, -1에 가까우면 정반대의 의미입니다.

실제로는 대부분 0과 1 사이의 값이 나옵니다. 놀라운 결과 김개발 씨가 코드를 실행했을 때, 결과는 기대 이상이었습니다.

문장1과 문장2의 유사도는 약 0.87이 나왔습니다. "강아지"와 "개"가 같은 의미이고, "공원"과 "야외"도 비슷하며, "뛰어놀고"와 "놀고"도 유사하다는 것을 모델이 정확히 이해한 것입니다.

반면 문장1과 문장3의 유사도는 약 0.12였습니다. 개와 프로그래밍은 전혀 관련이 없으므로 당연한 결과입니다.

실무에서의 활용 박시니어 씨가 김개발 씨의 화면을 보더니 고개를 끄덕였습니다. "이제 이걸 고객 문의 시스템에 적용하면 됩니다." 실제 적용 방법은 이렇습니다.

먼저 데이터베이스에 있는 모든 과거 문의를 임베딩으로 변환하여 저장합니다. 새로운 문의가 들어오면, 그 문의도 임베딩으로 변환합니다.

그리고 코사인 유사도를 계산하여 가장 유사한 상위 5개 문의를 찾아냅니다. 이렇게 하면 상담사는 과거의 유사한 사례를 즉시 참고할 수 있고, 고객은 더 빠르고 정확한 답변을 받을 수 있습니다.

주의해야 할 점 하지만 모든 것이 완벽한 것은 아닙니다. 김개발 씨는 몇 가지 함정을 발견했습니다.

첫째, 문장이 너무 짧으면 정확도가 떨어집니다. "좋아요"나 "감사합니다" 같은 한 단어는 맥락이 부족하여 의미를 제대로 포착하기 어렵습니다.

둘째, 도메인 특화 용어는 때때로 잘못 이해됩니다. 예를 들어 회사 내부에서만 사용하는 약어나 전문 용어는 일반적인 임베딩 모델이 학습하지 못했을 수 있습니다.

셋째, 언어가 섞이면 문제가 생길 수 있습니다. 한 문장 안에 한국어와 영어가 섞여 있으면, 의미 파악이 부정확해질 수 있습니다.

최적의 텍스트 길이는? 박시니어 씨는 경험을 바탕으로 조언했습니다. "너무 짧지도, 너무 길지도 않게 하세요.

한 문단 정도가 적당합니다." 실험 결과, 50단어에서 200단어 사이가 가장 좋은 성능을 보였습니다. 한국어로는 약 100자에서 400자 정도입니다.

이 정도 길이면 충분한 맥락이 있으면서도, 주제가 분산되지 않습니다. 벡터를 어디에 저장할까요? 임베딩 벡터를 얻었다면, 이제 저장해야 합니다.

1536개의 숫자를 일반 데이터베이스 텍스트 컬럼에 저장할 수도 있지만, 검색 성능이 매우 느립니다. Pinecone 같은 벡터 전용 데이터베이스를 사용하거나, PostgreSQL에 pgvector 확장을 설치하는 것이 좋습니다.

이런 도구들은 벡터 검색에 최적화된 인덱스를 제공하여, 수백만 개의 벡터에서도 밀리초 단위로 검색할 수 있습니다. 정리하며 김개발 씨는 단일 텍스트 임베딩의 기본을 완전히 이해했습니다.

텍스트를 벡터로 변환하고, 코사인 유사도로 유사성을 측정하는 것이 생각보다 간단하다는 것을 알게 되었습니다. 이제 여러분도 직접 실험해 보세요.

좋아하는 문장 몇 개를 임베딩하고, 유사도를 계산해 보면 임베딩의 힘을 직접 체험할 수 있습니다.

실전 팁

💡 - 텍스트는 50-200 단어 길이가 가장 좋은 성능을 보입니다

  • numpy를 사용하면 벡터 연산이 훨씬 편리합니다
  • 코사인 유사도 0.8 이상이면 매우 유사한 것으로 판단할 수 있습니다

4. 배치 임베딩 처리

단일 텍스트 처리는 성공했지만, 김개발 씨에게는 새로운 고민이 생겼습니다. 데이터베이스에 고객 문의가 10만 건이나 있었던 것입니다.

"하나씩 처리하면 며칠이 걸리겠는데..." 박시니어 씨가 웃으며 말했습니다. "배치 처리를 사용하면 훨씬 빠릅니다."

배치 임베딩 처리는 여러 개의 텍스트를 효율적으로 처리하는 기법입니다. API 호출을 최소화하고, 병렬 처리와 재시도 로직을 적용하여 대량의 데이터를 안정적으로 변환할 수 있습니다.

속도 제한과 에러 처리를 고려한 설계가 중요합니다.

다음 코드를 살펴봅시다.

import boto3
import json
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Dict

bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')

def get_embedding_with_retry(text: str, max_retries: int = 3) -> List[float]:
    """재시도 로직이 포함된 임베딩 함수"""
    for attempt in range(max_retries):
        try:
            response = bedrock.invoke_model(
                modelId='amazon.titan-embed-text-v1',
                body=json.dumps({"inputText": text})
            )
            result = json.loads(response['body'].read())
            return result['embedding']
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)  # 지수 백오프
    return None

def batch_embed(texts: List[str], max_workers: int = 5) -> Dict[int, List[float]]:
    """여러 텍스트를 병렬로 임베딩"""
    results = {}

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        # 각 텍스트에 대해 작업 제출
        future_to_idx = {
            executor.submit(get_embedding_with_retry, text): idx
            for idx, text in enumerate(texts)
        }

        # 완료된 작업부터 처리
        for future in as_completed(future_to_idx):
            idx = future_to_idx[future]
            try:
                embedding = future.result()
                results[idx] = embedding
                print(f"완료: {idx + 1}/{len(texts)}")
            except Exception as e:
                print(f"실패 {idx}: {e}")

    return results

# 사용 예시
texts = [
    "배송이 언제 도착하나요?",
    "반품하고 싶습니다",
    "제품이 불량입니다",
    "환불 절차가 궁금해요",
    "주문을 취소하고 싶어요"
]

embeddings = batch_embed(texts, max_workers=3)
print(f"총 {len(embeddings)}개 처리 완료")

김개발 씨는 10만 건의 데이터를 처리해야 한다는 사실에 막막함을 느꼈습니다. 단순 계산으로 하나당 1초씩 걸린다면, 10만 초는 약 28시간입니다.

하루가 넘게 기다려야 한다는 뜻이었습니다. 왜 배치 처리가 필요할까요? 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다.

"API 호출의 대부분 시간은 네트워크 왕복에 소요됩니다. 실제 모델 추론은 매우 빠르죠." 하나씩 순차적으로 처리하면, 요청을 보내고 응답을 기다리는 동안 CPU는 놀고 있습니다.

하지만 병렬 처리를 하면, 여러 요청을 동시에 보내고 각각의 응답을 받을 수 있습니다. 마치 식당에서 주문하는 것과 같습니다.

한 명씩 차례로 주문하면 시간이 오래 걸리지만, 여러 명이 동시에 주문하면 훨씬 빠릅니다. ThreadPoolExecutor로 병렬 처리하기 Python에서 병렬 처리를 하는 가장 간단한 방법은 ThreadPoolExecutor를 사용하는 것입니다.

이것은 여러 개의 작업을 동시에 실행하는 스레드 풀을 만들어 줍니다. 위의 코드에서 max_workers=5로 설정했습니다.

이는 동시에 최대 5개의 API 호출을 할 수 있다는 의미입니다. 너무 많이 설정하면 AWS의 속도 제한에 걸릴 수 있고, 너무 적게 설정하면 속도가 느려집니다.

실험 결과, 3-5개가 가장 적절한 것으로 나타났습니다. 이 설정으로 10만 건을 처리하는 시간을 약 6시간으로 줄일 수 있었습니다.

재시도 로직이 왜 중요할까요? 김개발 씨는 처음에 재시도 없이 구현했다가 문제를 겪었습니다. 10만 건을 처리하는 도중에 몇 건이 네트워크 오류로 실패했는데, 어떤 것이 실패했는지 추적하기 어려웠습니다.

get_embedding_with_retry 함수는 실패하면 자동으로 재시도합니다. 첫 번째 실패 후 2초, 두 번째 실패 후 4초를 기다립니다.

이를 **지수 백오프(Exponential Backoff)**라고 합니다. 이 방법은 일시적인 네트워크 문제나 서비스 과부하 상황에서 매우 효과적입니다.

즉시 재시도하면 같은 오류가 반복될 수 있지만, 조금 기다렸다가 재시도하면 성공할 확률이 높아집니다. 진행 상황 추적하기 10만 건을 처리할 때, 진행 상황을 알 수 없으면 불안합니다.

프로그램이 멈춘 것인지, 느린 것인지 구분이 안 되기 때문입니다. 위의 코드에서는 as_completed를 사용하여, 작업이 완료될 때마다 즉시 처리하고 진행 상황을 출력합니다.

"완료: 1523/100000" 같은 메시지를 보면, 얼마나 남았는지 알 수 있어서 안심이 됩니다. 더 나아가서 tqdm 라이브러리를 사용하면 예쁜 진행 바를 만들 수도 있습니다.

김개발 씨는 나중에 이것도 추가했습니다. AWS 속도 제한 이해하기 AWS Bedrock에는 **속도 제한(Rate Limit)**이 있습니다.

기본적으로 분당 수천 개의 요청을 처리할 수 있지만, 무제한은 아닙니다. 만약 속도 제한에 걸리면, ThrottlingException 오류가 발생합니다.

이 경우 재시도 로직이 자동으로 작동하여, 잠시 대기 후 다시 시도합니다. 속도 제한은 계정별로 설정되며, AWS 지원팀에 요청하면 늘릴 수 있습니다.

대규모 서비스를 운영한다면, 미리 한도 증가를 요청하는 것이 좋습니다. 실무에서의 배치 처리 패턴 박시니어 씨는 자신의 경험을 공유했습니다.

"큰 작업은 항상 청크로 나눠서 처리하세요." 10만 건을 한 번에 처리하지 말고, 1000건씩 100번 처리하는 것이 더 안전합니다. 각 청크를 처리할 때마다 중간 결과를 데이터베이스에 저장하면, 중간에 문제가 생겨도 처음부터 다시 시작할 필요가 없습니다.

또한 큐 시스템을 사용하는 것도 좋은 방법입니다. SQS에 처리할 텍스트를 넣고, Lambda가 하나씩 꺼내서 처리하는 방식입니다.

이렇게 하면 시스템이 더 견고해지고, 확장도 쉬워집니다. 비용 최적화 배치 처리를 할 때는 비용도 고려해야 합니다.

10만 건을 처리하면 약 1달러 정도의 비용이 발생합니다. 적은 금액이지만, 매일 반복한다면 한 달에 30달러가 됩니다.

불필요한 재처리를 피하기 위해, 이미 임베딩이 있는 텍스트는 건너뛰는 로직을 추가하는 것이 좋습니다. 데이터베이스에 embedding 컬럼이 NULL인 것만 처리하면 됩니다.

에러 로깅과 모니터링 김개발 씨는 처음에 에러를 콘솔에만 출력했다가, 나중에 로그 파일을 확인할 수 없어서 곤란했습니다. 실무에서는 반드시 로깅을 해야 합니다.

Python의 logging 모듈을 사용하거나, AWS CloudWatch Logs에 로그를 전송하면 나중에 문제를 분석하기 쉽습니다. 특히 어떤 텍스트가 실패했는지 기록해 두면, 재처리할 때 유용합니다.

정리하며 김개발 씨는 배치 처리 시스템을 완성했습니다. 10만 건의 데이터를 안정적으로 처리할 수 있게 되었고, 진행 상황도 추적할 수 있게 되었습니다.

여러분도 대량의 데이터를 처리해야 한다면, 병렬 처리와 재시도 로직을 반드시 구현하세요. 처음에는 복잡해 보이지만, 한 번 만들어 두면 계속 재사용할 수 있습니다.

실전 팁

💡 - max_workers는 3-5 정도가 적당하며, AWS 속도 제한을 고려해야 합니다

  • 대량 처리는 1000건씩 청크로 나눠서 중간 결과를 저장하세요
  • 재시도 로직에 지수 백오프를 적용하면 안정성이 크게 향상됩니다

5. 임베딩 차원과 특징

배치 처리 시스템을 완성한 김개발 씨는 문득 궁금해졌습니다. "왜 1536차원일까?

더 많으면 더 정확하지 않을까?" 박시니어 씨가 고개를 저었습니다. "차원의 수는 단순히 많다고 좋은 게 아니에요.

적절한 균형이 중요합니다."

임베딩 차원은 벡터의 크기를 의미하며, Titan Embeddings는 1536차원을 사용합니다. 이는 의미를 표현하기에 충분하면서도 계산 효율을 유지하는 최적의 크기입니다.

각 차원은 텍스트의 서로 다른 의미적 특징을 포착하며, 정규화된 벡터로 제공됩니다.

다음 코드를 살펴봅시다.

import boto3
import json
import numpy as np
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')

def get_embedding(text):
    response = bedrock.invoke_model(
        modelId='amazon.titan-embed-text-v1',
        body=json.dumps({"inputText": text})
    )
    result = json.loads(response['body'].read())
    return np.array(result['embedding'])

# 다양한 주제의 텍스트 임베딩
texts = [
    "강아지가 공원에서 놀고 있다",
    "고양이가 집에서 자고 있다",
    "Python으로 웹 개발하기",
    "JavaScript 프레임워크 비교",
    "저녁에 파스타를 먹었다",
    "아침에 빵을 구웠다"
]

embeddings = [get_embedding(text) for text in texts]

# 벡터의 특성 분석
embedding = embeddings[0]
print(f"차원: {len(embedding)}")
print(f"L2 노름: {np.linalg.norm(embedding):.4f}")  # 정규화 확인
print(f"평균: {np.mean(embedding):.6f}")
print(f"표준편차: {np.std(embedding):.4f}")

# PCA로 2차원으로 축소하여 시각화
pca = PCA(n_components=2)
embeddings_2d = pca.fit_transform(embeddings)

plt.figure(figsize=(10, 8))
plt.scatter(embeddings_2d[:, 0], embeddings_2d[:, 1])
for i, text in enumerate(texts):
    plt.annotate(text[:10], (embeddings_2d[i, 0], embeddings_2d[i, 1]))
plt.title("Titan Embeddings 시각화 (PCA 2D)")
plt.savefig("embeddings_visualization.png")

김개발 씨는 임베딩을 사용하면서 벡터의 내부 구조가 궁금해졌습니다. "1536개의 숫자가 각각 무엇을 의미하는 걸까?

그리고 왜 하필 1536일까?" 차원의 의미 박시니어 씨가 설명을 시작했습니다. "각 차원은 텍스트의 서로 다른 의미적 특징을 나타냅니다." 예를 들어, 어떤 차원은 '동물'과 관련된 특징을 포착하고, 다른 차원은 '행동'과 관련된 특징을 포착합니다.

또 다른 차원은 '긍정/부정'의 감정을 표현할 수도 있습니다. 물론 실제로는 이렇게 명확하게 분리되지 않습니다.

하나의 차원이 여러 의미를 동시에 담고 있는 경우가 많습니다. 이를 **분산 표현(Distributed Representation)**이라고 합니다.

마치 요리의 맛이 여러 재료의 조합으로 만들어지는 것처럼, 텍스트의 의미도 1536개 차원의 조합으로 표현됩니다. 왜 1536차원일까요? 김개발 씨가 질문했습니다.

"더 많은 차원을 사용하면 더 정확하지 않을까요?" 박시니어 씨가 고개를 저었습니다. "이론적으로는 그렇지만, 실무에서는 trade-off가 있습니다." 차원이 많을수록 장점: - 더 복잡한 의미를 표현할 수 있습니다 - 비슷한 텍스트를 더 세밀하게 구분할 수 있습니다 차원이 많을수록 단점: - 저장 공간이 많이 필요합니다 (1536개 float vs 3072개 float) - 검색 속도가 느려집니다 - **차원의 저주(Curse of Dimensionality)**로 인해 오히려 성능이 떨어질 수 있습니다 1536차원은 이런 trade-off를 고려한 최적의 크기입니다.

OpenAI의 임베딩 모델도 같은 크기를 사용하며, 실험적으로 검증된 값입니다. 벡터는 정규화되어 있습니다 위의 코드에서 **L2 노름(norm)**을 계산하면 약 1.0이 나옵니다.

이는 벡터가 **정규화(normalized)**되어 있다는 의미입니다. 정규화란 벡터의 길이를 1로 만드는 것입니다.

마치 모든 화살표의 길이를 똑같이 만들어서, 방향만 비교하는 것과 같습니다. 이렇게 하면 코사인 유사도 계산이 단순한 내적으로 바뀝니다.

정규화되지 않은 벡터는 길이와 방향을 모두 나눠야 하지만, 정규화된 벡터는 방향만 비교하면 되므로 계산이 훨씬 빠릅니다. 벡터의 분포 특성 김개발 씨가 통계값을 출력해 보니, 평균은 거의 0에 가깝고 표준편차는 약 0.02 정도였습니다.

이는 벡터의 값들이 0을 중심으로 고르게 분포되어 있다는 의미입니다. 극단적으로 큰 값이나 작은 값이 없고, 모든 차원이 골고루 정보를 담고 있습니다.

만약 몇몇 차원만 큰 값을 가지고 나머지는 0에 가깝다면, 그 벡터는 정보를 효율적으로 표현하지 못하는 것입니다. 시각화로 이해하기 1536차원을 사람이 직접 이해하기는 불가능합니다.

하지만 **PCA(주성분 분석)**를 사용하면 2차원이나 3차원으로 축소하여 시각화할 수 있습니다. 위의 코드에서 6개의 텍스트를 2차원으로 축소했습니다.

그림을 보면, 동물 관련 문장들이 한쪽에, 프로그래밍 관련 문장들이 다른 쪽에, 음식 관련 문장들이 또 다른 쪽에 모여 있는 것을 볼 수 있습니다. 이것이 바로 임베딩의 힘입니다.

비슷한 주제의 텍스트는 벡터 공간에서 가까운 위치에 배치됩니다. 다른 모델과의 비교 박시니어 씨가 참고 자료를 보여주었습니다.

"다른 임베딩 모델들도 살펴보면 재미있어요." OpenAI의 text-embedding-3-large는 3072차원을 사용합니다. 더 정확하지만, 속도와 비용 면에서 불리합니다.

오픈소스 모델인 sentence-transformers는 384차원이나 768차원을 사용합니다. 속도는 빠르지만, 복잡한 의미를 표현하는 데는 한계가 있습니다.

Titan Embeddings의 1536차원은 이 둘 사이의 **스위트 스팟(sweet spot)**입니다. 대부분의 실무 상황에서 충분히 정확하면서도 효율적입니다.

임베딩 품질 평가하기 김개발 씨는 자신의 임베딩이 잘 작동하는지 확인하고 싶었습니다. "어떻게 평가하나요?" 가장 간단한 방법은 직접 테스트하는 것입니다.

비슷한 의미의 문장 쌍과 다른 의미의 문장 쌍을 준비하고, 유사도를 계산해 봅니다. 비슷한 쌍의 유사도가 높고, 다른 쌍의 유사도가 낮으면 잘 작동하는 것입니다.

더 체계적인 방법으로는 벤치마크 데이터셋을 사용하는 것이 있습니다. KLUE, KorSTS 같은 한국어 데이터셋으로 성능을 측정할 수 있습니다.

실무에서의 활용 팁 박시니어 씨가 마지막 조언을 했습니다. "임베딩을 저장할 때는 float16으로 압축하는 것도 고려해 보세요." float32 대신 float16을 사용하면 저장 공간이 절반으로 줄어들고, 검색 속도도 빨라집니다.

정확도는 약간 떨어지지만, 대부분의 경우 실용적으로 차이가 없습니다. 또한 **양자화(quantization)**를 적용하면 int8로도 저장할 수 있습니다.

이 경우 저장 공간이 4배 줄어들지만, 정확도 손실이 있으므로 신중하게 결정해야 합니다. 정리하며 김개발 씨는 임베딩 벡터의 내부 구조를 이해하게 되었습니다.

1536차원이라는 숫자가 단순히 큰 숫자가 아니라, 의미와 효율의 균형을 맞춘 최적의 선택이라는 것을 알게 되었습니다. 여러분도 벡터의 특성을 이해하고, 저장과 검색을 최적화하면 더 효율적인 시스템을 만들 수 있습니다.

실전 팁

💡 - L2 노름이 1.0에 가까운지 확인하여 벡터가 정규화되었는지 검증하세요

  • PCA로 2D 시각화하면 임베딩의 품질을 직관적으로 확인할 수 있습니다
  • float16으로 저장하면 공간을 절반으로 줄이면서도 실용적 정확도를 유지할 수 있습니다

6. 다국어 임베딩 지원

시스템을 운영하던 중, 김개발 씨에게 새로운 요청이 들어왔습니다. "해외 고객의 영어 문의도 처리할 수 있나요?" 김개발 씨는 걱정이 되었지만, 박시니어 씨가 안심시켰습니다.

"Titan Embeddings는 25개 이상의 언어를 지원해요. 추가 설정 없이 바로 사용할 수 있습니다."

Titan Embeddings의 다국어 지원은 영어, 한국어, 일본어, 중국어 등 25개 이상의 언어를 하나의 모델로 처리합니다. 언어별로 다른 모델이 필요하지 않으며, 서로 다른 언어 간의 의미적 유사성도 계산할 수 있습니다.

글로벌 서비스 구축에 최적화된 기능입니다.

다음 코드를 살펴봅시다.

import boto3
import json
import numpy as np

bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')

def get_embedding(text):
    response = bedrock.invoke_model(
        modelId='amazon.titan-embed-text-v1',
        body=json.dumps({"inputText": text})
    )
    result = json.loads(response['body'].read())
    return np.array(result['embedding'])

def cosine_similarity(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

# 다양한 언어로 같은 의미 표현
texts = {
    'ko': "강아지가 공원에서 놀고 있습니다",
    'en': "A dog is playing in the park",
    'ja': "犬が公園で遊んでいます",
    'zh': "狗在公园里玩耍",
    'es': "Un perro está jugando en el parque"
}

# 모든 텍스트를 임베딩
embeddings = {lang: get_embedding(text) for lang, text in texts.items()}

# 한국어를 기준으로 다른 언어와의 유사도 계산
ko_embedding = embeddings['ko']
print("한국어 문장과의 유사도:")
for lang, embedding in embeddings.items():
    if lang != 'ko':
        similarity = cosine_similarity(ko_embedding, embedding)
        print(f"  {lang}: {similarity:.4f}")

# 다국어 검색 예시
def multilingual_search(query, documents):
    """쿼리 언어와 무관하게 검색"""
    query_emb = get_embedding(query)
    doc_embs = [get_embedding(doc) for doc in documents]

    similarities = [cosine_similarity(query_emb, doc_emb)
                   for doc_emb in doc_embs]

    # 유사도 순으로 정렬
    results = sorted(zip(documents, similarities),
                    key=lambda x: x[1], reverse=True)
    return results

# 영어 쿼리로 다국어 문서 검색
query = "customer service inquiry"
documents = [
    "고객 서비스 문의입니다",
    "제품 배송 추적",
    "Customer support request",
    "返品と交換"
]

results = multilingual_search(query, documents)
print("\n검색 결과:")
for doc, score in results:
    print(f"  {doc}: {score:.4f}")

김개발 씨의 회사는 최근 해외 진출을 시작했습니다. 영어권 고객이 늘어나면서, 영어 문의도 처리해야 하는 상황이 되었습니다.

"기존 시스템을 완전히 다시 만들어야 하나?" 김개발 씨는 걱정이 앞섰습니다. 다국어 임베딩의 원리 박시니어 씨가 안심시켰습니다.

"Titan Embeddings는 이미 다국어를 지원해요. 같은 모델로 한국어와 영어를 모두 처리할 수 있습니다." 다국어 임베딩이란 서로 다른 언어의 텍스트를 같은 벡터 공간에 배치하는 기술입니다.

마치 세계 지도에 모든 나라를 표시하는 것처럼, 모든 언어를 하나의 공간에 표현합니다. 핵심은 의미가 비슷하면 언어가 달라도 가까운 위치에 배치된다는 것입니다.

"강아지"와 "dog"는 언어는 다르지만, 같은 의미이므로 벡터 공간에서 가까운 곳에 놓입니다. 실제 테스트 결과 김개발 씨는 같은 의미의 문장을 다섯 개 언어로 준비했습니다.

한국어, 영어, 일본어, 중국어, 스페인어로 "강아지가 공원에서 놀고 있다"를 표현했습니다. 결과는 놀라웠습니다.

한국어 문장과의 유사도가 모두 0.85 이상이었습니다. 언어는 전혀 다르지만, 의미가 같기 때문에 높은 유사도를 보인 것입니다.

반면 전혀 다른 의미의 문장, 예를 들어 "프로그래밍은 재미있다"는 언어와 관계없이 낮은 유사도를 보였습니다. 어떤 언어를 지원할까요? Titan Embeddings는 25개 이상의 언어를 공식적으로 지원합니다.

주요 언어는 다음과 같습니다: 아시아 언어: 한국어, 일본어, 중국어(간체/번체), 태국어, 베트남어, 인도네시아어 유럽 언어: 영어, 스페인어, 프랑스어, 독일어, 이탈리아어, 포르투갈어, 러시아어 기타: 아랍어, 터키어, 폴란드어, 네덜란드어 등 실제로는 지원 목록에 없는 언어도 어느 정도 작동합니다. 다만 성능은 보장되지 않으므로, 중요한 서비스라면 테스트를 거쳐야 합니다.

다국어 검색 구현하기 김개발 씨는 다국어 검색 기능을 구현하기로 했습니다. 고객이 영어로 질문하면, 한국어로 작성된 FAQ에서도 답변을 찾아주는 시스템입니다.

위의 코드에서 multilingual_search 함수를 만들었습니다. 이 함수는 쿼리 언어와 문서 언어가 달라도 의미적 유사성을 기반으로 검색합니다.

예를 들어 "customer service inquiry"라는 영어 쿼리로 검색하면, "고객 서비스 문의입니다"라는 한국어 문서가 가장 높은 점수로 검색됩니다. 언어의 장벽을 넘어선 것입니다.

실무 활용 사례 박시니어 씨가 실제 사례를 공유했습니다. "저희 팀은 이 기능으로 글로벌 FAQ 시스템을 만들었어요." 시스템 구조는 이렇습니다:


5. 고객의 언어에 맞는 답변을 보여줍니다

실전 팁

💡 - 25개 이상의 언어를 추가 설정 없이 바로 사용할 수 있습니다

  • 언어 간 유사도가 0.8 이상이면 같은 의미로 판단할 수 있습니다
  • 다국어 FAQ는 하나의 벡터 데이터베이스에 통합하여 관리하는 것이 효율적입니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#AWS#Bedrock#Embeddings#VectorDB#SemanticSearch

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.