# 370. 임베딩 서비스 분리 아키텍처 ## 개요 각 로빙이 독립적으로 '언어 의미 이해 엔진(ONNX 임베딩 모델)'을 탑재하던 구조에서, 중앙의 '언어 의미 번역 센터(임베딩 서비스)'를 함께 사용하는 구조로 전환하여 메모리 사용량을 극적으로 감소시키고 관리 효율성을 확보한 과정을 설명합니다. > "모든 로빙이 하나의 전문 번역가를 공유하면, 각자의 부담은 가벼워지고 소통의 질은 유지된다." ## 왜 임베딩 서비스를 분리했나? ### 임베딩이란? - **정의**: **임베딩은 "단어/문장의 의미"를 컴퓨터가 이해할 수 있는 숫자 배열(벡터)로 번역하는 기술**입니다. - **작동 방식**: "안녕하세요"라는 텍스트를 받으면, [0.1, 0.3, -0.5, ...] 와 같이 수백 개의 숫자 배열로 변환합니다. - **핵심 역할**: 의미가 비슷한 텍스트는 이 숫자 배열의 패턴도 유사해집니다. 이를 통해 로빙은 단어의 표면적 의미를 넘어, 문맥과 뉘앙스를 파악하는 '의미 기반 검색'을 할 수 있습니다. 로빙의 기억과 이해의 핵심 기술입니다. ### 분리 전 문제점: "모두가 각자 사전을 들고 다니는 비효율" 1. **메모리 낭비**: 모든 로빙이 동일한 '언어 의미 사전(임베딩 모델, 약 450MB)'을 각자 메모리에 올려두고 있었습니다. 로빙 10개가 동시에 작동하면, 똑같은 사전 10개를 펴놓는 것과 같아 약 4.5GB의 메모리가 낭비되었습니다. 2. **느린 시작 시간**: 각 로빙이 부팅될 때마다 이 무거운 사전을 펼치는 데(모델 로드) 5~10초의 시간이 걸렸습니다. 3. **어려운 업데이트**: 사전의 새 버전이 나올 때마다, 모든 로빙의 사전을 일일이 교체하고 재시작해야 하는 번거로움이 있었습니다. ### 분리 후 장점: "마을 중앙 도서관을 함께 이용하는 효율" 1. **메모리 절약**: 이제 마을 중앙 도서관(중앙 임베딩 서비스)에 가장 좋은 사전 한 권만 비치하고, 모든 로빙이 필요할 때마다 와서 찾아봅니다. 메모리 낭비가 사라졌습니다. 2. **빠른 시작**: 로빙은 더 이상 무거운 사전을 들고 다닐 필요 없이, 가볍게 시작하여 도서관에 질문만 하면 됩니다. 3. **쉬운 관리**: 새 사전이 나오면, 도서관의 사전 한 권만 교체하면 모든 로빙이 즉시 최신 정보를 이용할 수 있습니다. ## 해결책: 중앙 임베딩 서비스 (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는 서비스끼리 대화하는 약속(규칙)입니다. 로빙과 임베딩 서비스는 다음과 같이 간단한 대화를 나눕니다. ```python # 로빙의 질문 (POST /embed) { "texts": ["안녕하세요", "오늘 날씨가 좋네요"] } # 임베딩 서비스의 답변 (숫자 배열) { "embeddings": [ [0.1, 0.2, ...], # "안녕하세요"의 의미 [0.3, 0.4, ...] # "오늘 날씨가 좋네요"의 의미 ], "model": "jhgan/ko-sroberta-multitask", "dimensions": 768 } ``` ## 구현 가이드 ### 1. 로빙 측 변경사항: "직접 찾지 않고, 물어보기" ```python # 이전: 로빙이 직접 무거운 사전을 뒤적임 (메모리 낭비) 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 통합: "로빙의 기억 노트" ```python # 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)하지만, 의도·콜드메일 정확도 향상 효과가 더 큽니다. ## 교훈 - **전문가의 원리**: 모두가 전문가가 될 필요는 없습니다. '언어 의미 이해'라는 전문적인 작업은 전문가(서비스)에게 맡기고, 나머지는 자신의 역할에 집중하는 것이 효율적입니다. - **공유와 독립의 균형**: 핵심 엔진(임베딩)은 공유하여 효율을 높이되, 각자의 기억과 개성은 철저히 독립적으로 유지해야 합니다. --- *"임베딩은 공유하되, 기억은 분리하라"*