검색어 자동완성 API 최적화 - 11만 개 데이터에서 100ms 내에 응답
(velog.io)프로젝트 소개
- NewCodes는 기업 기술 블로그 큐레이팅 서비스
- Spring Boot + PostgreSQL 아키텍처
- 검색어 자동완성 기능 구현: Term 기반 추천, 자모 분리 검색, 초성 검색, 기업 페이지 추천
성능 문제 발견
- Term 테이블에 11만 개 데이터 축적
- API 응답 시간이 1000ㅡ 이상으로 증가
- 목표: 100ms 이내 응답
1차 시도: 인덱스 추가 (1000ms → 700ms)
-
varchar_pattern_ops를 사용한 LIKE 접두사 검색 최적화 인덱스 생성 -
CONCURRENTLY옵션으로 서비스 중단 없이 인덱스 생성 - term, decomposed_term, chosung 컬럼에 각각 인덱스 적용
2차 시도: LOWER 함수 인덱스 (700ms → 110ms)
-
LOWER()함수 사용으로 인한 풀스캔 문제 발견 - 함수 기반 인덱스(Functional Index) 생성
-
LOWER(컬럼명) varchar_pattern_ops형태로 인덱스 재구성
3차 시도: JOIN → EXISTS (110ms → 100ms)
- Corporation과 Article의 INNER JOIN이 성능 병목
- EXISTS 서브쿼리로 변경하여 스캔 범위 축소
- "데이터 존재 여부"만 확인하도록 최적화
4차 시도: 비정규화 & 커버링 인덱스 (100ms → 90ms)
-
total_frequency컬럼 추가로 집계 연산 제거 - GROUP BY, SUM 연산을 미리 계산된 값으로 대체
- 커버링 인덱스로 I/O 횟수 감소
-
INCLUDE절로 term과 total_frequency를 인덱스에 포함
5차 시도: JDBC Template (90ms → 80ms)
- JPA/Hibernate 오버헤드 제거
- JDBC Template으로 직접 쿼리 실행
- 단순 조회에서는 ORM 레이어 생략이 효과적
Nginx Rate Limiting 문제 해결
- 초기 설정: 1초에 2회 제한, burst 10
- 100ms 디바운싱으로 인한 요청 실패 발생
- 개선: 1초에 10회 허용, burst 20으로 변경
- 444 → 429 status code 변경
응답 데이터 크기 축소
- JSON 필드명 제거, 배열 기반 응답으로 변경
- 타입을 숫자로 구분 (0: Corporation, 1: Theme, 2: Term)
- 네트워크 전송 시간 감소
CompletableFuture 병렬 처리
- Corporation, Theme, Term 조회를 독립적으로 동시 실행
- 순차 실행 대비 최대 응답 시간만큼만 소요
- ExecutorService와 예외 처리 추가
최종 성과
- 초기 1000ms → 최종 80ms (개발 서버), 40ms (운영 서버)
- 약 90% 이상 성능 개선
주요 학습 내용
- 문제 정의와 방향성 설정의 중요성
- AI 활용과 개발자의 검수 균형
- 전체 아키텍처 관점의 설계 필요
- 인덱스 종류 선택: 단일/복합/커버링 인덱스
- 함수 사용 시 인덱스 무효화 주의
- JPA 내부 동작 이해
- EXPLAIN을 통한 쿼리 실행 계획 분석
향후 개선 방향
- Trie 자료구조 사용
- 자주 검색되는 용어 캐싱
- CDN 활용 (글로벌 서비스 시)
lower() 인덱스 대신 GIN 인덱스를 사용해보는건 고려해보셨을지 궁금하네요. 어차피 jdbctemplate로 raw sql 쓰셨으니 이 참에 FTS는 어떠신가요?
CompletableFuture.supplyAsync() 를 사용한 비동기 방식도 별도 ExecutorService를 지정하지 않는다면 forkjoinpool의 commonpool을 사용하니
리퀘스트 스레드 대신 사용하는 commonpool이 가득 차는 정도까지(cpu 코어 - 1개) 동시접속자가 많아지면 감당하지 못할 수도 있습니다.
이 부분은 reactive 방식으로 변경하거나 jvm 버전을 올려서 가상 스레드를 도입하는 편이 깔끔하게 해결할 수 있을 겁니다.
안녕하세요! 먼저 피드백 댓글 달아주셔서 정말 감사합니다.
GIN 인덱스는 해당 경우에서 필요하지 않다고 판단했습니다! 현재 검색어 자동완성 추천 API에서는 term 자체만 필요합니다. 해당 term이 어떤 article들에 속하는지는 필요하지 않습니다.
반면에, 검색 API에서는 GIN 인덱스와 유사한 인덱스를 사용하고 있습니다. postgres의 extension인 paradeDB를 활용해서 BM25 인덱스를 사용합니다.
포스팅에는 자세히 안 나와있지만 현재는 ExecutorService를 별도로 지정해서 사용하고 있습니다. 다만 말씀해주신 것처럼 reactive 방식이나 가상 스레드도 추후 고려해보겠습니다!!
블로그도 가서 원문도 읽어보고 왔습니다. 제목과 실제내용 사이 괴리가 좀 느껴지네요. 구현하신 기능이나 개선한 방향 등은 이미 기존에 존재하는 여러 오픈소스들에 구현, 반영되있는 부분이고, 작업하신 건 본인 서비스에서 최초 단순하게 기능 구현해뒀던 검색을 고도화한 부분인데 제목만 보면 알고리즘 대대적 개선한 것 같은 느낌이,, 이전글도 홍보로 flagged 되셨던데 작성 하실때 조금더 고민이 필요하시지 않을까 싶네요.