Esc
← 모든 글

라이브 활동 데이터와 비정기 배치 파이프라인 함께 설계하기

정적 지식 베이스와 사용자 활동 그래프를 Weaviate, Neo4j, PostgreSQL에 함께 적재하고 RAG와 추천 시스템에 활용하는 데이터 파이프라인 설계 전략.

데이터파이프라인을 설계하면서 서로 다르지만 서로 얽혀있는 두 가지 흐름을 정리해내는게 어려웠다. 하나는 사용자의 클릭, 조회, 검색, 북마크, 시청, 문제 풀이처럼 거의 실시간으로 들어오는 라이브 활동 데이터다. 다른 하나는 콘텐츠 메타데이터, 도메인 문서, 교육과정, 상품 카탈로그, 운영자가 업로드한 보정 파일처럼 비정기적으로 들어오는 정적 데이터다.

둘 중 하나만 다룰 때는 설계가 비교적 단순하다. 라이브 데이터는 스트림 또는 이벤트성 파이프라인으로 받고, 정적 데이터는 배치 작업으로 처리하면 된다. 문제는 실제 서비스에서는 두 데이터가 따로 존재하지 않는다는 점이다. 정적 데이터는 지식 베이스를 구성하고, 활동 데이터는 사용자가 그 지식 베이스 위에서 무엇을 했는지를 설명한다. RAG는 이 둘을 바탕으로 답변의 근거를 찾고, 추천 알고리즘은 이 둘을 바탕으로 다음에 볼 만한 대상을 고른다.

그래서 이번 글에서는 라이브 활동 데이터와 비정기 배치 파이프라인을 통합적으로 동작시키는 설계 전략을 정리해보려 한다. 여기서의 핵심은 “스트리밍과 배치를 하나의 기술로 합친다”가 아니다. 더 정확히는 정적 지식 레이어와 동적 활동 레이어가 서로 다른 속도로 갱신되더라도, PostgreSQL, Weaviate, Neo4j 위에서 하나의 일관된 검색·추천 기반으로 수렴하도록 만드는 것이다.

먼저 데이터의 역할을 분리하기#

이 구조에서 다루는 데이터는 크게 두 종류다.

첫 번째는 비정기적으로 인입되는 정적 데이터다. 문서, 콘텐츠, 개념, 태그, 카테고리, 성취기준, 상품 속성, 조직 정보처럼 비교적 자주 바뀌지 않지만 서비스의 의미 구조를 만드는 데이터다. 이 데이터는 지식 베이스의 원천이 된다.

정적 데이터는 보통 세 군데로 적재된다.

  • PostgreSQL: 원본 레코드, 정규화된 속성, 관리 화면, 검수 상태, 버전 메타데이터를 저장한다.
  • Weaviate: 문서 청크, 설명 텍스트, QA 데이터, 임베딩 벡터를 저장해 semantic search와 hybrid search에 사용한다.
  • Neo4j: 개념, 문서, 콘텐츠, 카테고리, 선수 관계, 포함 관계, 참조 관계를 LPG(Label Property Graph)로 표현한다.

두 번째는 라이브 활동 데이터다. 사용자가 어떤 문서를 열람했는지, 어떤 콘텐츠를 클릭했는지, 어떤 질의를 던졌는지, 어떤 추천 결과를 무시하거나 선택했는지 같은 데이터다. 이 데이터는 전처리 과정을 거쳐 PostgreSQL과 Neo4j에 들어간다. PostgreSQL에서는 이벤트 로그, 세션 요약, 사용자별 집계, 피처 테이블로 쓰이고, Neo4j에서는 기존 지식 그래프 위에 사용자 활동 레이어를 구성한다.

예를 들면 정적 지식 그래프가 이런 구조를 가진다고 하자.

(Concept)-[:PREREQUISITE_OF]->(Concept)
(Document)-[:EXPLAINS]->(Concept)
(Content)-[:COVERS]->(Concept)
(Content)-[:HAS_TAG]->(Tag)

활동 데이터가 들어오면 같은 그래프 위에 다음과 같은 관계가 추가된다.

(User)-[:VIEWED {count, last_seen_at}]->(Content)
(User)-[:ASKED {query, created_at}]->(Question)
(User)-[:BOOKMARKED {created_at}]->(Document)
(User)-[:SOLVED {score, solved_at}]->(Content)

이렇게 보면 정적 데이터와 활동 데이터는 단순히 저장소만 공유하는 것이 아니다. 정적 데이터는 서비스가 이해하는 세계를 만들고, 활동 데이터는 사용자가 그 세계 안에서 남긴 경로를 만든다.

통합의 기준은 처리 기술이 아니라 데이터 계약이다#

스트리밍 엔진을 쓸지, 배치 스케줄러를 쓸지, 큐를 쓸지는 중요한 문제다. 하지만 그보다 먼저 정해야 할 것은 데이터 계약이다.

정적 데이터와 활동 데이터는 서로 다른 속도로 들어오지만 결국 같은 엔티티를 참조한다. 활동 이벤트의 content_id는 PostgreSQL의 콘텐츠 테이블, Weaviate의 객체, Neo4j의 Content 노드와 연결되어야 한다. 문서 청크의 document_id는 원본 문서 테이블과 그래프 노드, 벡터 객체를 추적할 수 있어야 한다.

따라서 최소한 다음 기준은 맞춰야 한다.

항목필요한 이유
안정적인 비즈니스 키PostgreSQL, Weaviate, Neo4j 사이에서 같은 대상을 식별하기 위해
원천 시스템과 수집 시각데이터가 어디서 언제 들어왔는지 추적하기 위해
발생 시각과 처리 시각의 분리활동이 일어난 시점과 파이프라인이 알게 된 시점을 구분하기 위해
스키마 또는 데이터 버전정적 지식 베이스의 변경과 재임베딩 필요성을 판단하기 위해
삭제·비활성·정정 표현그래프 엣지와 벡터 객체가 오래된 상태로 남지 않게 하기 위해
idempotency key같은 파일이나 이벤트를 재처리해도 중복 반영되지 않게 하기 위해

특히 발생 시각과 처리 시각은 반드시 분리해야 한다. 활동 이벤트는 사용자가 어제 발생시킨 이벤트가 네트워크 지연으로 오늘 들어올 수 있고, 비정기 배치 데이터는 한 달 전 문서의 수정본이 오늘 업로드될 수 있다. 수집 시각만으로 최신성을 판단하면 그래프와 벡터 DB가 잘못된 상태로 갱신된다.

통합 파이프라인에서 중요한 질문은 “지금 들어온 데이터가 최신인가?”가 아니다. 더 정확한 질문은 **“이 데이터는 어떤 엔티티의 어떤 표현을 어느 저장소에서 어떤 권위로 갱신해야 하는가?”**이다.

저장소별 스키마를 따로 설계하되 함께 맞춘다#

이 파이프라인에서는 PostgreSQL, Weaviate, Neo4j가 모두 같은 데이터를 바라보지만, 같은 모양으로 저장하지는 않는다. PostgreSQL은 테이블과 제약조건에 맞게, Weaviate는 컬렉션과 벡터 검색 속성에 맞게, Neo4j는 라벨과 관계 타입에 맞게 데이터를 표현한다. 그래서 각 DB의 스키마를 독립적으로 설계하되, 서로 맞춰주는 작업이 반드시 필요하다.

이때 기준이 되는 것은 하나의 canonical schema다. 예를 들어 Content, Document, Chunk, Concept, User, Activity 같은 핵심 엔티티를 먼저 정의하고, 각 엔티티가 어떤 식별자와 버전, 상태, 원천 정보를 가져야 하는지 정한다. 그 다음 이 canonical schema를 각 저장소에 맞게 투영한다.

canonical entityPostgreSQLWeaviateNeo4j
Documentdocuments 테이블문서 메타데이터 또는 청크의 참조 속성(:Document) 노드
Chunkdocument_chunks 테이블검색 대상 컬렉션 객체(:Chunk) 노드 또는 문서의 속성
Conceptconcepts 테이블검색 필터용 메타데이터(:Concept) 노드
Contentcontents 테이블추천 후보 텍스트 객체(:Content) 노드
Activityactivity_events, user_features 테이블보통 직접 적재하지 않거나 일부 질의 로그만 적재(:User)-[:VIEWED]->(:Content) 같은 관계

여기서 중요한 것은 모든 저장소에 같은 필드를 억지로 복제하는 것이 아니다. 각 저장소가 맡은 역할에 필요한 속성은 다르다. 대신 서로를 다시 연결할 수 있는 공통 키와 버전 정보는 반드시 맞춰야 한다. 예를 들어 Weaviate의 청크 객체에는 chunk_id, document_id, content_id, schema_version, kb_version, active 같은 메타데이터가 필요하고, Neo4j의 노드와 엣지에도 동일한 비즈니스 키와 유효 기간이 있어야 한다. PostgreSQL에는 이 매핑을 검산할 수 있는 기준 테이블과 적재 작업 이력이 남아야 한다.

스키마 정합성을 맞추는 작업은 초기 설계 때 한 번 하고 끝나는 일이 아니다. 정적 데이터의 필드가 추가되거나, 청킹 전략이 바뀌거나, 그래프 관계 타입이 늘어나거나, 추천 피처가 바뀌면 세 저장소의 스키마 매핑도 함께 바뀐다. 따라서 파이프라인에는 “PostgreSQL 테이블은 바뀌었는데 Weaviate 필터 속성은 그대로인 상태”, “Neo4j에는 새 관계가 생겼는데 RAG 검색 후처리는 이전 관계만 보는 상태”를 잡아낼 수 있는 스키마 버전과 호환성 검사가 필요하다.

권장 구조: 원천 보존, 표준화, 투영, 수렴#

라이브와 배치를 통합할 때는 처음부터 최종 테이블, 최종 그래프, 최종 벡터 컬렉션에 직접 쓰기보다 단계를 나누는 편이 안전하다.

원천 입력
-> Raw 보존
-> 표준화
-> 저장소별 투영
-> 수렴된 서빙 모델

첫 번째는 원천 보존 계층이다. 들어온 데이터는 가능한 한 원본 형태에 가깝게 저장한다. 라이브 이벤트는 append-only 이벤트 로그로 남기고, 배치 파일은 파일 단위 메타데이터와 함께 보존한다. 이 계층의 목적은 분석 편의성이 아니라 재처리 가능성이다. 청킹 전략이 바뀌거나 임베딩 모델이 바뀌거나 그래프 스키마가 바뀌었을 때 다시 계산할 수 있어야 한다.

두 번째는 표준화 계층이다. 서로 다른 원천의 데이터를 공통 스키마로 맞춘다. 필드명, 타입, 타임존, 상태값, 식별자 체계를 정리한다. 정적 데이터라면 문서, 청크, 개념, 태그, 출처, 버전 같은 단위로 쪼개고, 활동 데이터라면 사용자, 세션, 이벤트, 대상 엔티티, 이벤트 타입으로 정규화한다.

세 번째는 저장소별 투영 계층이다. 같은 표준 데이터를 각 저장소의 목적에 맞는 형태로 변환한다.

  • PostgreSQL에는 관리와 검증, 조인, 집계에 적합한 정규화 테이블을 둔다.
  • Weaviate에는 검색 대상 텍스트와 임베딩, 검색 필터로 쓸 메타데이터를 둔다.
  • Neo4j에는 엔티티 간 관계와 사용자 활동 관계를 그래프 형태로 둔다.

네 번째는 수렴된 서빙 모델이다. RAG와 추천 시스템이 실제로 참조하는 관점이다. RAG는 Weaviate에서 관련 청크를 찾고, Neo4j에서 개념 관계나 사용자 맥락을 확장하며, PostgreSQL에서 권한·상태·품질 정보를 확인할 수 있다. 추천 시스템은 Neo4j의 사용자 활동 그래프와 지식 그래프를 함께 보고 후보를 만들고, PostgreSQL의 집계 피처나 Weaviate의 임베딩 유사도를 함께 활용할 수 있다.

정적 데이터는 지식 베이스의 버전으로 다룬다#

비정기적으로 들어오는 정적 데이터는 단순한 배치 파일이 아니다. 이 데이터는 RAG와 추천 시스템이 바라보는 세계의 구조를 바꾼다. 새 문서가 들어오면 검색 후보가 늘어나고, 새 개념 관계가 들어오면 GraphRAG의 탐색 경로가 바뀌며, 콘텐츠 속성이 수정되면 추천 후보의 의미가 바뀐다.

따라서 정적 데이터는 지식 베이스의 버전으로 다루는 편이 좋다. 파일 하나가 들어왔을 때 단순히 테이블을 덮어쓰는 것이 아니라, 다음을 기록해야 한다.

  • 어떤 원천에서 들어왔는가
  • 어떤 기간 또는 범위를 커버하는가
  • 전체 스냅샷인가, 증분인가
  • 기존 데이터를 대체하는가, 추가하는가
  • 어떤 문서와 청크가 생성·변경·비활성화되었는가
  • 어떤 임베딩 모델과 청킹 전략으로 Weaviate에 적재되었는가
  • 어떤 노드와 엣지가 Neo4j에 생성·변경·비활성화되었는가

여기서 중요한 것은 Weaviate와 Neo4j를 PostgreSQL의 부속 캐시처럼만 보지 않는 것이다. 물론 운영 메타데이터와 정합성의 기준은 PostgreSQL에 둘 수 있다. 하지만 RAG와 추천에서 실제 질의가 발생하는 저장소는 Weaviate와 Neo4j다. 그러므로 벡터 객체와 그래프 노드 역시 명확한 식별자, 버전, 활성 상태, 원천 추적 정보를 가져야 한다.

예를 들어 문서가 업데이트되면 단순히 새 청크를 추가하는 것으로 끝나지 않는다. 이전 청크를 비활성화해야 하고, 새 청크의 임베딩을 생성해야 하며, 해당 문서가 설명하는 개념 관계도 다시 계산해야 할 수 있다. Neo4j의 Document-[:EXPLAINS]->Concept 관계와 Weaviate의 청크 객체가 서로 다른 버전을 가리키면 RAG 결과의 근거가 흔들린다.

활동 데이터는 그래프의 사용자 활동 레이어가 된다#

라이브 활동 데이터는 빠르게 들어오지만, 그대로 추천이나 RAG에 쓰기에는 거칠다. 클릭 한 번, 조회 한 번, 검색 한 번은 신호이긴 하지만 노이즈도 많다. 그래서 활동 데이터는 전처리를 거쳐 PostgreSQL과 Neo4j에 들어가야 한다.

PostgreSQL에서는 활동 데이터를 이벤트 로그와 집계 피처로 관리한다. 예를 들어 사용자별 최근 조회 콘텐츠, 카테고리별 클릭 수, 세션별 질의 수, 콘텐츠별 전환율 같은 값은 테이블 구조가 더 다루기 쉽다. 모델 학습, 대시보드, A/B 테스트 분석에도 이런 형태가 유리하다.

Neo4j에서는 활동 데이터를 LPG의 사용자 활동 레이어로 표현한다. 중요한 점은 정적 지식 그래프와 활동 그래프를 완전히 분리하지도, 무작정 섞지도 않는 것이다. 같은 그래프 안에 두되 레이블과 관계 타입, 속성으로 계층을 명확히 나눠야 한다.

정적 지식 레이어
(Content)-[:COVERS]->(Concept)
(Concept)-[:PREREQUISITE_OF]->(Concept)
(Document)-[:EXPLAINS]->(Concept)
사용자 활동 레이어
(User)-[:VIEWED]->(Content)
(User)-[:SEARCHED]->(Query)
(User)-[:SELECTED]->(Content)
(User)-[:FAILED_ON]->(Concept)

이 구조의 장점은 사용자 행동을 의미 구조와 연결할 수 있다는 점이다. 사용자가 특정 콘텐츠를 여러 번 봤다면 단순히 그 콘텐츠를 좋아한다고 볼 수도 있지만, 그래프에서는 그 콘텐츠가 다루는 개념, 선수 개념, 인접 개념까지 확장해서 해석할 수 있다. 추천 시스템은 사용자가 본 콘텐츠와 직접 유사한 콘텐츠뿐 아니라, 사용자가 반복해서 막히는 개념의 선수 개념을 추천할 수도 있다.

RAG에서도 마찬가지다. 사용자의 질문을 Weaviate에서 검색한 뒤, Neo4j에서 사용자의 최근 활동 노드와 연결된 개념을 참고하면 검색 결과를 개인화하거나 문맥화할 수 있다. 같은 질문이라도 사용자가 최근에 어떤 개념을 학습했는지에 따라 우선 보여줄 근거 문서가 달라질 수 있다.

Weaviate와 Neo4j는 서로 다른 질문에 답한다#

Weaviate와 Neo4j를 함께 쓰는 시스템에서 흔히 생기는 혼란은 둘 다 “검색”에 쓰인다는 점이다. 하지만 두 저장소가 잘하는 질문은 다르다.

Weaviate는 텍스트와 의미 유사도에 강하다.

  • 이 질문과 의미가 비슷한 문서 청크는 무엇인가?
  • 이 콘텐츠와 설명이 비슷한 다른 콘텐츠는 무엇인가?
  • 키워드와 벡터 유사도를 함께 봤을 때 상위 후보는 무엇인가?

Neo4j는 관계와 경로에 강하다.

  • 이 개념의 선수 개념은 무엇인가?
  • 이 사용자가 최근 본 콘텐츠들은 어떤 개념으로 이어지는가?
  • 특정 개념을 설명하는 문서와 연습 콘텐츠는 어떻게 연결되는가?
  • 비슷한 활동 패턴을 가진 사용자는 어떤 콘텐츠로 이동했는가?

PostgreSQL은 정합성, 상태, 집계, 운영 조회에 강하다.

  • 이 콘텐츠는 현재 게시 가능한 상태인가?
  • 이 문서는 검수 완료되었는가?
  • 이 사용자는 이 리소스에 접근 권한이 있는가?
  • 최근 7일간 콘텐츠별 클릭률은 얼마인가?

따라서 통합 설계에서는 저장소별 책임을 명확히 해야 한다. Weaviate가 관계 추론까지 떠안거나, Neo4j가 긴 본문 semantic search까지 떠안거나, PostgreSQL이 모든 검색과 추천 로직을 직접 수행하려고 하면 각 저장소의 장점이 사라진다.

RAG에서는 검색 후보와 근거 경로를 분리한다#

RAG에 이 구조를 사용할 때는 후보 검색과 근거 확장을 분리해서 생각하는 편이 좋다.

사용자 질문이 들어오면 먼저 Weaviate에서 의미적으로 가까운 청크를 찾는다. 이때 BM25와 vector를 함께 쓰는 hybrid search가 유용하다. 도메인 용어가 정확히 포함된 문서는 BM25가 잡고, 표현은 다르지만 의미가 가까운 문서는 vector가 잡는다.

그 다음 Neo4j에서 검색된 청크와 연결된 문서, 개념, 선수 관계, 관련 콘텐츠를 확장한다. 이 단계는 “비슷한 문장 찾기”가 아니라 “찾은 근거가 지식 구조 안에서 어디에 위치하는지 확인하기”에 가깝다. 사용자의 최근 활동이 있다면 해당 사용자가 이미 본 콘텐츠, 자주 틀린 개념, 최근 질문한 개념도 함께 참고할 수 있다.

마지막으로 PostgreSQL에서 상태와 권한, 품질 정보를 확인한다. 검색으로는 높은 점수를 받은 청크라도 비공개 문서이거나 검수 전 데이터라면 답변 근거로 쓰면 안 된다. 추천에서도 마찬가지다. 후보로는 적절해 보여도 이미 완료한 콘텐츠이거나 노출 정책에 맞지 않으면 제외해야 한다.

이렇게 나누면 RAG 파이프라인이 더 설명 가능해진다.

질문
-> Weaviate: 의미적으로 가까운 후보 검색
-> Neo4j: 개념 관계와 사용자 맥락 확장
-> PostgreSQL: 상태, 권한, 품질 필터링
-> LLM: 근거 기반 응답 생성

검색 결과가 이상할 때도 어디서 문제가 생겼는지 분리해서 볼 수 있다. 임베딩이 문제인지, 그래프 관계가 문제인지, 권한 필터가 문제인지, 청킹 전략이 문제인지 추적할 수 있다.

추천에서는 후보 생성과 피처 보강을 나눈다#

추천 알고리즘에서도 같은 원칙이 적용된다. Weaviate, Neo4j, PostgreSQL이 모두 추천에 쓰일 수 있지만, 각 저장소가 맡는 역할은 다르게 잡는 편이 좋다.

Neo4j는 후보 생성에 특히 유용하다. 사용자가 본 콘텐츠에서 연결된 개념을 찾고, 그 개념과 인접한 콘텐츠를 후보로 가져올 수 있다. 사용자와 콘텐츠의 이분 그래프를 활용해 비슷한 사용자가 소비한 콘텐츠를 찾을 수도 있다. 지식 그래프가 있다면 단순히 “비슷한 콘텐츠”를 넘어 “현재 이해 수준에서 다음에 볼 만한 콘텐츠”를 찾는 데 쓸 수 있다.

Weaviate는 의미 유사 후보를 보강하는 데 유용하다. 사용자가 최근 읽은 문서나 질문한 내용과 의미적으로 가까운 콘텐츠를 찾거나, 새로 들어온 콘텐츠의 초기 추천 후보를 만들 수 있다. 활동 데이터가 부족한 신규 콘텐츠도 텍스트 임베딩이 있으면 추천 후보로 올릴 수 있으므로 콜드스타트에 도움이 된다.

PostgreSQL은 피처와 정책을 담당한다. 최근 활동 집계, 인기도, 완료 여부, 난이도, 노출 가능 상태, 실험군 정보 같은 값은 관계 탐색보다 테이블과 집계가 더 적합하다.

추천 요청
-> Neo4j: 사용자 활동 그래프와 지식 그래프 기반 후보 생성
-> Weaviate: 의미 유사 후보 추가
-> PostgreSQL: 집계 피처, 정책, 상태 필터 적용
-> Ranking: 점수화와 재정렬

이 구조에서 중요한 것은 추천 결과를 다시 활동 데이터로 되돌려 보내는 것이다. 어떤 후보가 노출되었고, 사용자가 무엇을 클릭했고, 무엇을 무시했는지 기록해야 다음 추천이 개선된다. 추천 시스템은 단방향 파이프라인이 아니라 피드백 루프다.

비정기 배치는 예외가 아니라 지식 베이스 갱신 이벤트다#

많은 시스템에서 비정기 배치는 예외처럼 취급된다. 누군가 파일을 올리면 임시 스크립트를 돌리고, 결과를 특정 테이블에 덮어쓴다. 처음에는 빠르게 해결되는 것처럼 보이지만, 시간이 지나면 추적 불가능한 보정과 재처리 실패가 쌓인다.

정적 데이터가 지식 베이스를 구성하는 시스템에서는 비정기 배치를 더 엄격하게 다뤄야 한다. 하나의 배치 인입은 PostgreSQL 한 곳만 바꾸는 작업이 아니라, Weaviate의 검색 공간과 Neo4j의 관계 구조를 동시에 바꾸는 작업이다. 즉 배치 파일 하나는 지식 베이스 갱신 이벤트다.

이때 필요한 것은 작업 단위의 상태 관리다.

단계확인할 것
수신파일 또는 원천 데이터가 정상적으로 들어왔는가
검증필수 필드, 타입, 참조 키, 중복, 삭제 대상이 유효한가
표준화공통 엔티티와 속성으로 변환되었는가
PostgreSQL 적재기준 테이블과 메타데이터가 갱신되었는가
Weaviate 적재청크와 임베딩 객체가 올바른 버전으로 반영되었는가
Neo4j 적재노드와 엣지가 중복 없이 병합되었는가
검산저장소 간 엔티티 수, 활성 상태, 참조 무결성이 맞는가

중간 단계에서 실패하면 전체 상태가 어긋날 수 있다. PostgreSQL에는 새 문서가 있는데 Weaviate에는 임베딩이 없거나, Neo4j에는 개념 관계가 있는데 원본 문서가 비활성화된 상태가 될 수 있다. 따라서 배치 작업은 성공 또는 실패만 기록하지 말고, 저장소별 반영 상태를 따로 기록해야 한다.

충돌 해결 규칙을 명시한다#

통합 파이프라인의 어려움은 데이터가 들어오는 것이 아니라 서로 다른 데이터가 같은 사실에 대해 다른 값을 말할 때 시작된다.

예를 들어 정적 배치에서 콘텐츠의 태그가 바뀌었는데, 활동 그래프에는 사용자가 기존 태그 기반 추천을 통해 클릭한 이력이 남아 있을 수 있다. 문서가 비활성화되었는데 Weaviate에는 이전 청크가 검색될 수도 있다. 개념 관계가 수정되었는데 추천 알고리즘이 이전 그래프 경로를 캐시하고 있을 수도 있다.

이때 필요한 것은 암묵적인 덮어쓰기가 아니라 명시적인 충돌 해결 규칙이다.

  • 정적 데이터의 새 버전이 들어오면 이전 Weaviate 객체를 삭제할지 비활성화할지
  • Neo4j의 기존 엣지를 끊을지, 유효 기간을 닫을지
  • 활동 이력이 참조하던 콘텐츠가 비활성화되면 추천과 RAG에서 어떻게 제외할지
  • 사용자의 과거 활동을 새 개념 체계로 재매핑할지
  • 배치 갱신 중 RAG와 추천 요청은 이전 버전을 볼지, 부분 갱신 상태를 볼지

특히 그래프에서는 삭제가 조심스럽다. 정적 노드를 물리 삭제하면 과거 활동 엣지가 매달릴 곳을 잃을 수 있다. 따라서 운영 시스템에서는 물리 삭제보다 active, valid_from, valid_to, version 같은 속성으로 유효성을 관리하는 편이 안전한 경우가 많다.

집계와 임베딩은 재계산 가능해야 한다#

라이브와 배치가 섞이는 시스템에서 집계를 단순 누적으로만 처리하면 곧 한계에 부딪힌다. 늦게 들어온 이벤트, 중복 제거, 콘텐츠 비활성화, 개념 체계 변경이 발생하면 이미 계산된 피처와 추천 후보를 바꿔야 하기 때문이다.

따라서 중요한 산출물은 재계산 가능해야 한다.

PostgreSQL의 사용자별 피처 테이블은 특정 기간이나 특정 사용자 집합만 다시 계산할 수 있어야 한다. Neo4j의 사용자 활동 관계도 이벤트 원천에서 다시 만들 수 있어야 한다. Weaviate의 임베딩 객체는 청킹 전략이나 임베딩 모델이 바뀌었을 때 재생성할 수 있어야 한다.

이때 모든 것을 매번 전체 재처리하는 방식은 운영 비용이 크다. 그래서 재처리 범위를 작게 만들 수 있는 메타데이터가 필요하다.

  • 어떤 배치 파일이 어떤 문서와 콘텐츠를 바꿨는가
  • 어떤 문서 변경이 어떤 청크와 임베딩을 바꿨는가
  • 어떤 콘텐츠 변경이 어떤 그래프 노드와 엣지를 바꿨는가
  • 어떤 사용자 활동 집계가 어떤 기간의 이벤트에 의존하는가

결국 통합 파이프라인의 운영성은 “다시 계산할 수 있는가”와 “작게 다시 계산할 수 있는가”에서 갈린다.

운영 관점에서는 관측 가능성이 설계의 일부다#

이런 구조는 저장소가 여러 개라서 관측 가능성이 없으면 금방 불투명해진다. 파이프라인은 성공했는데 RAG 검색 결과가 이상하거나, 추천 후보가 갑자기 줄거나, 그래프에는 노드가 있는데 벡터 검색에는 나오지 않는 문제가 생길 수 있다.

최소한 다음 지표는 관리하는 편이 좋다.

  • 원천별 수집 지연 시간
  • 배치 파일별 처리 상태와 커버리지
  • PostgreSQL, Weaviate, Neo4j의 엔티티 수 차이
  • Weaviate 임베딩 생성 실패 건수
  • Neo4j 노드·엣지 병합 건수와 중복 제거 건수
  • 활동 이벤트의 late arrival 건수
  • 사용자 활동 엣지 생성 실패 건수
  • RAG 검색 결과의 empty result 비율
  • 추천 후보 생성 실패율
  • 주요 콘텐츠나 개념의 비정상적인 노출 감소

특히 “배치 작업이 성공했다”와 “지식 베이스가 서빙 가능한 상태가 되었다”는 다르다. PostgreSQL 적재가 성공했더라도 임베딩 생성이 실패하면 RAG에서 검색되지 않는다. Neo4j 적재가 누락되면 GraphRAG와 그래프 추천에서 경로가 끊긴다. 따라서 최종 상태는 저장소별 성공 여부가 아니라 서빙 관점의 readiness로 판단해야 한다.

피해야 할 설계#

가장 피해야 할 것은 각 파이프라인이 각 저장소를 제각각 갱신하는 구조다. 배치 작업이 PostgreSQL을 갱신하고, 별도의 스크립트가 Weaviate를 갱신하고, 또 다른 작업이 Neo4j를 갱신하는데 이 셋 사이의 공통 작업 ID와 버전이 없다면 어느 순간부터 결과를 설명할 수 없게 된다.

두 번째는 Weaviate와 Neo4j를 단순 캐시처럼 취급하는 것이다. 검색과 추천이 실제로 그 저장소를 바라본다면, 그 저장소의 데이터 모델과 갱신 정책도 1급 설계 대상이어야 한다. “나중에 다시 밀어 넣으면 된다”는 식으로 운영하면 장애 시점에 무엇을 다시 밀어 넣어야 하는지 알 수 없다.

세 번째는 활동 데이터를 원천 이벤트 없이 집계만 남기는 것이다. 추천 피처는 시간이 지나며 바뀐다. 이벤트 타입 정의가 바뀌거나, 봇 필터링 기준이 바뀌거나, 세션 기준이 바뀌면 과거 집계를 다시 만들 수 있어야 한다. 원천 이벤트가 없으면 잘못된 집계를 고칠 수 없다.

마지막으로 RAG와 추천의 데이터 기준을 분리하지 않는 것도 위험하다. RAG는 답변 근거의 정확성과 권한이 중요하고, 추천은 후보 다양성, 최신성, 사용자 반응이 중요하다. 둘 다 같은 Weaviate와 Neo4j를 쓰더라도 필터링 기준과 랭킹 기준은 다르게 설계해야 한다.

정리#

라이브 활동 데이터와 비정기 배치 파이프라인을 통합한다는 것은 모든 데이터를 같은 방식으로 처리한다는 뜻이 아니다. 오히려 서로 다른 데이터의 성격을 인정하고, 그 차이를 명시적인 계약과 저장소별 책임으로 흡수하는 일에 가깝다.

정적 데이터는 지식 베이스를 구성한다. PostgreSQL은 정합성과 운영 메타데이터를 관리하고, Weaviate는 의미 검색을 위한 벡터 공간을 만들고, Neo4j는 개념과 콘텐츠의 관계 구조를 만든다. 활동 데이터는 이 지식 베이스 위에 사용자 활동 레이어를 추가한다. 그리고 이 통합된 구조는 RAG의 검색 근거가 되기도 하고, 추천 알고리즘의 후보 생성과 피처 보강에 쓰이기도 한다.

좋은 통합 설계는 다음 원칙을 가진다.

  • 원천 데이터는 보존한다.
  • 정적 데이터는 지식 베이스의 버전으로 관리한다.
  • 활동 데이터는 사용자 활동 그래프 레이어로 모델링한다.
  • PostgreSQL, Weaviate, Neo4j의 책임을 분리한다.
  • 각 저장소의 스키마는 canonical entity 기준으로 함께 맞춘다.
  • 저장소 간 공통 식별자와 작업 메타데이터를 둔다.
  • RAG에서는 의미 검색과 그래프 확장, 상태 필터링을 분리한다.
  • 추천에서는 후보 생성과 피처 보강, 랭킹을 분리한다.
  • 집계, 임베딩, 그래프 투영은 재계산 가능하게 만든다.
  • 관측 가능성을 파이프라인의 일부로 설계한다.

결국 데이터 파이프라인의 품질은 “얼마나 빨리 처리하는가”만으로 결정되지 않는다. 더 중요한 것은 늦게 도착한 정적 데이터, 계속 쌓이는 활동 데이터, 그리고 여러 저장소에 투영된 데이터가 시간이 지나며 얼마나 일관된 검색·추천 기반으로 수렴하는가이다. RAG와 추천 시스템을 위한 파이프라인이라면 더더욱 그렇다. 데이터는 단순히 저장되는 것이 아니라, 사용자의 질문과 다음 행동을 결정하는 시스템의 작동 방식 그 자체가 된다.