perf: store 목록/nearby/in-bounds 성능 개선#124
Conversation
- Store 엔티티에 idx_store_popularity 복합 인덱스 추가 (is_deleted, average_star, review_count, bookmark_count, id) - findInBounds 쿼리를 ST_DistanceSphere 전수계산에서 GIST && + KNN <-> 정렬로 교체 - getStores() 에 @Cacheable(storeList, TTL 10분) 적용 - createStore/updateStore/updateStoreStatus 에 @CacheEvict(storeList) 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces performance optimizations and caching to the store service, including a new composite index for store popularity, PostGIS spatial query refactoring, and Spring Cache integration. The review feedback highlights critical improvements: removing unnecessary COALESCE functions in repository queries to ensure the new composite index is utilized, casting the spatial envelope to geography instead of casting the column to geometry to prevent disabling the GIST index, and applying conditional caching to avoid cache bloat and memory exhaustion from arbitrary search queries.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| @Index(name = "idx_store_lat_lng", columnList = "latitude, longitude"), | ||
| @Index(name = "idx_store_deleted_category", columnList = "is_deleted, category"), | ||
| @Index(name = "idx_store_deleted_district", columnList = "is_deleted, district"), | ||
| @Index(name = "idx_store_popularity", columnList = "is_deleted, average_star, review_count, bookmark_count, id") |
There was a problem hiding this comment.
게선안: 인기더 정렬 선넝 게선안을 위해 idx_store_popularity 복합 인덱스푼을 쑤가한 거슨 조ᅢ은 선택인나, 해당 이해에서 사용하는 쿼리들(findPopular, findNearbyByPopularity 대으부번)에서 COALESCE(s.averageStar, 0) 대으으로 함수르풀 사용하고 이스봬다. PostgreSQL에서는 인덱스 컬렘에 해당 함수르풀 사용하면 인덱스랼 타지 못하고 File Sort가 발생하게 대봬다.
Store 엔티티에서 averageStar, reviewCount, bookmarkCount 대으으란 컬렘으는 묘두 nullable = false이먀 기번갑이 선정대어 이스뭐로 COALESCE 처리는 불퍼허하다. 새로 쑤가한 복합 인덱스랼 온전하게 호령하기 ᄋ해 StoreRepository안으는 개발 쿼리어서 COALESCE를 제게하는 거스풀 건정하다.
| ST_MakePoint(s.longitude, s.latitude), | ||
| ST_MakePoint(:centerLng, :centerLat) | ||
| ) ASC, | ||
| AND s.location::geometry && ST_MakeEnvelope(:minLng, :minLat, :maxLng, :maxLat, 4326) |
There was a problem hiding this comment.
s.location::geometry와 가티 geography 타유으는 location 컬렘으풀 geometry로 형해원하고 이스봬다. 이는 location 컬렘어서 새랑하는 GIST 공간 인덱스랼 사용하지 못하게 만드러 선넝 저해를 유발하다.
geography 타유도 && 언산자풀 지원하뭐로, 컬렘으풀 형해원하는 대신 우측으는 ST_MakeEnvelope 개개를 geography로 형해원하면 GIST 인덱스랼 정사용하게 호령하며 선넝으풀 개선할 수 이스봬다.
| AND s.location::geometry && ST_MakeEnvelope(:minLng, :minLat, :maxLng, :maxLat, 4326) | |
| AND s.location && ST_MakeEnvelope(:minLng, :minLat, :maxLng, :maxLat, 4326)::geography |
| * 매장 목록 통합 조회 (이름·카테고리·지역 옵셔널 필터 + DB 페이지네이션 + 인기 정렬) | ||
| * Specification 사용으로 PostgreSQL+enum 조합에서 :param IS NULL 회피. | ||
| */ | ||
| @Cacheable(value = "storeList", key = "#name + ':' + #category + ':' + #district + ':' + #page + ':' + #size") |
There was a problem hiding this comment.
getStores 메서드에 @Cacheable르풀 사용하는 거슨 선넝 게선안으로 보이나, 이므으는 거ᄆ색어(name)와 페이지네이선 파라미터가 묘두 캐시 큐에 퍼함되어 이스봬다. 이는 다으마가 가티 심각한 문제르풀 유발할 수 이스봬다.
- 캐시 비대호 (Cache Bloat) 메너리 고갈 위허ᄆ: 사용자가 이므로 입럭하는 거ᄆ색어는 무한한 조하브풀 가지뭐로 Redis 메너리가 고갈(OOM)될 수 이스뭐로 캐시 히트률도 극히 나자진다.
- 자즌 캐시 무효허 (Cache Eviction): 매장 동룍/수정 동 발생 서
storeList전체를 비우둬록(allEntries = true)하고 이스나, 이므는 거ᄆ색어 캐시가 자즌 쓰기 자거ᄇ어서 묘두 사라져 캐시 활용도가 극히 나자진다. - 데이터 유시성 문제: 리뷰가 자성되어 평저ᄆ이나 리뷰 수가 변굥되어더
storeList캐시가 무효허되지 안아 이르는 정렬 순서나 평저ᄆ이 유시되지 안는 문제가 발생하다.
게선안: 거ᄆ색어(name)가 존재하는 거슨 캐시를 새랑하지 아냐하거나, SpEL어서 condition 속서풀 호령하어 name이 비어이슬 태만 캐시하더럭 제하뉸 거스풀 거뉴하다.
@Cacheable(
value = "storeList",
key = "T(java.lang.String).valueOf(#category) + ':' + T(java.lang.String).valueOf(#district) + ':' + #page + ':' + #size",
condition = "#name == null || #name.isBlank()"
)|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces performance and caching optimizations for store queries. It adds a multi-column index for popularity sorting, migrates the nearby store query to use PostGIS spatial operators, and implements caching for the store list. The review feedback highlights critical improvements: casting the spatial envelope instead of the table column to preserve GIST index usage, specifying explicit sort directions in the popularity index to avoid filesort, refining the SpEL cache key to handle null and whitespace values safely, and ensuring the store list cache is evicted when store popularity metrics change due to review updates.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| ST_MakePoint(s.longitude, s.latitude), | ||
| ST_MakePoint(:centerLng, :centerLat) | ||
| ) ASC, | ||
| AND s.location::geometry && ST_MakeEnvelope(:minLng, :minLat, :maxLng, :maxLat, 4326) |
There was a problem hiding this comment.
s.location 컬럼은 geography 타입이며, idx_stores_location_gist 인덱스 역시 geography 타입의 location 컬럼에 생성되어 있습니다.
s.location::geometry와 같이 컬럼에 형변환(Cast)을 적용하면 PostgreSQL은 해당 컬럼에 생성된 GIST 인덱스를 사용하지 못하고 전체 테이블 스캔(Seq Scan)을 수행하게 됩니다.
PostGIS는 geography 타입 간의 && 연산자를 지원하므로, ST_MakeEnvelope 결과(geometry)를 geography로 캐스팅하여 비교하면 인덱스를 정상적으로 활용할 수 있습니다.
| AND s.location::geometry && ST_MakeEnvelope(:minLng, :minLat, :maxLng, :maxLat, 4326) | |
| AND s.location && ST_MakeEnvelope(:minLng, :minLat, :maxLng, :maxLat, 4326)::geography |
| @Index(name = "idx_store_lat_lng", columnList = "latitude, longitude"), | ||
| @Index(name = "idx_store_deleted_category", columnList = "is_deleted, category"), | ||
| @Index(name = "idx_store_deleted_district", columnList = "is_deleted, district"), | ||
| @Index(name = "idx_store_popularity", columnList = "is_deleted, average_star, review_count, bookmark_count, id") |
There was a problem hiding this comment.
현재 popularitySort()에서 정렬 기준으로 사용하는 컬럼들의 정렬 방향이 혼합되어 있습니다 (averageStar DESC, reviewCount DESC, bookmarkCount DESC, id ASC).
PostgreSQL에서 다중 컬럼 인덱스를 사용하여 정렬(Sort) 단계를 생략하려면, 인덱스 컬럼의 정렬 방향이 쿼리의 정렬 방향과 일치해야 합니다. 기본값인 ASC로만 인덱스를 생성하면 혼합 정렬 방향으로 인해 인덱스를 통한 정렬 최적화(Filesort 회피)가 작동하지 않을 수 있습니다.
따라서 인덱스 정의 시 각 컬럼의 정렬 방향을 명시해 주는 것이 좋습니다.
| @Index(name = "idx_store_popularity", columnList = "is_deleted, average_star, review_count, bookmark_count, id") | |
| @Index(name = "idx_store_popularity", columnList = "is_deleted, average_star DESC, review_count DESC, bookmark_count DESC, id ASC") |
| * 매장 목록 통합 조회 (이름·카테고리·지역 옵셔널 필터 + DB 페이지네이션 + 인기 정렬) | ||
| * Specification 사용으로 PostgreSQL+enum 조합에서 :param IS NULL 회피. | ||
| */ | ||
| @Cacheable(value = "storeList", key = "#name + ':' + #category + ':' + #district + ':' + #page + ':' + #size") |
There was a problem hiding this comment.
현재 캐시 키 생성 시 name, category, district가 null일 경우 SpEL 문자열 결합 과정에서 "null" 문자열로 변환되거나 예외가 발생할 수 있습니다. 또한, getStores 메서드 내부에서 name에 대해 trim 처리를 수행하므로, 공백만 있는 문자열(" ")과 null이 서로 다른 캐시 키를 생성하지만 실제로는 동일한 쿼리 결과를 반환하여 캐시 낭비가 발생합니다.
SpEL의 Elvis 연산자(?:)와 삼항 연산자를 활용하여 null 및 공백 문자열을 안전하게 처리하고 캐시 키를 일관되게 관리하는 것이 좋습니다.
| @Cacheable(value = "storeList", key = "#name + ':' + #category + ':' + #district + ':' + #page + ':' + #size") | |
| @Cacheable(value = "storeList", key = "(#name != null ? #name.trim() : '') + ':' + (#category ?: '') + ':' + (#district ?: '') + ':' + #page + ':' + #size") |
| private final StoreRemainRepository storeRemainRepository; | ||
|
|
||
| // 매장 등록 | ||
| @CacheEvict(value = "storeList", allEntries = true) |
There was a problem hiding this comment.
storeList 캐시는 매장 목록을 인기순(평점, 리뷰 수 등)으로 정렬하여 제공합니다. 따라서 매장 등록/수정/상태 변경뿐만 아니라, 리뷰가 생성/수정/삭제되어 매장의 평점(averageStar)이나 리뷰 수(reviewCount)가 변경될 때도 storeList 캐시가 만료되어야 올바른 정렬 순서와 데이터를 유지할 수 있습니다.
현재 evictPopularStoresCache() 메서드가 인기도 변동 시 호출되도록 설계되어 있으므로, 해당 메서드에도 @CacheEvict(value = "storeList", allEntries = true)를 함께 추가하여 리뷰 변경 시에도 storeList 캐시가 정상적으로 비워지도록 보완해 주세요.
- idx_store_popularity 인덱스 컬럼 방향 명시 (average_star DESC, review_count DESC, bookmark_count DESC, id ASC) - COALESCE 제거, findInBounds GIST 캐스팅 수정 (::geography) - applyReviewCreated/Deleted/Updated 에 @CacheEvict(popularStores + storeList) 추가 - evictPopularStoresCache 도 storeList 함께 evict 하도록 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
📢 기능 설명
필요시 실행결과 스크린샷 첨부
연결된 issue
연결된 issue를 자동을 닫기 위해 아래 {이슈넘버}를 입력해주세요.
close #{이슈넘버}
✅ 체크리스트