# 백엔드: PostgreSQL, ChromaDB, Vector Memory 설계

> 현재 문서는 하이브리드 구조를 설명하던 시점의 이력 문서입니다.
> 2026-03-17 기준 운영 런타임은 `skill-rag-file`, `rb8001` 핵심 벡터/검색/그래프 경로를 PostgreSQL 중심으로 전환했으므로, 이 문서를 현행 SSOT로 해석하면 안 됩니다.

## 개요

로빙의 백엔드는 전통적인 관계형 데이터베이스(PostgreSQL)와 벡터 데이터베이스(ChromaDB)를 조합하여, 구조화된 데이터와 의미 기반 검색을 모두 지원합니다. 이를 통해 로빙은 정확한 기록과 유연한 기억을 동시에 가질 수 있습니다.

## 왜 하이브리드 구조를 선택했나?

로빙의 기억은 인간의 뇌처럼 두 가지 방식으로 작동해야 합니다.

1.  **사실 기억 (좌뇌 역할)**: "내 이름은 김종태", "내 레벨은 10" 처럼 명확하고 변하지 않는 사실을 정확하게 기록해야 합니다. 약속 시간, 이메일 주소처럼 한 글자도 틀리면 안 되는 정보들이 여기에 해당합니다.
2.  **연상 기억 (우뇌 역할)**: "오늘 아침 대화는 좀 슬펐어" 처럼, 대화의 전체적인 분위기나 뉘앙스, 핵심 의미를 기억해야 합니다. "그때 그 느낌이랑 비슷한데?" 와 같이 의미적으로 관련된 기억을 떠올릴 수 있어야 합니다.

**PostgreSQL**은 '사실 기억'에 특화된, 마치 **꼼꼼한 일기장**과 같습니다. 정해진 양식에 따라 데이터를 정확하게 기록하고, 조건에 맞는 정보를 오차 없이 찾아냅니다.

**ChromaDB**는 '연상 기억'에 특화된, 마치 **창의적인 아이디어 노트**와 같습니다. 대화의 '의미' 자체를 숫자 배열(벡터)로 저장하여, "비슷한 느낌"이나 "관련된 맥락"의 기억을 빠르게 찾아낼 수 있습니다.

이 두 가지를 함께 사용함으로써, 로빙은 **정확한 사실을 기반으로 유연하고 맥락에 맞는 추론**을 할 수 있는, 더 인간에 가까운 기억 시스템을 갖추게 됩니다.

## 왜 이 기술들을 선택했나?

### PostgreSQL을 선택한 이유

**PostgreSQL = 로빙의 일기장 (정확한 기록)**

1. **신뢰성**: 30년 이상 검증된 데이터베이스, 데이터 손실 거의 없음
2. **ACID 보장**: 모든 거래가 완벽하게 기록되거나 안 되거나 (중간 상태 없음)
3. **복잡한 쿼리**: 여러 테이블을 조합한 복잡한 질문 가능
4. **확장성**: 수백만 건의 데이터도 문제없이 처리

**다른 선택지와 비교**:
- MySQL: 간단하지만 고급 기능 부족
- MongoDB: NoSQL이라 관계 표현 어려움
- SQLite: 동시 접속 제한, 대용량 처리 어려움

### ChromaDB를 선택한 이유

**ChromaDB = 로빙의 연상 기억 (의미 기반 검색)**

1. **벡터 검색**: "비슷한 의미"를 찾아낼 수 있음
2. **임베딩 저장**: 768차원 벡터를 효율적으로 저장/검색
3. **가벼움**: PostgreSQL에 비해 설치/관리 간단
4. **Python 친화적**: 로빙 코드와 자연스럽게 통합

**다른 선택지와 비교**:
- Pinecone: 클라우드 전용, 비용 발생
- Weaviate: 너무 무겁고 복잡
- FAISS: 영속성 관리 어려움

## 데이터 구조 설계

### PostgreSQL 스키마 (구조화된 데이터)

```sql
-- 사용자의 기본 정보와 레벨 등 명확한 데이터를 저장하는 테이블입니다.
CREATE TABLE user (
    id UUID PRIMARY KEY,
    email VARCHAR(255) UNIQUE,
    name VARCHAR(100),
    created_at TIMESTAMP,
    level INTEGER DEFAULT 1,
    experience INTEGER DEFAULT 0
);

-- 로빙의 개성(스탯)과 감정 상태 등 성장 정보를 기록하는 테이블입니다.
CREATE TABLE robeing (
    id UUID PRIMARY KEY,
    user_id UUID REFERENCES user(id),
    name VARCHAR(100),
    -- 스탯 (게임처럼 수치화)
    intelligence INTEGER DEFAULT 10,
    wisdom INTEGER DEFAULT 10,
    charisma INTEGER DEFAULT 10,
    -- 감정 상태 (Inside Out 모델)
    joy_level FLOAT DEFAULT 0.5,
    sadness_level FLOAT DEFAULT 0.0,
    -- ... (기타 감정들)
    last_active TIMESTAMP,
    total_interactions INTEGER DEFAULT 0
);

-- 로빙이 습득한 능력(스킬)의 레벨과 숙련도를 관리하는 테이블입니다.
CREATE TABLE skills (
    id UUID PRIMARY KEY,
    robeing_id UUID REFERENCES robeing(id),
    skill_name VARCHAR(100),
    skill_level INTEGER DEFAULT 1,
    experience_points INTEGER DEFAULT 0,
    last_used TIMESTAMP,
    success_rate FLOAT DEFAULT 0.0
);

-- 로빙이 수행한 모든 작업의 성공 여부, 소요 시간 등을 기록하여 나중에 분석할 수 있게 하는 로그 테이블입니다.
CREATE TABLE task_history (
    id UUID PRIMARY KEY,
    robeing_id UUID REFERENCES robeing(id),
    task_type VARCHAR(50),
    input_data JSONB,
    output_data JSONB,
    success BOOLEAN,
    duration_ms INTEGER,
    created_at TIMESTAMP DEFAULT NOW()
);
```

### ChromaDB 컬렉션 (의미 기반 데이터)

```python
# 로빙의 대화나 문서 내용을 '의미' 기반으로 저장하는 기억 컬렉션입니다.
# 나중에 "비슷한 이야기"를 찾아낼 때 사용됩니다.
memories_collection = {
    "name": "rb8001_{user_uuid}_memory",  # 사용자별로 기억을 완전히 분리합니다.
    "metadata": {
        "description": "로빙의 모든 대화 및 문서 기억",
        "embedding_model": "ko-sroberta-multitask"
    },
    "documents": [
        {
            "id": "memory_uuid",
            "text": "오늘 사용자가 프로젝트 마감일 걱정을 표현했다",
            "embedding": [0.1, 0.2, ...],  # 텍스트의 '의미'를 나타내는 768차원 숫자 배열
            "metadata": {
                "timestamp": "2025-08-08T10:30:00",
                "emotion_state": {
                    "dominant": "anxiety",
                    "intensity": 0.7
                },
                "entropy_score": 2.3,  # 이 기억이 얼마나 중요한지(놀라웠는지) 나타내는 점수
                "context": "work_discussion",
                "user_id": "user_123"
            }
        }
    ]
}
```

## 두 데이터베이스의 협업

### 하이브리드 쿼리 예시

```python
# 현재 대화와 관련된 맥락 정보를 종합적으로 가져오는 함수의 예시입니다.
async def recall_relevant_context(user_id: str, current_text: str):
    """
    로빙이 응답을 생성하기 전, 필요한 모든 배경지식을 준비하는 과정입니다.
    인간으로 치면, 대답하기 전에 잠시 생각하며 관련 정보를 떠올리는 것과 같습니다.
    """
    
    # 1. [사실 기억 조회] PostgreSQL에서 사용자의 프로필, 로빙의 스탯 등 '사실' 정보를 가져옵니다.
    user_data = await postgres.query("""
        SELECT u.*, r.*
        FROM user u
        JOIN robeing r ON u.id = r.user_id
        WHERE u.id = $1
    """, user_id)
    
    # 2. [연상 기억 조회] ChromaDB에서 현재 대화와 '의미적으로 유사한' 과거의 기억들을 떠올립니다.
    similar_memories = chromadb.query(
        query_texts=[current_text],
        n_results=10,
        where={"user_id": user_id}
    )
    
    # 3. [기억 필터링] 떠올린 기억들 중, 중요도(엔트로피)가 높은 핵심 기억만 추려냅니다.
    important_memories = [
        m for m in similar_memories 
        if m['metadata']['entropy_score'] > 2.0
    ]
    
    # 4. [능력 확인] PostgreSQL에서 현재 상황에 사용할 수 있는 스킬 목록을 확인합니다.
    relevant_skills = await postgres.query("""
        SELECT * FROM skills 
        WHERE robeing_id = $1 
        AND skill_name IN $2
        ORDER BY skill_level DESC
    """, user_data['robeing_id'], extract_skills(current_text))
    
    # 5. [정보 종합] 수집된 모든 정보를 종합하여 최종 응답 생성을 위한 컨텍스트를 만듭니다.
    return {
        'user_profile': user_data,
        'memories': important_memories,
        'available_skills': relevant_skills
    }
```

## 메모리 최적화 전략

### 1. 계층적 저장

```
# 자주 쓰는 기억은 빠르게 접근 가능한 곳에, 가끔 쓰는 기억은 깊은 곳에 보관하는 전략입니다.
HOT (자주 접근) → PostgreSQL 메모리 캐시 (RAM, 가장 빠름)
 ↓
WARM (가끔 접근) → ChromaDB 인덱스 (SSD, 중간 속도)
 ↓  
COLD (거의 안 접근) → 디스크 아카이브 (HDD, 가장 느리지만 저렴)
```

### 2. 벡터 압축

```python
# 기억의 '의미'를 담은 숫자 배열(벡터)의 크기를 줄여 저장 공간을 절약하는 기술입니다.
# 원본: 768차원 float32 = 3KB
# 압축: PCA(차원 축소) → 256차원 → int8(양자화) = 256B (12배 압축)
def compress_embedding(embedding):
    # PCA 기법으로 벡터의 핵심 성분만 남겨 차원을 줄입니다. (768 → 256)
    reduced = pca_model.transform(embedding)
    # 양자화 기술로 각 숫자를 더 작은 형태로 변환하여 용량을 줄입니다. (float32 → int8)
    quantized = (reduced * 127).astype(np.int8)
    return quantized
```

### 3. 선택적 망각

```python
# 인간처럼 덜 중요하거나 오래된 기억을 점진적으로 잊어(요약/삭제) 메모리를 확보하는 기능입니다.
def selective_forgetting(memories, max_size=1000):
    """
    기억의 총량이 한계를 넘으면, 중요도가 낮은 기억부터 정리합니다.
    """
    if len(memories) <= max_size:
        return memories
    
    # 각 기억의 중요도를 엔트로피, 접근 빈도, 감정 강도를 조합하여 계산합니다.
    for memory in memories:
        memory.importance = (
            memory.entropy_score * 0.4 +  # 이 기억이 얼마나 독특하고 놀라웠는가
            memory.access_count * 0.3 +   # 이 기억을 얼마나 자주 꺼내보았는가
            memory.emotional_intensity * 0.3  # 이 기억이 얼마나 강한 감정과 연결되어 있는가
        )
    
    # 중요도 하위 20%의 기억을 망각 대상으로 선정합니다.
    threshold = np.percentile([m.importance for m in memories], 20)
    return [m for m in memories if m.importance > threshold]
```

## 백업 및 복구

### 1. PostgreSQL 백업

```bash
# 매일 자정에 로빙의 '일기장(PostgreSQL)' 전체를 백업합니다.
pg_dump robeing_db > /backup/postgres/robeing_$(date +%Y%m%d).sql
```

### 2. ChromaDB 백업

```python
# 로빙의 '연상 기억(ChromaDB)'을 JSON 파일 형태로 백업합니다.
def backup_chromadb():
    collections = client.list_collections()
    for collection in collections:
        data = collection.get(include=['embeddings', 'metadatas', 'documents'])
        with open(f'/backup/chroma/{collection.name}.json', 'w') as f:
            json.dump(data, f)
```

## 결론

PostgreSQL과 ChromaDB의 조합은 로빙에게 두 가지 중요한 능력을 제공합니다:

1. **PostgreSQL**: 정확한 사실과 기록을 저장하는 "좌뇌"
2. **ChromaDB**: 의미와 맥락을 이해하는 "우뇌"

이 두 시스템이 협력하여 로빙은 단순한 데이터 저장을 넘어, 진정한 "기억"과 "이해"를 가진 디지털 동료가 됩니다.

*참고: 상세한 임베딩 서비스 분리 전략은 [370번 문서](./370_임베딩_서비스_분리_아키텍처.md) 참조*
