DB 전략 — 트랜잭션 이중 지원 + 벡터 저장소 분리
작성 시각: 2026-09-13 확정 결정: 트랜잭션(업무) 데이터는 MySQL·PostgreSQL 이중 지원을 유지하고, 벡터 검색(RAG)만 PostgreSQL(pgvector) 또는 외부 벡터 저장소로 분리한다.
1. 왜 이렇게 나누는가
- 엔터프라이즈 고객은 기존 DB 표준(MySQL 또는 PostgreSQL)을 바꾸지 않는다 → 업무 DB 고정은 수주 장애물이다.
- 벡터 검색은 "기능 스토리지"라 고객 환경에 맞춰 교체할 수 있다(pgvector ↔ 외부 벡터 DB).
- AI 완성(요약·번역·정합성 텍스트 분석)은 DB를 쓰지 않는다(텍스트만 LLM에 전달) → DB 중립.
2. 지원 매트릭스
| 계층 | 예시 | MySQL | PostgreSQL | 근거 |
|---|---|---|---|---|
| 트랜잭션/업무 | 프로세스·전결 규정·알림·사용자 | ✅ | ✅ | drizzle/schema.mysql.ts + schema.pg.ts + server/db-connection.ts dialect helper |
| AI 완성 | 게시본 요약, 자동 번역, 정합성(텍스트 비교) | ✅ | ✅ | aiCompletionEnabled() = PRIVATE_LLM_URL 설정 |
| 벡터 검색(RAG) | 유사 문서·질의응답·의미 중복 탐지 | ⚠️ | ✅ | vectorSearchEnabled() = RAG_ENABLED + postgresql + PRIVATE_EMBEDDING_URL, 또는 외부 벡터 저장소 주입 |
⚠️ MySQL에서 벡터 검색을 쓰려면 외부 벡터 저장소(Qdrant·Milvus·Elasticsearch kNN 등)를
setVectorStore()로 주입한다. pgvector의<=>·::vector·ANN 인덱스에 해당하는 기능이 MySQL에는 없다.
3. capability 플래그 (server/ai/aiClient.ts)
| 함수 | 조건 | 대상 기능 |
|---|---|---|
aiCompletionEnabled() | PRIVATE_LLM_URL 설정 | 요약·번역·정합성 등 LLM 완성 계열 |
isPostgresDialect() | DATABASE_DIALECT=postgresql | 벡터 검색의 전제 조건 |
vectorSearchEnabled() | RAG_ENABLED=true ∧ postgresql ∧ PRIVATE_EMBEDDING_URL | RAG·유사도 계열 |
AI 완성 기능은 RAG 스위치·업무 DB 종류와 무관하게 켜진다(기존 RAG_ENABLED 공유 결합을 해소).
4. 벡터 저장소 계약 (server/rag/vectorStore.ts)
ts
interface VectorStore {
readonly kind: string;
health(): { kind: string; available: boolean; reason?: string };
search(input: { vector: readonly number[]; principal: RagPrincipal; limit: number }): Promise<RagSearchResult[]>;
}- 기본 구현
createPgVectorStore()— 트랜잭션 안에서set_config('my.app_tenant_id | my.app_user_org_id | my.app_user_security_level', ..., true)(SET LOCAL)로 RLS 컨텍스트를 설정한 뒤 pgvector<=>로 유사 청크를 조회한다. - 비활성 구현
createDisabledVectorStore(reason)— 사유와 함께 실패하며, 게시·AI 완성 기능에는 영향을 주지 않는다. - 외부 저장소:
setVectorStore(adapter)로 주입(후속: Qdrant/Milvus 어댑터 구현). privateRag.searchPrivateRag()는 저장소를 해석해 위임만 하며, 비활성이면 임베딩 호출도 하지 않는다.
5. 지켜야 할 규율 (이중 지원 비용을 낮추는 방법)
- 업무 저장소(
server/repositories)에서 PG 전용 SQL 금지:set_config,::vector,ON CONFLICT,RETURNING,ILIKE,jsonb연산자,pg_*함수.- 예외는 벡터/RAG 계층(
server/rag/**)과 dialect helper(server/db-connection.ts)에만 허용한다.
- 예외는 벡터/RAG 계층(
- dialect 차이는 helper로 흡수한다:
insertReturningId·getAffectedRowCount·dialectUpsert· advisory lock(PGpg_try_advisory_xact_lock/ MySQLGET_LOCK). - 스키마는
drizzle/schema.mysql.ts와drizzle/schema.pg.ts를 항상 함께 갱신하고drizzle/schema.ts(barrel)에 등록한다. - 마이그레이션은 dialect별로 각각 만든다 — 새 PostgreSQL DDL은 재실행 안전하게 멱등(
IF NOT EXISTS)으로 작성한다.
6. 현재 진행 상태 (2026-09-13)
| 항목 | 상태 |
|---|---|
| PostgreSQL 마이그레이션 | 0014 ~ 0016 적용·검증 완료(멱등 DDL, 개발 DB 반영) |
| MySQL 마이그레이션 | 0027·0028 파일만 유지 — 생성·적용·검증은 후속(초기 개발은 PostgreSQL 집중) |
| 검증 스크립트 | pnpm db:postgres:verify-governance — 트랜잭션 롤백으로 상태 불변 확인 |
| 호환 테스트 | server/db-connection.compatibility.test.ts로 dialect별 동작 검증 |
| RAG/벡터 | server/rag/vectorStore.ts 계약 도입, pgvector·비활성 구현 완료 |
7. 남은 작업
- 외부 벡터 저장소 어댑터(Qdrant·Milvus) 실제 구현 + 운영 환경에서
setVectorStore()주입 경로 - MySQL 환경에서 RAG 비활성 사유를 화면에 노출(
privateRagHealth().reason활용) — 현재는 health 응답으로만 제공 - MySQL 마이그레이션 생성·적용 검증(PostgreSQL 검증 완료 후)
8. AI·벡터 계층의 서비스 경계 (플랫폼 아키텍처 연결)
이 저장소의 AI 구현은 조직의 기존 플랫폼 경계 문서를 따른다.
| AI 계층 | 소유자 | 근거 |
|---|---|---|
계약·유틸(@hohyun94/portal-ai) | portal-foundation | 소비 포털이 빌드하는 재사용 계약 |
| 포털별 적용(게시본 요약 트리거·번역 대상 필드) | ProcessCenter | 업무 정책 |
| 다중 포털 탐색 실행 조합(포털 어댑터 오케스트레이션) | ExploreGateway | ExploreGateway/docs/SERVICE_BOUNDARIES.md, ProcessCenter/docs/EXPLORE_ADAPTER_CONTRACT.md |
| 문서 인덱스·RAG·citation | RagService | FileStore/docs/ARCHITECTURE.md, UiPlatform/docs/DECISION_LOG.md(2026-09-13 정정) |
원본 파일 보관·RAG 발행(ragPublishAllowed) | FileStore | FileStore/docs/ARCHITECTURE.md |
| 모델·키·비용·감사(LLM 완성) | ModelGateway(모델 게이트웨이) | UiPlatform/docs/DECISION_LOG.md(2026-09-13), MODEL_GATEWAY_SCOPE.md |
server/rag/**·server/ai/**는 이제 위임·폴백 계층이다. LLM 완성은MODEL_GATEWAY_URL(ModelGateway), 문서 인덱스·RAG는RAG_SERVICE_URL(RagService)로 나가고, 각각 미설정·실패일 때만 로컬(PRIVATE_LLM_URL·pgvectorVectorStore)로 폴백한다.- RAGCenter 연동 상태(2026-09-26): PC가 맡는 것은 ① 검색·답변 위임(
server/ragCenterClient.ts→/api/v1/search·/api/v1/chat/stream,rag:read위임 토큰) ② 수집 발행(server/ragPublicationService.ts→POST /api/v1/documents,rag:admin) ③ 삭제 전파(같은 admin API, 실패는 비차단) ④ 첨부 원본 제공(GET /api/rag/v1/attachments/:assetId— 서명 URL을 서버가 받아 스트리밍)이다. 식별자는server/rag/ragIdentity.ts의 uuidv5 투영이 단일 기준이다. 상세·잔여 항목:docs/RAGCENTER_INTEGRATION_PLAN.md§4·§5. server/rag·server/ai는 폴백 전용으로 유지한다 — P3 결정(2026-09-26): 삭제하지 않는다(RAGService 장애 시 로컬이 유일한 검색 수단이고,VectorStore계약이 외부 벡터 DB 어댑터 주입 지점이다). 재검토는 RAGService 가용성이 목표(SLO)를 만족하고 배포 스택에 기본 포함될 때. 계약(@hohyun94/portal-ai)과 포털 적용 로직은 각각portal-foundation·이 저장소에 유지한다.- 탐색 어댑터(구현 완료):
server/remoteExplore.ts가POST /api/explore/v1/search를 제공한다(서비스 토큰EXPLORE_GATEWAY_API_KEY+x-explore-subject-id). 계약:docs/EXPLORE_ADAPTER_CONTRACT.md·docs/explore-adapter.openapi.json. - ModelGateway 위임 구현 완료(2026-09-13):
server/ai/aiClient.ts가MODEL_GATEWAY_URL·MODEL_GATEWAY_SERVICE_TOKEN설정 시 게이트웨이를 우선 사용하고(귀속 헤더x-portal-id·x-purpose·x-actor자동 부착) 없으면PRIVATE_LLM_URL로 폴백한다. 남은 것은 운영 환경의 주소·토큰 주입과 P3(프롬프트 표준화)다. - 목적(purpose) 기반 모델 라우팅(2026-09-26): 모델을 고정하지 않으면(
MODEL_GATEWAY_MODEL·PRIVATE_LLM_MODEL미설정) 요청 본문에 예약값model: "auto"를 보내 게이트웨이가purposeModels(목적별 모델)·기본 모델로 고른다. 소비 포탈은 목적(x-purpose)만 알면 되고, 모델·키·쿼터·비용·감사는 게이트웨이가 소유한다. 모델 고정이 필요한 배포는 기존 env를 그대로 쓰면 우선 적용된다. - RAG 답변 생성도 같은 경로로 통일(2026-09-26):
server/rag/privateRag.ts의answerPrivateRag()가resolveAiProvider({ purpose: "rag.answer", actor })를 거친다.server/rag/privateAi.ts에는 임베딩(createPrivateEmbedding)만 남아, LLM 호출이 게이트웨이를 우회해 키·쿼터·감사가 갈라지지 않는다. 화면 진입은 2026-10-05 상단 검색창(연합 검색 + 중앙 RagService 답변)으로 통합돼/rag·/explore화면과 메뉴 항목이 사라졌다 — 코드는 아래 P3 대로 남는다.