From 1e39bdff817a11795adfe4aef370f0d1586f2332 Mon Sep 17 00:00:00 2001 From: dongan Date: Tue, 2 Jun 2026 22:18:56 +0900 Subject: [PATCH 1/2] =?UTF-8?q?perf:=20store=20=EB=AA=A9=EB=A1=9D/nearby/i?= =?UTF-8?q?n-bounds=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/main/java/com/catchtable/store/entity/Store.java | 7 ++++--- .../com/catchtable/store/repository/StoreRepository.java | 8 ++------ .../java/com/catchtable/store/service/StoreService.java | 4 ++++ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/catchtable/store/entity/Store.java b/src/main/java/com/catchtable/store/entity/Store.java index a8230b8..043f62b 100644 --- a/src/main/java/com/catchtable/store/entity/Store.java +++ b/src/main/java/com/catchtable/store/entity/Store.java @@ -14,9 +14,10 @@ @Table( name = "stores", indexes = { - @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_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") } ) @Getter diff --git a/src/main/java/com/catchtable/store/repository/StoreRepository.java b/src/main/java/com/catchtable/store/repository/StoreRepository.java index 2f0fa6c..c1ca58d 100644 --- a/src/main/java/com/catchtable/store/repository/StoreRepository.java +++ b/src/main/java/com/catchtable/store/repository/StoreRepository.java @@ -121,12 +121,8 @@ List findNearbyWithGist(@Param("lat") double latitude, @Query(value = """ SELECT * FROM stores s WHERE s.is_deleted = false - AND s.latitude BETWEEN :minLat AND :maxLat - AND s.longitude BETWEEN :minLng AND :maxLng - ORDER BY ST_DistanceSphere( - ST_MakePoint(s.longitude, s.latitude), - ST_MakePoint(:centerLng, :centerLat) - ) ASC, + AND s.location::geometry && ST_MakeEnvelope(:minLng, :minLat, :maxLng, :maxLat, 4326) + ORDER BY s.location <-> ST_SetSRID(ST_MakePoint(:centerLng, :centerLat), 4326)::geography ASC, s.id ASC """, nativeQuery = true) diff --git a/src/main/java/com/catchtable/store/service/StoreService.java b/src/main/java/com/catchtable/store/service/StoreService.java index 8f13f98..4dd2362 100644 --- a/src/main/java/com/catchtable/store/service/StoreService.java +++ b/src/main/java/com/catchtable/store/service/StoreService.java @@ -45,6 +45,7 @@ public class StoreService { private final StoreRemainRepository storeRemainRepository; // 매장 등록 + @CacheEvict(value = "storeList", allEntries = true) @Transactional public StoreCreateResponse createStore(Long userId, StoreCreateRequest request) { userRepository.getAdminOrThrow(userId, ErrorCode.ADMIN_ONLY_STORE_CREATE); @@ -57,6 +58,7 @@ public StoreCreateResponse createStore(Long userId, StoreCreateRequest request) * 매장 목록 통합 조회 (이름·카테고리·지역 옵셔널 필터 + DB 페이지네이션 + 인기 정렬) * Specification 사용으로 PostgreSQL+enum 조합에서 :param IS NULL 회피. */ + @Cacheable(value = "storeList", key = "#name + ':' + #category + ':' + #district + ':' + #page + ':' + #size") @Transactional(readOnly = true) public List getStores(String name, Category category, District district, int page, int size) { int limitedSize = Math.min(size, 100); @@ -196,6 +198,7 @@ public StoreDetailResponse getStore(Long storeId) { } // 매장 정보 수정 + @CacheEvict(value = "storeList", allEntries = true) @Transactional public StoreUpdateResponse updateStore(Long userId, Long storeId, StoreUpdateRequest request) { userRepository.getAdminOrThrow(userId, ErrorCode.ADMIN_ONLY_STORE_UPDATE); @@ -210,6 +213,7 @@ public StoreUpdateResponse updateStore(Long userId, Long storeId, StoreUpdateReq } // 매장 상태 변경 + @CacheEvict(value = "storeList", allEntries = true) @Transactional public StoreStatusUpdateResponse updateStoreStatus(Long userId, Long storeId, StoreStatusUpdateRequest request) { userRepository.getAdminOrThrow(userId, ErrorCode.ADMIN_ONLY_STORE_STATUS); From fc6e683313c539b4c8f21f06ec9597f93574b382 Mon Sep 17 00:00:00 2001 From: dongan Date: Tue, 2 Jun 2026 22:35:22 +0900 Subject: [PATCH 2/2] =?UTF-8?q?perf:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=202=EC=B0=A8=20=EB=B0=98=EC=98=81=20=E2=80=94=20?= =?UTF-8?q?=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EB=B0=A9=ED=96=A5=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C,=20=EC=BA=90=EC=8B=9C=20evict=20=EB=88=84=EB=9D=BD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../java/com/catchtable/store/entity/Store.java | 2 +- .../store/repository/StoreRepository.java | 15 +++++---------- .../catchtable/store/service/StoreService.java | 8 +++++--- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/catchtable/store/entity/Store.java b/src/main/java/com/catchtable/store/entity/Store.java index 043f62b..8bf6e0c 100644 --- a/src/main/java/com/catchtable/store/entity/Store.java +++ b/src/main/java/com/catchtable/store/entity/Store.java @@ -17,7 +17,7 @@ @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") + @Index(name = "idx_store_popularity", columnList = "is_deleted, average_star DESC, review_count DESC, bookmark_count DESC, id ASC") } ) @Getter diff --git a/src/main/java/com/catchtable/store/repository/StoreRepository.java b/src/main/java/com/catchtable/store/repository/StoreRepository.java index c1ca58d..caf86be 100644 --- a/src/main/java/com/catchtable/store/repository/StoreRepository.java +++ b/src/main/java/com/catchtable/store/repository/StoreRepository.java @@ -16,15 +16,10 @@ public interface StoreRepository extends JpaRepository, JpaSpecific List findAllByIsDeletedFalse(); - /** - * 인기 매장 정렬 — averageStar DESC → reviewCount DESC → bookmarkCount DESC → id ASC - * - averageStar는 NULL일 수 있어 COALESCE로 0 처리 - * - 모든 통계 값이 동률일 때 ID 오름차순(=먼저 등록된 순)으로 결정성 보장 - */ @Query(value = """ SELECT s FROM Store s WHERE s.isDeleted = false - ORDER BY COALESCE(s.averageStar, 0) DESC, + ORDER BY s.averageStar DESC, s.reviewCount DESC, s.bookmarkCount DESC, s.id ASC @@ -80,9 +75,9 @@ AND ST_DistanceSphere( ST_MakePoint(s.longitude, s.latitude), ST_MakePoint(:lon, :lat) ) <= :radiusMeters - ORDER BY COALESCE(s.average_star, 0) DESC, - COALESCE(s.review_count, 0) DESC, - COALESCE(s.bookmark_count, 0) DESC, + ORDER BY s.average_star DESC, + s.review_count DESC, + s.bookmark_count DESC, s.id ASC """, nativeQuery = true) @@ -121,7 +116,7 @@ List findNearbyWithGist(@Param("lat") double latitude, @Query(value = """ SELECT * FROM stores s WHERE s.is_deleted = false - AND s.location::geometry && ST_MakeEnvelope(:minLng, :minLat, :maxLng, :maxLat, 4326) + AND s.location && ST_MakeEnvelope(:minLng, :minLat, :maxLng, :maxLat, 4326)::geography ORDER BY s.location <-> ST_SetSRID(ST_MakePoint(:centerLng, :centerLat), 4326)::geography ASC, s.id ASC """, diff --git a/src/main/java/com/catchtable/store/service/StoreService.java b/src/main/java/com/catchtable/store/service/StoreService.java index 4dd2362..f07fdd1 100644 --- a/src/main/java/com/catchtable/store/service/StoreService.java +++ b/src/main/java/com/catchtable/store/service/StoreService.java @@ -58,7 +58,7 @@ public StoreCreateResponse createStore(Long userId, StoreCreateRequest request) * 매장 목록 통합 조회 (이름·카테고리·지역 옵셔널 필터 + DB 페이지네이션 + 인기 정렬) * Specification 사용으로 PostgreSQL+enum 조합에서 :param IS NULL 회피. */ - @Cacheable(value = "storeList", key = "#name + ':' + #category + ':' + #district + ':' + #page + ':' + #size") + @Cacheable(value = "storeList", key = "T(String).valueOf(#category) + ':' + T(String).valueOf(#district) + ':' + #page + ':' + #size", condition = "#name == null") @Transactional(readOnly = true) public List getStores(String name, Category category, District district, int page, int size) { int limitedSize = Math.min(size, 100); @@ -81,10 +81,9 @@ public List getPopularStores(int limit) { .toList(); } - @CacheEvict(value = "popularStores", allEntries = true) + @CacheEvict(value = {"popularStores", "storeList"}, allEntries = true) @Transactional public void evictPopularStoresCache() { - // 매장 등록/수정/리뷰 생성 등 인기도 변동 시 호출 } @Tool(description = "사용자 주변의 인기 매장을 조회합니다. '내 주변 맛집', '근처 인기 매장', '주변 맛집 추천' 등의 요청에 사용하세요. 위치 정보가 없으면 사용할 수 없습니다.") @@ -227,6 +226,7 @@ public StoreStatusUpdateResponse updateStoreStatus(Long userId, Long storeId, St * 자체 리뷰 생성 시 호출 — 외부 시드된 별점/리뷰수를 base로 평균에 합산. * 리스너에서 비동기로 호출되므로 리뷰 작성 자체엔 영향을 주지 않는다. */ + @CacheEvict(value = {"popularStores", "storeList"}, allEntries = true) @Transactional public void applyReviewCreated(Long storeId, int newStar) { Store store = storeRepository.findByIdAndIsDeletedFalse(storeId) @@ -234,6 +234,7 @@ public void applyReviewCreated(Long storeId, int newStar) { store.applyReviewCreated(newStar); } + @CacheEvict(value = {"popularStores", "storeList"}, allEntries = true) @Transactional public void applyReviewDeleted(Long storeId, int deletedStar) { Store store = storeRepository.findByIdAndIsDeletedFalse(storeId) @@ -241,6 +242,7 @@ public void applyReviewDeleted(Long storeId, int deletedStar) { store.applyReviewDeleted(deletedStar); } + @CacheEvict(value = {"popularStores", "storeList"}, allEntries = true) @Transactional public void applyReviewUpdated(Long storeId, int oldStar, int newStar) { Store store = storeRepository.findByIdAndIsDeletedFalse(storeId)