- 7-8월 초기 구축 문서 12개를 _archive/troubleshooting/2025_07-08_initial_setup/로 이동 - book/300_architecture/390_human_in_the_loop_intent_learning.md를 journey/research/intent_classification/로 이동 (개발 여정 문서) - 빈 폴더 제거 (journey/assets/*)
7.7 KiB
7.7 KiB
370. 임베딩 서비스 분리 아키텍처
개요
각 로빙이 독립적으로 '언어 의미 이해 엔진(ONNX 임베딩 모델)'을 탑재하던 구조에서, 중앙의 '언어 의미 번역 센터(임베딩 서비스)'를 함께 사용하는 구조로 전환하여 메모리 사용량을 극적으로 감소시키고 관리 효율성을 확보한 과정을 설명합니다.
"모든 로빙이 하나의 전문 번역가를 공유하면, 각자의 부담은 가벼워지고 소통의 질은 유지된다."
왜 임베딩 서비스를 분리했나?
임베딩이란?
- 정의: 임베딩은 "단어/문장의 의미"를 컴퓨터가 이해할 수 있는 숫자 배열(벡터)로 번역하는 기술입니다.
- 작동 방식: "안녕하세요"라는 텍스트를 받으면, [0.1, 0.3, -0.5, ...] 와 같이 수백 개의 숫자 배열로 변환합니다.
- 핵심 역할: 의미가 비슷한 텍스트는 이 숫자 배열의 패턴도 유사해집니다. 이를 통해 로빙은 단어의 표면적 의미를 넘어, 문맥과 뉘앙스를 파악하는 '의미 기반 검색'을 할 수 있습니다. 로빙의 기억과 이해의 핵심 기술입니다.
분리 전 문제점: "모두가 각자 사전을 들고 다니는 비효율"
- 메모리 낭비: 모든 로빙이 동일한 '언어 의미 사전(임베딩 모델, 약 450MB)'을 각자 메모리에 올려두고 있었습니다. 로빙 10개가 동시에 작동하면, 똑같은 사전 10개를 펴놓는 것과 같아 약 4.5GB의 메모리가 낭비되었습니다.
- 느린 시작 시간: 각 로빙이 부팅될 때마다 이 무거운 사전을 펼치는 데(모델 로드) 5~10초의 시간이 걸렸습니다.
- 어려운 업데이트: 사전의 새 버전이 나올 때마다, 모든 로빙의 사전을 일일이 교체하고 재시작해야 하는 번거로움이 있었습니다.
분리 후 장점: "마을 중앙 도서관을 함께 이용하는 효율"
- 메모리 절약: 이제 마을 중앙 도서관(중앙 임베딩 서비스)에 가장 좋은 사전 한 권만 비치하고, 모든 로빙이 필요할 때마다 와서 찾아봅니다. 메모리 낭비가 사라졌습니다.
- 빠른 시작: 로빙은 더 이상 무거운 사전을 들고 다닐 필요 없이, 가볍게 시작하여 도서관에 질문만 하면 됩니다.
- 쉬운 관리: 새 사전이 나오면, 도서관의 사전 한 권만 교체하면 모든 로빙이 즉시 최신 정보를 이용할 수 있습니다.
해결책: 중앙 임베딩 서비스 (skill-embedding)
로빙의 여러 기능 중, '언어 의미 이해'라는 전문적인 역할을 별도의 마이크로서비스(Microservice), 즉 '기능별로 나뉜 전문 서비스'로 분리했습니다.
아키텍처: "각자의 집과 중앙 도서관"
이전: "모든 집에 두꺼운 백과사전 비치"
┌─────────────┐
│ rb10508의 집 │
│ 백과사전 (988MB) │
└─────────────┘
┌─────────────┐
│ rb8001의 집 │
│ 백과사전 (416MB) │
└─────────────┘
이후: "가벼운 집 + 중앙 도서관"
┌─────────────┐
│ rb10508의 집 │
│ (118MB) │──┐
└─────────────┘ │
│ (API 통신: "이 단어 뜻이 뭐야?")
▼
┌─────────────┐ ┌─────────────────┐
│ rb8001의 집 │ │ 중앙 도서관 (skill-embedding) │
│ (200MB) │──│ (874.4MB) │
└─────────────┘ │ - 최고의 백과사전 1권 │
│ - 24시간 운영 (Port 8515) │
▼ └─────────────────┘
┌─────────────┐
│ rb10408의 집 │
│ (30MB) │──┘
└─────────────┘
기술 스택 선택 이유
-
FastAPI + Uvicorn:
- 왜? Python 기반의 초고속 도로. 여러 로빙이 동시에 질문해도 막힘없이 처리(비동기)할 수 있습니다.
-
ONNX Runtime:
- 왜? 같은 사전이라도 더 가볍고 빠르게 읽을 수 있게 해주는 기술. 기존 방식(PyTorch)보다 3배 빠르고 메모리는 절반만 사용합니다.
-
Ko-SRoBERTa(multitask) SentenceTransformer → ONNX 변환 (768차원):
- 왜? 한국어 대화·명령 코퍼스에 특화돼 의도 분류·콜드메일 필터 정확도가 MiniLM 대비 20pt 이상 향상되었습니다.
API 설계: "간단한 질문과 명확한 답변"
API는 서비스끼리 대화하는 약속(규칙)입니다. 로빙과 임베딩 서비스는 다음과 같이 간단한 대화를 나눕니다.
# 로빙의 질문 (POST /embed)
{
"texts": ["안녕하세요", "오늘 날씨가 좋네요"]
}
# 임베딩 서비스의 답변 (숫자 배열)
{
"embeddings": [
[0.1, 0.2, ...], # "안녕하세요"의 의미
[0.3, 0.4, ...] # "오늘 날씨가 좋네요"의 의미
],
"model": "jhgan/ko-sroberta-multitask",
"dimensions": 768
}
구현 가이드
1. 로빙 측 변경사항: "직접 찾지 않고, 물어보기"
# 이전: 로빙이 직접 무거운 사전을 뒤적임 (메모리 낭비)
from onnx_embedder import ONNXEmbedder
embedder = ONNXEmbedder("/models/onnx/...") # 449MB 모델 로드
# 변경: 가볍게 도서관(임베딩 서비스)에 전화해서 물어봄
class HTTPEmbeddingFunction(EmbeddingFunction):
def __init__(self):
# 도서관의 전화번호(URL)는 환경에 따라 설정
self.url = f"{os.getenv('SKILL_EMBEDDING_URL', 'http://localhost:8515')}/embed"
def __call__(self, input: List[str]) -> List[List[float]]:
# 물어볼 단어들을 서비스로 보내고, 의미(벡터)를 받아옴
response = requests.post(self.url, json={"texts": input}, timeout=30)
return response.json()["embeddings"]
2. ChromaDB 통합: "로빙의 기억 노트"
# memory.py - 로빙의 기억을 저장하는 코드
# 각 로빙은 자신만의 기억 노트(Collection)를 가짐
self.episodic = self.client.get_or_create_collection(
name=f"{self.robeing_id}_episodic",
# 단어의 의미를 물어볼 때는 중앙 도서관(HTTPEmbeddingFunction)을 이용
embedding_function=HTTPEmbeddingFunction()
)
# 결과: 기억은 각자 독립적으로, 의미 분석은 중앙에서 공유
성능 분석
메모리 절감 (실측 데이터)
로빙 이름 | 이전 메모리 | 이후 메모리 | 절감량
------------|------------|------------|--------
rb10508 | 988MB | 118MB | -870MB (88%↓)
rb8001 | 416MB | 200MB | -216MB (52%↓)
rb10408 | 55MB | 30MB | -25MB (45%↓)
# 예상 절감 효과
- 로빙 10개 운영 시: 8.7GB 메모리 절약
- 로빙 100개 운영 시: 87GB 메모리 절약 (서버 1대 가격)
속도 영향
- 내부 통신 시간: MiniLM 대비 약 +30ms 증가(총 35~40ms)하지만, 의도·콜드메일 정확도 향상 효과가 더 큽니다.
교훈
- 전문가의 원리: 모두가 전문가가 될 필요는 없습니다. '언어 의미 이해'라는 전문적인 작업은 전문가(서비스)에게 맡기고, 나머지는 자신의 역할에 집중하는 것이 효율적입니다.
- 공유와 독립의 균형: 핵심 엔진(임베딩)은 공유하여 효율을 높이되, 각자의 기억과 개성은 철저히 독립적으로 유지해야 합니다.
"임베딩은 공유하되, 기억은 분리하라"