검색 엔진 내재화 PoC
트리거 기반 증분 색인으로 상품 변경의 검색 반영 주기를 하루 → 1분으로 단축
문제와 배경
- 기존 외부 검색 서비스는 매일 새벽 상품을 한꺼번에 받아 학습해, 오늘 바꾸거나 새로 등록한 상품이 다음 날 오전 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에 남겼다가 언제 다시 반영해도 순서와 상관없이 새 인덱스가 최신이 됨
- 전체 색인 상태 행을 먼저 커밋하고 새 인덱스에 전체 상품 적재
- 그동안 증분 색인은 서비스 인덱스에 계속 반영하고 반영한 상품 id를 backlog에 기록
- 적재가 끝나면 alias(검색 요청이 가리키는 인덱스 별칭)를 새 인덱스로 전환
- 상태 행을 지워 전체 색인을 끝냄. 이제 증분 색인은 새 인덱스에 씀
- 다음 증분 실행이 backlog를 변경 기록 테이블로 옮겨 같은 실행에서 새 인덱스에 반영
- 전체 색인 시작 때 상태 행이 아직 안 보여 backlog 없이 반영된 변경은, 전체 적재가 상태 행 커밋 뒤에 DB를 읽기 시작하므로 적재분에 이미 들어 있음. 상태 행을 본 뒤 반영한 상품은 모두 backlog에 남음
결과
- 상품 변경 반영이 다음 날 오전 9시 → 1분 주기로 바뀌어 새로 등록한 상품도 다음 증분 색인에서 바로 검색됨
- 수동 SQL, 다건 UPDATE, hard delete, 가격과 행사 시간경계까지 모두 변경 기록 테이블을 거쳐 반영됨(최종적 일관성)
- 전체 색인과 겹쳐도 변경을 놓치지 않고, 전체 색인 중에도 서비스 인덱스에 1분 주기로 반영됨