Conversation
- findNearby() 바운딩박스 선필터 + (latitude, longitude) 복합 B-tree 인덱스 추가 - 전수 정렬(풀 스캔) → 반경 5km BETWEEN 후보 축소 후 거리 정렬 - PostGIS geometry 컬럼 + GIST 인덱스 마이그레이션 스크립트 추가 - findNearbyWithGist() (ST_DWithin + KNN) 쿼리 추가 - 인기 매장 Redis 캐시 적용 (CacheConfig 신규, TTL 5분) - 캐시 미적용으로 매 요청마다 DB 집계 쿼리 실행되던 문제 해결 - 리뷰 조회 페이징 적용 (전체 로드 → 최근 20건) - 엔티티 인덱스 추가: Store / Reservation / Review / Menu / Bookmark FK 컬럼 - Bookmark 폴더 삭제 배치 UPDATE (forEach N번 → 벌크 1번) - page size 상한 100 제한, findPopular countQuery 최적화 - k6 07 에러율 오탐 수정 (SLA 체크 분리) - k6 08 http_req_failed 오탐 제거, 주석 비관적락→Redis Lua 정정 - Tomcat 메트릭 수집 활성화 - k6 run.ps1 prometheus push interval 2s 설정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- location 컬럼 geometry → geography 타입 변경 (GIST 인덱스 직접 활용) - findNearbyWithGist 쿼리 불필요한 ::geography 캐스팅 제거 - Store.java columnDefinition geography(Point, 4326)로 수정 - CacheConfig GenericJackson2JsonRedisSerializer 복원 (타입 정보 보존) - @Cacheable sync=true 추가 (Cache Stampede 방지) - @Modifying clearAutomatically=true 추가 (영속성 컨텍스트 불일치 방지) - findPopular 무의미한 countQuery 제거 (List 반환 시 미실행) - migration_v3_geometry_to_geography.sql 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- migration_v2/v3 분리 파일 제거, migration_v2_postgis_geography.sql 하나로 통합 (운영 서버는 geography 타입으로 단일 마이그레이션 실행) - after-changes.md 갱신: Gemini 코드리뷰 반영 사항 추가 (geography 타입, sync=true, clearAutomatically=true, countQuery 제거) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- migration_v2_postgis_geography.sql: geography(Point, 4326) 직접 추가 (로컬의 geometry→geography 2단계 과정을 운영용으로 통합) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Feat: 부하 테스트 기반 성능 개선
There was a problem hiding this comment.
Code Review
This pull request introduces performance optimizations, including PostGIS spatial indexing for nearby store searches, Redis caching for popular stores, query pagination for reviews, bulk soft deletes for bookmarks, and various database indexes. However, several critical issues must be addressed: a compilation error in StoreRepository due to a missing parenthesis, a bug in BookmarkService where a detached entity is not updated, and a data integrity issue where the location field in Store is marked as non-insertable and non-updatable. Additionally, the popular stores cache eviction method is never invoked, and redundant indexes are defined on Reservation and Store.
| @Query(value = """ | ||
| SELECT s FROM Store s | ||
| WHERE s.isDeleted = false | ||
| ORDER BY COALESCE(s.averageStar, 0) DESC, | ||
| s.reviewCount DESC, | ||
| s.bookmarkCount DESC, | ||
| s.id ASC | ||
| """) | ||
| """, |
There was a problem hiding this comment.
@query 어노테이션의 닫는 괄호())가 누락되어 컴파일 에러가 발생합니다. 기존 코드에서 value 속성을 명시하면서 마지막 괄호를 닫지 않고 쉼표(,)로 끝마쳤습니다. 이를 올바르게 닫아주어야 합니다.
@Query(value = """
SELECT s FROM Store s
WHERE s.isDeleted = false
ORDER BY COALESCE(s.averageStar, 0) DESC,
s.reviewCount DESC,
s.bookmarkCount DESC,
s.id ASC
"")| bookmarkRepository.softDeleteAllByFolderId(folderId); | ||
|
|
||
| folder.softDelete(); |
There was a problem hiding this comment.
BookmarkRepository의 softDeleteAllByFolderId 메서드에는 @Modifying(clearAutomatically = true) 설정이 적용되어 있습니다. 이로 인해 벌크 업데이트 쿼리 실행 직후 영속성 컨텍스트(Persistence Context)가 완전히 비워지게(clear) 되며, 이전에 조회했던 folder 엔티티는 준영속(Detached) 상태가 됩니다.
따라서 그 이후에 호출되는 folder.softDelete()는 준영속 상태의 객체 필드만 변경할 뿐, 트랜잭션 커밋 시점에 데이터베이스로 변경 사항이 반영(Flush)되지 않아 폴더 자체가 삭제되지 않는 심각한 버그가 발생합니다.
이를 해결하기 위해 folder.softDelete()를 벌크 삭제 메서드 호출 전에 실행하도록 순서를 변경해야 합니다. 이렇게 하면 벌크 쿼리가 실행되기 전 하이버네이트의 자동 플러시(Auto-Flush)에 의해 폴더의 삭제 상태가 DB에 먼저 반영된 후 벌크 삭제가 안전하게 진행됩니다.
| bookmarkRepository.softDeleteAllByFolderId(folderId); | |
| folder.softDelete(); | |
| folder.softDelete(); | |
| bookmarkRepository.softDeleteAllByFolderId(folderId); |
|
|
||
| // PostGIS geometry 컬럼 — ST_DWithin/KNN 공간 인덱스 활용 | ||
| // migration_v2_postgis_geometry.sql 실행 후 GIST 인덱스 적용됨 | ||
| @Column(columnDefinition = "geography(Point, 4326)", insertable = false, updatable = false) |
There was a problem hiding this comment.
현재 location 필드가 insertable = false, updatable = false로 설정되어 있어, 애플리케이션에서 새로운 매장을 등록하거나 기존 매장의 위경도를 수정할 때 location 컬럼이 데이터베이스에 반영되지 않거나 갱신되지 않습니다. migration_v2_postgis_geography.sql 마이그레이션 스크립트에도 트리거가 정의되어 있지 않으므로, 신규 매장의 location은 NULL로 남게 되어 공간 인덱스 검색(findNearbyWithGist)에서 누락되는 심각한 데이터 정합성 문제가 발생합니다.
이를 해결하기 위해 insertable = false, updatable = false 설정을 제거하고, 엔티티 내부의 @PrePersist 및 @PreUpdate 생명주기 메서드를 통해 위경도가 변경될 때마다 location 필드를 갱신해주거나, 데이터베이스 레벨에서 BEFORE INSERT OR UPDATE 트리거를 추가하여 자동으로 location을 계산하도록 개선해야 합니다.
| @CacheEvict(value = "popularStores", allEntries = true) | ||
| @Transactional | ||
| public void evictPopularStoresCache() { | ||
| // 매장 등록/수정/리뷰 생성 등 인기도 변동 시 호출 | ||
| } |
There was a problem hiding this comment.
현재 evictPopularStoresCache() 메서드가 정의되어 있지만, StoreService 내의 매장 등록(createStore), 수정(updateStore), 리뷰 생성/삭제/수정(applyReviewCreated, applyReviewDeleted, applyReviewUpdated) 시점에 실제로 호출되지 않고 있습니다. 이로 인해 인기 매장 목록 캐시(popularStores)가 최대 5분 동안 갱신되지 않는 데이터 불일치 문제가 발생합니다.
해당 메서드들을 직접 호출하거나, 각 메서드에 @CacheEvict(value = "popularStores", allEntries = true) 어노테이션을 추가하여 인기도가 변경될 때 캐시가 자동으로 비워지도록 개선해야 합니다.
| @Index(name = "idx_reservation_user_id", columnList = "user_id"), | ||
| @Index(name = "idx_reservation_user_status", columnList = "user_id, status"), |
There was a problem hiding this comment.
복합 인덱스 idx_reservation_user_status (user_id, status)가 존재하므로, 선두 컬럼인 user_id 단독 검색 시에도 이 복합 인덱스를 활용할 수 있습니다. 따라서 단일 컬럼 인덱스인 idx_reservation_user_id (user_id)는 중복이며, 불필요한 디스크 공간 차지 및 쓰기 성능 저하를 유발하므로 제거하는 것이 좋습니다.
| @Index(name = "idx_reservation_user_id", columnList = "user_id"), | |
| @Index(name = "idx_reservation_user_status", columnList = "user_id, status"), | |
| @Index(name = "idx_reservation_user_status", columnList = "user_id, status"), |
| @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") |
There was a problem hiding this comment.
마이그레이션 SQL 스크립트(migration_v2_postgis_geography.sql 25행)에서 is_deleted = false 조건이 있는 부분 인덱스(Partial Index) idx_stores_lat_lng를 생성하고 있습니다. StoreRepository의 findNearby 쿼리 역시 항상 is_deleted = false 조건을 포함하므로 이 부분 인덱스만으로 충분히 커버됩니다. 따라서 Store.java에 정의된 글로벌 복합 인덱스 @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")
📢 기능 설명
필요시 실행결과 스크린샷 첨부
연결된 issue
연결된 issue를 자동을 닫기 위해 아래 {이슈넘버}를 입력해주세요.
close #{이슈넘버}
✅ 체크리스트