지금 운영 중인 도서관 관리 시스템에서 도서 검색은 LIKE '%키워드%' 한 줄로 처리하고 있습니다. 제목·저자에 부분 일치하는 도서를 찾는 가장 단순한 방식이고, 도서가 몇백 권일 때는 아무 문제가 없었습니다.
문제는 규모입니다. 제가 근무하는 도서관만 해도 등록된 도서가 1만 권을 넘고, 주기적으로 계속 들어옵니다. "이 검색이 앞으로도 버틸까?"라고 자문하자마자 답은 뻔했습니다. LIKE '%...%'는 앞에 와일드카드가 붙는 순간 어떤 인덱스도 탈 수 없고, 결국 테이블 전체를 훑습니다. 여기까지는 누구나 아는 이야기입니다.
그래서 다음 수순도 뻔해 보였습니다. "검색은 Elasticsearch로 넘기자." 실제로 많은 서비스가 그렇게 합니다. 그런데 이걸 "요즘 다들 쓰니까", "빠르다니까"로 도입하고 싶지는 않았습니다. 개인 프로젝트인 만큼, 얼마나 빨라졌는지 직접 재보면서 넘어가고 싶었습니다.
결정적인 계기는 『Real MySQL 8.0』을 읽다가 RDB에도 역색인(FULLTEXT 인덱스)이 있다는 걸 확인한 순간이었습니다. 역색인이 ES만의 것이 아니라면, 왜 다들 굳이 검색 엔진을 별도로 두는 걸까요? 정말 RDB의 역색인만으로는 안 되는 걸까요? 이 글은 그 질문에 직접 답을 내보는 과정입니다. 순서는 이렇게 잡았습니다.
본문에 계속 나올 용어를 먼저 간단히 정리하겠습니다.
비교를 하려면 먼저 "느려질 만한" 규모의 데이터가 있어야 합니다. 규모가 작으면 엔진 간 차이가 측정 노이즈에 묻혀버려서, 무엇을 재든 결론을 낼 수 없기 때문입니다.
규모는 실제 환경에서 역산했습니다. 제가 근무하는 도서관이 도서 1만 건, 소장본 1.2만 건 정도입니다. 한 칸짜리 작은 도서관이 이 정도니까, 책이 꾸준히 들어오는 사내 도서관을 가정해 열 배로 잡았습니다. Book 10만 건, BookItem(소장본) 12만 건, Loan(대출) 25만 건, 합계 47만 건입니다. 책만 늘리지 않고 소장본·대출 이력까지 함께 늘린 건, 실제 서비스라면 도서가 늘어날 때 연관 데이터도 같이 늘어나기 때문입니다.
처음 떠오른 건 "그냥 JPA saveAll로 넣지 뭐"였습니다. 그런데 47만 건을 엔티티로 하나하나 만들어 영속화하면 Hibernate의 1차 캐시·dirty checking 오버헤드가 그대로 쌓입니다. SQL 프로시저도 고민했지만 이쪽은 다른 문제가 있었습니다. 소장본 관리번호는 (주제)-(번호) 정규식을 지켜야 하고 반납 기한은 대출일 + 14일로 계산되는데, 이 규칙이 이미 엔티티 코드에 있습니다. SQL로 또 짜면 같은 규칙을 두 군데서 관리하게 됩니다.
그래서 순수 JDBC batch insert를 쓰되, 엔티티에 있는 상수·정규식(BookItem.MANAGEMENT_NUMBER_REGEX, Loan.LOAN_PERIOD_DAYS)은 그대로 가져다 썼습니다. rewriteBatchedStatements=true 옵션으로 배치를 묶어 보낸 덕분에 47만 건 적재에 12초 정도 걸렸습니다.