검색 엔진 내재화 PoC

트리거 기반 증분 색인으로 상품 변경의 검색 반영 주기를 하루 → 1분으로 단축

평소 1분 주기 증분 색인 흐름 전체 색인 중 backlog 기록과 alias 전환 흐름

문제와 배경

  • 기존 외부 검색 서비스는 매일 새벽 상품을 한꺼번에 받아 학습해, 오늘 바꾸거나 새로 등록한 상품이 다음 날 오전 9시에야 검색에 반영됨
  • 내재화 PoC에서 증분 색인과 전체 색인을 직접 설계하고 구현하기로 함. 다만 놓치면 안 되는 변경이 여러 갈래였음
    • 쓰기 경로가 JPA, 벌크 JPQL, MyBatis, 수동 SQL, 긴급 DB 직접 수정으로 흩어져 있고, hard delete는 흔적이 남지 않음
    • 가격과 행사는 DB 쓰기 없이 시작과 종료 시각이 지나는 것만으로 값이 바뀜
    • 전체 색인은 새 인덱스를 만들어 바꿔 끼우는데, 새 인덱스는 적재를 시작한 시점의 DB를 읽어 적재 중 바뀐 상품은 전환 순간 예전 상태로 돌아감

해결 과정

변경 감지

후보장점단점과 제약판단
updated_at 워터마크 스캔(쿼리 기반 CDC)원천 변경 없음늦게 커밋된 변경 누락. MyBatis와 수동 SQL은 updated_at 갱신 보장 없음. hard delete 흔적 없음
앱 이벤트나 JPA 엔티티 리스너코드에서 의도가 명확발행 지점이 흩어져 하나만 빠져도 누락. 리스너는 영속성 컨텍스트를 거치지 않는 벌크 JPQL, MyBatis, 수동 SQL을 못 잡음
Debezium(로그 기반 CDC)WAL만 읽어 쓰기 경로에 부하 없이 커밋된 변경과 hard delete 모두 포착Kafka Connect 등 새 인프라 도입 + 팀에 운영 경험 없음
PG 트리거 → 변경 기록 테이블(트리거 기반 CDC)쓰기 경로와 무관하게 커밋된 변경과 hard delete 모두 포착. 값이 그대로인 UPDATE 제외 같은 적재 규칙을 트리거 SQL로 제어쓰기마다 트리거 실행 비용채택
  • 트리거 비용은 pgbench 초당 100건 쓰기에서 트리거 유무 지연 차이가 노이즈 이내라 채택
  • 트리거는 변경 내용 없이 (대상 종류, id)만 append-only로 쌓음. 카테고리와 행사는 한 건이 수만 상품에 걸치므로 쓰기 트랜잭션에서는 상품 id로 바꾸지 않음
  • 증분 색인 Job은 1분마다 카테고리와 행사 같은 상품 외 변경을 조인으로 영향받는 상품 id로 전개(fan-out)하고, 상품 단위로 묶어(GROUP BY) 읽은 뒤 상품마다 DB 현재 상태로 문서를 다시 조립해 upsert함. 변경 내용 재생이 아니라 현재 상태 덮어쓰기라 재처리와 순서 뒤바뀜에 멱등. 읽은 행만 지우므로 처리 중 들어온 변경은 새 행으로 남고, 실패해도 행이 남아 다음 실행에서 재시도됨(at-least-once)
  • 트리거가 못 잡는 가격 및 행사의 시작과 종료는 직전 스캔 이후 경계 시각이 지난 행을 찾아 같은 테이블에 쌓음(시간경계 스캔). 경계 시각은 저장값이라 updated_at 워터마크와 달리 늦은 커밋에 영향받지 않음

전체 색인과 겹칠 때

후보장점단점과 제약판단
전체 색인 중 증분 중단단순전체 색인 동안 검색 결과가 시작 시점에 멈춤
이전과 새 인덱스 dual write전환 즉시 최신두 인덱스 쓰기 순서 경합으로 덮어쓰기 또는 삭제 문서 부활 가능성
backlog 기록 후 전환 뒤 증분 색인으로 반영서비스 인덱스 최신 유지 + 유실 없음. 증분 경로 하나로 처리전환 직후 다음 증분 실행까지 새 인덱스가 적재 시작 시점 상태채택
  • 증분이 현재 상태를 덮어쓰는 멱등 방식이라, 전체 색인 중 반영한 상품 id만 backlog에 남겼다가 언제 다시 반영해도 순서와 상관없이 새 인덱스가 최신이 됨
  1. 전체 색인 상태 행을 먼저 커밋하고 새 인덱스에 전체 상품 적재
  2. 그동안 증분 색인은 서비스 인덱스에 계속 반영하고 반영한 상품 id를 backlog에 기록
  3. 적재가 끝나면 alias(검색 요청이 가리키는 인덱스 별칭)를 새 인덱스로 전환
  4. 상태 행을 지워 전체 색인을 끝냄. 이제 증분 색인은 새 인덱스에 씀
  5. 다음 증분 실행이 backlog를 변경 기록 테이블로 옮겨 같은 실행에서 새 인덱스에 반영
  • 전체 색인 시작 때 상태 행이 아직 안 보여 backlog 없이 반영된 변경은, 전체 적재가 상태 행 커밋 뒤에 DB를 읽기 시작하므로 적재분에 이미 들어 있음. 상태 행을 본 뒤 반영한 상품은 모두 backlog에 남음

결과

  • 상품 변경 반영이 다음 날 오전 9시 → 1분 주기로 바뀌어 새로 등록한 상품도 다음 증분 색인에서 바로 검색됨
  • 수동 SQL, 다건 UPDATE, hard delete, 가격과 행사 시간경계까지 모두 변경 기록 테이블을 거쳐 반영됨(최종적 일관성)
  • 전체 색인과 겹쳐도 변경을 놓치지 않고, 전체 색인 중에도 서비스 인덱스에 1분 주기로 반영됨