-
Notifications
You must be signed in to change notification settings - Fork 1
[feat] Elastic Search와 MySQL을 썼을 때 기능별 성능 비교 #179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughMySQL 기반 엔티티/리포지토리 추가, DTO에 fromEntity 팩토리 추가, MySQL/QueryDSL을 활용한 검색·추천·주변시설 조회 리포지토리 신설, 통합 테스트에서 데이터 규모 확대와 성능 측정 로깅 추가, 일부 테스트 라이프사이클 변경. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant C as Client
participant R as WaybleZoneQuerySearchMysqlRepository
participant Q as JPAQueryFactory/MySQL
participant D as DTO Mappers
C->>R: searchWaybleZonesByCondition(cond, pageable)
R->>Q: SELECT zones WITH haversine + filters ORDER BY distance LIMIT page+1
Q-->>R: List<WaybleZone>
R->>D: WaybleZoneInfoResponseDto.fromEntity(zone)
D-->>R: WaybleZoneInfoResponseDto
R-->>C: Slice<WaybleZoneSearchResponseDto>
sequenceDiagram
autonumber
participant C as Client
participant RR as WaybleZoneQueryRecommendMysqlRepository
participant Q as JPAQueryFactory/MySQL
participant D as DTO Mappers
C->>RR: searchPersonalWaybleZones(user, lat, lon, size)
RR->>Q: SELECT candidate zones WITH haversine (<=50km)
Q-->>RR: List<WaybleZone>
RR->>Q: SELECT recent visit logs (30 days)
Q-->>RR: List<WaybleZoneVisitLog>
RR->>RR: compute scores (distance, similarity, recency)
RR->>D: WaybleZoneInfoResponseDto.fromEntity(zone)
D-->>RR: Info DTO
RR-->>C: List<WaybleZoneRecommendResponseDto> (sorted by score)
sequenceDiagram
autonumber
participant C as Client
participant F as WaybleFacilityQuerySearchMysqlRepository
participant Q as JPAQueryFactory/MySQL
C->>F: findNearbyFacilitiesByType(condition)
F->>Q: SELECT facilities WITH haversine<=10km AND type ORDER BY distance LIMIT 50
Q-->>F: List<WaybleFacilityMySQL>
F-->>C: List<WaybleFacilityResponseDto>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java (1)
111-125: existsBy… 검사 + 개별 save 루프는 N+1 패턴 유발
existsByUserIdAndZoneId를 루프마다 호출하면 저장 건수만큼 조회 쿼리가 발생합니다.- 미리 고유 zoneId 집합을 뽑아
saveAll로 일괄 저장하는 방식이 효율적입니다.아래처럼 중복 없는 zoneId를 먼저 샘플링 후 saveAll 하세요:
- for (int i = 1; i <= SAMPLES / 3; i++) { - Long zoneId = (long) (Math.random() * SAMPLES) + 1; - if(!recommendLogDocumentRepository.existsByUserIdAndZoneId(userId, zoneId)) { - RecommendLogDocument recommendLogDocument = RecommendLogDocument - .builder() - .id(UUID.randomUUID().toString()) - .userId(userId) - .zoneId(zoneId) - .recommendationDate(makeRandomDate()) - .recommendCount(1L) - .build(); - recommendLogDocumentRepository.save(recommendLogDocument); - } - } + ThreadLocalRandom tlr = ThreadLocalRandom.current(); + int target = (int) (SAMPLES / 3); + Set<Long> uniqueZoneIds = new HashSet<>(target); + while (uniqueZoneIds.size() < target) { + uniqueZoneIds.add(tlr.nextLong(1, SAMPLES + 1)); + } + List<RecommendLogDocument> batch = uniqueZoneIds.stream() + .map(zid -> RecommendLogDocument.builder() + .id(UUID.randomUUID().toString()) + .userId(userId) + .zoneId(zid) + .recommendationDate(makeRandomDate()) + .recommendCount(1L) + .build()) + .toList(); + recommendLogDocumentRepository.saveAll(batch);
🧹 Nitpick comments (30)
src/main/java/com/wayble/server/explore/entity/WaybleFacilityMySQL.java (2)
11-12: 거리/주변 검색을 위한 인덱스 추가 제안 (MySQL 성능 비교의 공정성 확보).현재 위/경도를 Double 두 컬럼으로 보관하고 있어 범위(bounding box) 필터링 시 풀스캔 가능성이 있습니다. 성능 비교 목적(PR 목표) 관점에서 최소한 BTREE 인덱스는 걸어두는 편이 공정합니다. 필요 시
facility_type도 보조 인덱스로 추가하세요.아래와 같이 인덱스를 추가하는 것을 권장합니다.
-@Table(name = "wayble_facility") +@Table( + name = "wayble_facility", + indexes = { + @Index(name = "idx_wayble_facility_lat_lon", columnList = "latitude, longitude"), + @Index(name = "idx_wayble_facility_type", columnList = "facility_type") + } +)
18-22: 지리공간 타입(POINT) + SPATIAL INDEX 고려.거리순 조회 성능을 제대로 비교하려면 MySQL 측에서도
POINT(SRID 4326)+SPATIAL INDEX(또는 generated column 기반) 사용이 이상적입니다. 현재 설계(Double 2개)는 하버사인 + 범위 필터로도 충분히 비교는 가능하나, ES 대비 MySQL이 불리하게 측정될 수 있습니다.간단 대안(마이그레이션 비용 낮음):
- 우선 BTREE 복합 인덱스(lat, lon)로 시작 → 필요 시 후속 PR에서 Hibernate Spatial 도입 or 생성 칼럼(POINT) + 공간 인덱스 적용.
src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java (1)
16-29: EsWaybleZoneFacility 경유를 없애는 오버로드 추가 제안.현재 JPA 엔티티(WaybleZoneFacility) → EsWaybleZoneFacility → FacilityResponseDto 변환 체인을 사용하고 있어 불필요한 객체 생성이 있습니다. 직접 JPA 엔티티를 받는 오버로드를 추가하면 의존/오버헤드가 줄고 가독성도 좋아집니다. (이미
WaybleZoneFacilityimport가 존재)public static FacilityResponseDto from(EsWaybleZoneFacility facility) { if (facility == null) { return null; } return FacilityResponseDto.builder() .hasSlope(facility.isHasSlope()) .hasNoDoorStep(facility.isHasNoDoorStep()) .hasElevator(facility.isHasElevator()) .hasTableSeat(facility.isHasTableSeat()) .hasDisabledToilet(facility.isHasDisabledToilet()) .floorInfo(facility.getFloorInfo()) .build(); } + +public static FacilityResponseDto from(WaybleZoneFacility facility) { + if (facility == null) { + return null; + } + return FacilityResponseDto.builder() + .hasSlope(facility.isHasSlope()) + .hasNoDoorStep(facility.isHasNoDoorStep()) + .hasElevator(facility.isHasElevator()) + .hasTableSeat(facility.isHasTableSeat()) + .hasDisabledToilet(facility.isHasDisabledToilet()) + .floorInfo(facility.getFloorInfo()) + .build(); +}src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java (1)
17-23: NPE 가드 추가 권장: ES Document의 location null 가능성.
facilityDocument.getLocation()이 null이면 NPE가 발생합니다. 인덱싱/마이그레이션 과정에서 위치가 비어있을 가능성이 있으므로 fail-fast 또는 안전 처리 권장합니다.public static WaybleFacilityResponseDto from(WaybleFacilityDocument facilityDocument) { - return WaybleFacilityResponseDto.builder() - .latitude(facilityDocument.getLocation().getLat()) - .longitude(facilityDocument.getLocation().getLon()) - .facilityType(facilityDocument.getFacilityType()) - .build(); + if (facilityDocument == null || facilityDocument.getLocation() == null) { + throw new IllegalArgumentException("WaybleFacilityDocument.location must not be null"); + } + return WaybleFacilityResponseDto.builder() + .latitude(facilityDocument.getLocation().getLat()) + .longitude(facilityDocument.getLocation().getLon()) + .facilityType(facilityDocument.getFacilityType()) + .build(); }필요 시, 예외 대신
null반환 or 기본값(0.0) 적용으로 바꿀 수 있으니 의도에 맞게 선택하세요.src/main/java/com/wayble/server/explore/dto/common/WaybleZoneInfoResponseDto.java (1)
37-51: 시설 매핑에서 ES 변환 중간단계 제거 제안.이미 DTO 쪽에
FacilityResponseDto.from(WaybleZoneFacility)오버로드를 추가하면, 아래와 같이 ES 경유 없이 바로 매핑 가능합니다. 불필요한 객체 생성/의존성 감소 및 가독성 개선.- .facility(waybleZone.getFacility() != null ? - FacilityResponseDto.from(EsWaybleZoneFacility.from(waybleZone.getFacility())) : null) + .facility(waybleZone.getFacility() != null ? + FacilityResponseDto.from(waybleZone.getFacility()) : null)오버로드 추가 후
import com.wayble.server.explore.entity.EsWaybleZoneFacility;(Line 3)도 제거 가능합니다.src/main/java/com/wayble/server/explore/dto/search/response/WaybleZoneSearchResponseDto.java (1)
24-30: fromEntity 정적 팩토리 추가 OK. 네이밍 일관성/널 처리만 점검 권장
- 구현 자체는 간결하고 기존 from(WaybleZoneDocument, Double)와 대칭을 이룹니다.
- 단, 동일 DTO 내에
from(…Document, …)와fromEntity(…Entity, …)가 혼재합니다. 팀 컨벤션에 따라 오버로드(from(WaybleZone, Double))로 통일할지 유지할지 결정 필요.- distance가 Double nullable입니다. 상위 레이어가 항상 값을 보장하는지(예: 거리 기반 응답에서는 null 불가) 확인해 주세요. 필요 시 @NotNull 어노테이션 또는 사전 검증 도입을 권장합니다.
상위 검색/검증 API에서 distance가 null이 되지 않는지 한 번만 확인 부탁드립니다.
src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java (4)
100-101: 수명주기 변경(@BeforeEach/@AfterEach)로 대량 데이터의 생성/삭제가 매 테스트 반복
- 데이터 재사용이 가능하다면 클래스 단위 셋업(@BeforeAll)/티어다운(@afterall)로 되돌리고, 테스트 간 데이터 충돌을 피하기 위해 네임스페이스/랜덤 시드 분리로 해결하는 것을 고려해 주세요.
- 삭제도 deleteAll 대신 deleteAllInBatch(지원 시) 또는 인덱스/테이블 truncate(통합 테스트 DB 한정)로 비용을 낮출 수 있습니다.
Also applies to: 179-185
127-175: 대량 데이터 생성 루프 최적화(랜덤, 배치 저장, 출력 최소화)
- Math.random 다중 호출 대신 ThreadLocalRandom 사용이 빠르고 가비지 적습니다.
waybleZoneDocumentRepository.save/userRepository.save/waybleZoneVisitLogRepository.save각각의 개별 호출은 오버헤드 큽니다. 가능하다면 컬렉션으로 모아saveAll사용을 권장합니다.- 테스트 본문에서 대용량 JSON pretty print는 I/O 병목입니다. 필요 시 로거/조건부 출력으로 제한하세요.
다음 스케치처럼 개선 가능합니다(개념 예):
- for (int i = 1; i <= SAMPLES; i++) { - Map<String, Double> points = makeRandomPoint(); + ThreadLocalRandom tlr = ThreadLocalRandom.current(); + List<WaybleZoneDocument> docs = new ArrayList<>((int) SAMPLES); + List<User> users = new ArrayList<>((int) SAMPLES); + List<WaybleZoneVisitLog> logs = new ArrayList<>(); + for (int i = 1; i <= SAMPLES; i++) { + Map<String, Double> points = makeRandomPoint(); ... - waybleZoneDocumentRepository.save(waybleZoneDocument); + docs.add(waybleZoneDocument); ... - userRepository.save(user); + users.add(user); - int count = (int) (Math.random() * 20) + 1; + int count = tlr.nextInt(1, 21); for (int j = 0; j < count; j++) { - Long zoneId = (long) (Math.random() * SAMPLES) + 1; + long zoneId = tlr.nextLong(1, SAMPLES + 1); ... - waybleZoneVisitLogRepository.save(visitLogDocument); + logs.add(visitLogDocument); } } + waybleZoneDocumentRepository.saveAll(docs); + userRepository.saveAll(users); + waybleZoneVisitLogRepository.saveAll(logs);
207-229: System.currentTimeMillis() 기반 수동 타이밍/대량 콘솔 출력 최소화 권장
- 타이밍에는 nanoTime() 또는 StopWatch(스프링)를 추천합니다.
- 대량 pretty print는 성능 측정을 왜곡합니다. 측정값만 요약 출력하거나 로거 INFO 수준으로 전환해 조건부로 보이게 하세요.
다음과 같이 공통 유틸로 추출하면 중복도 줄어듭니다:
- long startTime = System.currentTimeMillis(); + long startTime = System.nanoTime(); ... - long endTime = System.currentTimeMillis(); - long responseTime = endTime - startTime; - System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms"); + long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime); + log.info("==== 성능 측정 결과 ====\n 응답 시간: {} ms", elapsedMs);Also applies to: 252-276, 301-326
242-247: 날짜 단정 비교는 경계 시각에 플래키 가능성
recommendationDate가 서비스에서LocalDate.now()로 저장된다는 가정하에, 자정 경계 또는 타임존 이슈로 테스트가 드물게 실패할 수 있습니다. 하루 허용 오차(또는 동일 일자 여부)로 비교하는 것이 안전합니다.- assertThat(recommendLogDocument.get().getRecommendationDate()).isEqualTo(LocalDate.now()); + assertThat(recommendLogDocument.get().getRecommendationDate()) + .isBetween(LocalDate.now().minusDays(1), LocalDate.now());src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java (4)
55-57: MySQL 시드/정리 로직 추가는 적절하나, 실제 엔드포인트가 ES 경로만 사용 중이면 효과가 상쇄됩니다
- 테스트는 MySQL 엔티티를 시드하지만, 현재 서비스(
WaybleFacilityDocumentService)는 ES 레포만 사용 중으로 보입니다. 이 경우 본 테스트는 MySQL 경로 성능을 검증하지 못합니다.- MySQL 경로를 활성화할 수 있도록 서비스에 토글(프로퍼티/프로파일/빈 주입) 추가와 MySQL 경로 전용 테스트를 권장합니다.
서비스 토글 예시(별도 파일 참고용):
// WaybleFacilityServiceFacade (신규) @Service @RequiredArgsConstructor public class WaybleFacilityServiceFacade { private final WaybleFacilityQuerySearchRepository esRepo; private final WaybleFacilityQuerySearchMysqlRepository mysqlRepo; @Value("${feature.facility.search-backend:es}") private String backend; public List<WaybleFacilityResponseDto> findNearby(WaybleFacilityConditionDto dto) { return "mysql".equalsIgnoreCase(backend) ? mysqlRepo.findNearbyFacilitiesByType(dto) : esRepo.findNearbyFacilitiesByType(dto); } }그리고 컨트롤러/기존 서비스 주입을 Facade로 교체 후, 테스트에서 프로퍼티로
feature.facility.search-backend=mysql을 주입해 MySQL 경로 성능을 계측하세요.Also applies to: 104-120, 126-126
67-67: SAMPLES 1000 + @beforeeach: 성능 테스트는 프로파일/태그로 분리 권장WaybleZoneRecommend 테스트와 동일하게, 대량 시드는 perf 태그에서만 수행되도록 조정해 CI 불안정을 낮추세요.
- private static final int SAMPLES = 1000; + private static final int SAMPLES = + Integer.parseInt(System.getProperty("WAYBLE_FACILITY_SAMPLES", "200"));Also applies to: 75-76
147-169: 타이밍/로그 출력 패턴 통일 및 최소화System.out 대신 로거 사용, nanoTime/StopWatch 사용으로 측정 정밀도와 가독성을 높이세요. 대량 JSON pretty print는 제거를 권장합니다(필요 시 DEBUG 한정).
Also applies to: 213-235
86-103: 배치 저장으로 삽입 비용 절감
- ES/문서/관계형 각각
saveAll을 사용해 삽입을 배치화하면 속도가 크게 개선됩니다.- waybleFacilityDocumentRepository.save(rampDocument); - waybleFacilityDocumentRepository.save(elevatorDocument); + docs.add(rampDocument); + docs.add(elevatorDocument); ... - waybleFacilityMySQLRepository.save(ramp); - waybleFacilityMySQLRepository.save(elevator); + mysqls.add(ramp); + mysqls.add(elevator); ... + waybleFacilityDocumentRepository.saveAll(docs); + waybleFacilityMySQLRepository.saveAll(mysqls);Also applies to: 104-120
src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java (5)
106-117: @BeforeEach/@AfterEach 대량 시드/정리 반복 → 테스트 전반 비용 급증위 Recommend/Facility 테스트와 동일한 맥락으로, 클래스 단위 시드 또는 배치 저장/삭제 도입을 고려해 주세요.
Also applies to: 179-185
144-153: 사용자 랜덤 속성 생성은 재현성 저하 → 고정 시드/ThreadLocalRandom 사용 권장
- 현재 Random 생성이 여러 유틸에서 매번 이뤄져 재현성/일관성이 떨어집니다.
- 테스트 상단에 고정 시드 Random 혹은 ThreadLocalRandom 사용으로 일관성과 성능을 얻을 수 있습니다.
200-263: 반복되는 성능 측정/JSON 출력 로직은 헬퍼로 추출
- 각 테스트마다 동일한 타이밍/출력 코드가 반복됩니다. 헬퍼 메서드 또는 JUnit 확장(예:
@RegisterExtension)으로 공통화하면 유지보수성이 좋아집니다.Also applies to: 268-324, 326-381, 383-441
555-560: 실패 메시지 오타: 좋아요 정렬 검증인데 '방문 수'라고 표기메시지가 혼동을 줍니다. '좋아요 수'로 수정해 주세요.
- "방문 수 정렬 오류: 인덱스 %d의 방문 수(%d)가 인덱스 %d의 좋아요 수(%d)보다 크면 안됩니다" + "좋아요 수 정렬 오류: 인덱스 %d의 좋아요 수(%d)가 인덱스 %d의 좋아요 수(%d)보다 크면 안 됩니다"
565-623: substring(0, 2) 사용 시 방어 로직 고려(안전성)현재 데이터셋 이름들은 길이가 충분해 안전하나, 이후 짧은 이름이 들어오면
StringIndexOutOfBoundsException이 발생할 수 있습니다. 최소 길이 체크 또는min(2, length)사용을 권장합니다.- .param("zoneName", zoneName.substring(0, 2)) + .param("zoneName", zoneName.substring(0, Math.min(2, zoneName.length())))src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchMysqlRepository.java (6)
39-50: Haversine만으로 반경 필터링 시 인덱스 미활용 → bounding box 사전 필터 권장삼각함수 기반의 Haversine는 일반적으로 인덱스를 못 탑니다. 반경(r) 기준 대략적 bounding box(위·경도 범위)로 1차 필터 후 Haversine 정밀 필터를 적용하면 성능이 크게 개선됩니다.
다음과 같이 whereConditions에 bounding box를 추가하세요(클래스 상수 EARTH_RADIUS_KM 필요):
// 거리 조건 (반경 내) - whereConditions.and(distanceExpression.loe(radius)); + double lat = cond.latitude(); + double lon = cond.longitude(); + double latDelta = Math.toDegrees(radius / EARTH_RADIUS_KM); + double lonDelta = Math.toDegrees(radius / (EARTH_RADIUS_KM * Math.cos(Math.toRadians(lat)))); + whereConditions.and(waybleZone.address.latitude.between(lat - latDelta, lat + latDelta)); + whereConditions.and(waybleZone.address.longitude.between(lon - lonDelta, lon + lonDelta)); + // bounding box로 1차 축소 후 정밀 필터 + whereConditions.and(distanceExpression.loe(radius));추가로, 주소(lat/long) 컬럼에 개별 인덱스 또는 복합 인덱스 도입을 검토하세요.
56-59: zoneName 부분일치 검색: 풀스캔 가능성 → Full-Text/Prefix 최적화 검토containsIgnoreCase는 LIKE '%q%' 패턴으로 인덱스 활용이 어렵습니다. 대용량에서 성능 저하가 큽니다.
대안:
- MySQL FULLTEXT + MATCH ... AGAINST(BOOLEAN MODE) 사용(전용 인덱스 필요).
- 접두사 검색으로 UX를 설계할 수 있다면 'q%' 형태로 전환하여 인덱스 활용.
- 별도 normalised 컬럼(소문자/공백제거) 준비 후 likeIgnoreCase 대신 equals/startsWith로 전략 조정.
29-29: 미사용 상수 제거DISTRICT_SEARCH_SIZE는 사용되지 않습니다. 혼란 방지를 위해 제거하세요.
- private static final int DISTRICT_SEARCH_SIZE = 3;
131-147: 지구 반지름 상수 중복 정의 제거 및 일관화Haversine 계산에서 반지름을 메서드마다 로컬 상수로 정의하고 있습니다. 클래스 상수로 올려 재사용하세요.
다음 변경을 권장합니다:
- 클래스 필드 추가(파일 상단 임의 위치):
private static final double EARTH_RADIUS_KM = 6371.0;
- 메서드 내 로컬 상수 제거 및 참조 변경:
- // 지구 반지름 (km) - final double EARTH_RADIUS = 6371.0; - return Expressions.numberTemplate(Double.class, "{0} * 2 * ASIN(SQRT(...))", EARTH_RADIUS, ...) + return Expressions.numberTemplate(Double.class, "{0} * 2 * ASIN(SQRT(...))", EARTH_RADIUS_KM, ...)- final double R = 6371; // 지구 반지름 (km) + final double R = EARTH_RADIUS_KM; // 지구 반지름 (km)Also applies to: 149-161
166-201: 텍스트 유사도 로직 중복 → 공용 유틸로 추출 권장ES/MySQL 레포지토리 모두 동일(또는 유사)한 normalize/Levenshtein/Jaccard 구현을 포함합니다. 유지보수·테스트 용이성을 위해 공용 유틸(예: TextSimilarityUtils)로 추출하세요. 임계값(0.7/0.6)은 설정값으로 외부화하면 A/B 테스트에도 유리합니다.
Also applies to: 203-263
23-27: MySQL 리포지토리 빈 미사용 확인 및 프로파일링 전략 제안현재
WaybleZoneSearchService에서는 ES 기반 리포지토리(WaybleZoneQuerySearchRepository)만 주입되고, MySQL 구현체(WaybleZoneQuerySearchMysqlRepository)는 주석 처리되어 실제로 사용되지 않습니다.
WaybleZoneQuerySearchMysqlRepository클래스는@Repository로 빈이 등록되나,@Profile혹은@Qualifier설정이 없어 모든 환경에서 빈이 로드됩니다.
파일:src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchMysqlRepository.java:23WaybleZoneSearchService에서 MySQL 리포지토리 import 및 주입부가 아래 위치에서 주석 처리되어 있습니다.
파일:src/main/java/com/wayble/server/explore/service/WaybleZoneSearchService.java:4–5,:22- //import com.wayble.server.explore.repository.search.WaybleZoneQuerySearchMysqlRepository; - //private final WaybleZoneQuerySearchMysqlRepository waybleZoneQuerySearchRepository;이 상태로는 ES와 MySQL 간 성능 비교 자동화 및 런타임 전환이 불가능하므로, 아래와 같은 구성을 권장드립니다:
@Profile("mysql")및@Profile("es")어노테이션으로 프로파일별 빈 등록- 또는 Spring Cloud Feature Toggle 및
@ConditionalOnProperty활용- 공통 인터페이스(
WaybleZoneQuerySearchRepository) 기반 Strategy 패턴/Factory 클래스를 도입하여 런타임 시점에 구현체 선택이렇게 구성하면 환경에 따라 ES/MySQL 구현체를 유연하게 전환할 수 있고, A/B 테스트나 성능 비교 자동화를 손쉽게 수행할 수 있습니다.
src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendMysqlRepository.java (5)
120-127: recencyScore 기본값이 상수(0.3)로 고정되어 정렬에 영향 無추천 로그 맵이 비어 있으면 모든 항목이 동일한 recencyScore(0.3)를 받아 순위에 영향이 없습니다. 의도대로 “최근 추천일수록 감점” 효과를 내려면 기본값 0으로 두고 데이터가 있을 때만 가점을 주거나, 반대로 감점을 음수로 모델링하세요.
- double recencyScore = RECENCY_WEIGHT; + double recencyScore = 0.0; LocalDate lastRecommendDate = recentRecommendDateMap.get(zone.getId()); if (lastRecommendDate != null) { long daysSince = ChronoUnit.DAYS.between(lastRecommendDate, LocalDate.now()); double factor = 1.0 - Math.min(daysSince, MAX_DAY_DIFF) / (double) MAX_DAY_DIFF; // 0~1 - recencyScore = RECENCY_WEIGHT * (1.0 - factor); // days=0 -> 0점, days=30 -> full 점수 + recencyScore = RECENCY_WEIGHT * (1.0 - factor); // days=0 -> 0점, days=30 -> full 점수 }또는 “감점” 개념을 유지하려면 recencyScore를 음수로 합산하고 totalScore 계산식을 조정하세요.
45-55: 거리 필터 성능 최적화: bounding box 사전 필터 추가 권장검색 레포지토리와 동일하게 bounding box로 1차 축소 후 Haversine 정밀 필터를 적용하면 MySQL 인덱스를 활용할 수 있습니다.
예시:
double latDelta = Math.toDegrees(50.0 / EARTH_RADIUS_KM); double lonDelta = Math.toDegrees(50.0 / (EARTH_RADIUS_KM * Math.cos(Math.toRadians(latitude)))); BooleanBuilder bb = new BooleanBuilder() .and(waybleZone.address.latitude.between(latitude - latDelta, latitude + latDelta)) .and(waybleZone.address.longitude.between(longitude - lonDelta, longitude + lonDelta)); List<WaybleZone> nearbyZones = queryFactory .selectFrom(waybleZone) .leftJoin(waybleZone.facility).fetchJoin() .where(bb.and(distanceExpression.loe(50.0))) .orderBy(distanceExpression.asc(), waybleZone.id.asc()) .limit(100) .fetch();
131-144: DTO 매핑 계층 의존성 점검: ES 전용 타입 의존 최소화MySQL 경로에서 EsWaybleZoneFacility.from(...)을 경유하는 것은 계층 혼합입니다. 가능하다면 FacilityResponseDto.fromEntity(...)와 같은 MySQL 엔티티 전용 매핑을 사용해 의존을 분리하세요.
대안:
- .facility(zone.getFacility() != null ? - FacilityResponseDto.from(EsWaybleZoneFacility.from(zone.getFacility())) : null) + .facility(zone.getFacility() != null ? + FacilityResponseDto.fromEntity(zone.getFacility()) : null)해당 팩토리 메서드가 없다면 추가를 권장합니다.
33-39: 가중치/임계값 구성값 외부화DISTANCE_WEIGHT/SIMILARITY_WEIGHT/RECENCY_WEIGHT 및 MAX_DAY_DIFF는 실험/튜닝 대상입니다. application-*.yml 또는 Feature Flag로 외부화해 환경별 조정이 가능하도록 하세요.
48-54: 정렬 안정성 보강거리 동일 시 순서가 비결정적일 수 있습니다. id 추가 정렬을 권장합니다.
- .orderBy(distanceExpression.asc()) + .orderBy(distanceExpression.asc(), waybleZone.id.asc())
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (12)
src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java(1 hunks)src/main/java/com/wayble/server/explore/dto/common/WaybleZoneInfoResponseDto.java(2 hunks)src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java(2 hunks)src/main/java/com/wayble/server/explore/dto/search/response/WaybleZoneSearchResponseDto.java(2 hunks)src/main/java/com/wayble/server/explore/entity/WaybleFacilityMySQL.java(1 hunks)src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityMySQLRepository.java(1 hunks)src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchMysqlRepository.java(1 hunks)src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendMysqlRepository.java(1 hunks)src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchMysqlRepository.java(1 hunks)src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java(8 hunks)src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java(11 hunks)src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java(15 hunks)
🧰 Additional context used
🧬 Code graph analysis (10)
src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java (4)
src/main/java/com/wayble/server/explore/entity/EsWaybleZoneFacility.java (2)
from(20-33)ToString(6-34)src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneDetailDto.java (1)
FacilityInfo(40-43)src/main/java/com/wayble/server/wayblezone/entity/WaybleZoneFacility.java (1)
Entity(8-57)src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneFacilityRepository.java (1)
WaybleZoneFacilityRepository(8-11)
src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityMySQLRepository.java (1)
src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityDocumentRepository.java (1)
WaybleFacilityDocumentRepository(8-10)
src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java (2)
src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityRegisterDto.java (1)
Builder(9-23)src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityConditionDto.java (1)
WaybleFacilityConditionDto(8-21)
src/main/java/com/wayble/server/explore/dto/common/WaybleZoneInfoResponseDto.java (3)
src/main/java/com/wayble/server/wayblezone/entity/WaybleZone.java (2)
from(147-160)fromAdminDto(162-186)src/main/java/com/wayble/server/wayblezone/service/WaybleZoneService.java (1)
zone(33-59)src/main/java/com/wayble/server/explore/entity/WaybleZoneDocument.java (1)
fromDto(56-67)
src/main/java/com/wayble/server/explore/entity/WaybleFacilityMySQL.java (2)
src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityRegisterDto.java (1)
Builder(9-23)src/main/java/com/wayble/server/explore/entity/WaybleFacilityDocument.java (1)
ToString(12-27)
src/main/java/com/wayble/server/explore/dto/search/response/WaybleZoneSearchResponseDto.java (3)
src/main/java/com/wayble/server/explore/dto/search/response/WaybleZoneDistrictResponseDto.java (2)
from(13-20)Builder(7-21)src/main/java/com/wayble/server/wayblezone/entity/WaybleZone.java (1)
from(147-160)src/main/java/com/wayble/server/explore/entity/WaybleZoneDocument.java (1)
fromDto(56-67)
src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchMysqlRepository.java (3)
src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendMysqlRepository.java (1)
Repository(27-192)src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java (6)
Repository(20-101)findNearbyFacilitiesByType(31-100)gd(52-60)gds(70-78)b(39-65)f(51-61)src/main/java/com/wayble/server/explore/service/WaybleFacilityDocumentService.java (2)
Service(13-25)findNearbyFacilityDocuments(22-24)
src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchMysqlRepository.java (2)
src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java (3)
Repository(24-278)searchWaybleZonesByCondition(34-126)q(42-76)src/main/java/com/wayble/server/explore/service/WaybleZoneSearchService.java (2)
Service(16-52)searchWaybleZonesByCondition(26-31)
src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java (1)
src/test/java/com/wayble/server/direction/service/WalkingServiceTest.java (1)
BeforeEach(35-77)
src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendMysqlRepository.java (3)
src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchMysqlRepository.java (1)
Repository(17-78)src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendRepository.java (5)
Repository(30-163)searchPersonalWaybleZones(48-162)hit(117-158)gds(66-69)gd(56-59)src/main/java/com/wayble/server/explore/service/WaybleZoneRecommendService.java (1)
Service(23-103)
🔇 Additional comments (8)
src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityMySQLRepository.java (1)
6-7: 기본 CRUD 리포지토리 추가 LGTM.테스트 시딩과 비교 실험에 필요한 최소 구성입니다. 추후 범위/거리 검색은 전용 Query 리포지토리에서 처리하는 것으로 보이며 본 인터페이스는 이 상태로 충분합니다.
src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java (1)
25-31: MySQL 엔티티 → DTO 변환 메서드 추가 LGTM.필드 매핑(위도/경도/유형) 정확합니다. 성능 비교 경로에서 이 팩토리 메서드를 일관되게 사용하면 좋겠습니다.
src/main/java/com/wayble/server/explore/dto/common/WaybleZoneInfoResponseDto.java (1)
37-51: 주소/좌표 Null 안전성 검증 완료
WaybleZone의 모든 생성/업데이트 경로에서.address(...)가 필수로 설정되며,WaybleZone.from(WaybleZoneRegisterDto)와WaybleZone.fromImporter(…, Address address)모두에서 누락 없이address를 세팅합니다. 따라서getAddress()가 null을 반환하지 않아toFullAddress(),getLatitude(),getLongitude()호출 시 NPE 우려가 없습니다.src/main/java/com/wayble/server/explore/dto/search/response/WaybleZoneSearchResponseDto.java (1)
5-5: 엔티티 매핑 경로 추가는 타당합니다WaybleZone 엔티티를 직접 매핑하기 위한 import 추가는 본 PR의 목적(ES vs MySQL 경로 비교)에 필요한 변경으로 보이며, 기존 ES Document 경로와 충돌하지 않습니다.
src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchMysqlRepository.java (1)
61-77: Haversine 계산식 검증: 방향/라디안 처리 모두 적절합니다
- lat:
{facility} - {user}/ lon:{facility} - {user}차분, cos(user)*cos(facility) 구성 모두 정상입니다.- 지구 반지름 6371.0km도 테스트 유틸과 일치합니다.
src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchMysqlRepository.java (2)
61-65: fetch join 중복 가능성 점검 필요facility 연관관계가 다대일/일대일이면 안전하나, 일대다일 경우 fetch join로 인해 중복 row가 발생할 수 있습니다. 이 경우 .distinct() 추가가 필요합니다.
연관관계 카디널리티가 일대다라면 다음을 고려하세요:
- List<WaybleZone> zones = queryFactory - .selectFrom(waybleZone) + List<WaybleZone> zones = queryFactory + .selectFrom(waybleZone).distinct() .leftJoin(waybleZone.facility).fetchJoin()
71-81: DTO 변환 시 NPE 가드 제안(주소 결측 케이스 대비)주소 또는 위경도가 NULL인 데이터가 있다면 NPE가 발생합니다(도메인 제약으로 불가능하다면 무관). 안전하게 가드하거나, 쿼리에서 isNotNull 조건을 미리 주는 방식을 권장합니다.
다음 중 하나를 선택:
- 쿼리에 not-null 조건 추가:
- whereConditions.and(distanceExpression.loe(radius)); + whereConditions.and(distanceExpression.loe(radius)) + .and(waybleZone.address.latitude.isNotNull()) + .and(waybleZone.address.longitude.isNotNull());
- 매핑 시 가드:
.map(zone -> { if (zone.getAddress() == null || zone.getAddress().getLatitude() == null || zone.getAddress().getLongitude() == null) { return WaybleZoneSearchResponseDto.fromEntity(zone, null); } double distance = calculateHaversineDistanceJava(...); return WaybleZoneSearchResponseDto.fromEntity(zone, distance); })src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendMysqlRepository.java (1)
107-115: 거리 점수 스케일 확인 요청1/(1+distanceKm)은 0~1로 수렴하지만, 10km 이상에서 기여도가 급격히 작아집니다. UX 기준 거리 민감도가 다르면 로그/루트/선형 클립 등 다른 스케일을 검토하세요.
필요 시 piecewise 함수나 min(distance, cap)을 적용해 롱테일 감소를 제어할 수 있습니다.
| public static WaybleZoneInfoResponseDto fromEntity(WaybleZone waybleZone) { | ||
| return WaybleZoneInfoResponseDto.builder() | ||
| .zoneId(waybleZone.getId()) | ||
| .zoneName(waybleZone.getZoneName()) | ||
| .zoneType(waybleZone.getZoneType()) | ||
| .thumbnailImageUrl(waybleZone.getMainImageUrl()) | ||
| .address(waybleZone.getAddress().toFullAddress()) | ||
| .latitude(waybleZone.getAddress().getLatitude()) | ||
| .longitude(waybleZone.getAddress().getLongitude()) | ||
| .averageRating(waybleZone.getRating()) | ||
| .reviewCount(waybleZone.getReviewCount()) | ||
| .facility(waybleZone.getFacility() != null ? | ||
| FacilityResponseDto.from(EsWaybleZoneFacility.from(waybleZone.getFacility())) : null) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
검증 스크립트: ES 변환을 거쳐 FacilityResponseDto로 매핑하는 패턴 사용 여부 점검.
리팩터 시 영향 범위를 빠르게 확인하려면 다음 검색으로 호출 패턴을 점검하세요.
🏁 Script executed:
#!/bin/bash
# EsWaybleZoneFacility.from(...)를 거쳐 FacilityResponseDto.from(...)으로 들어오는 호출 검색
rg -nP -C2 'FacilityResponseDto\.from\s*\(\s*EsWaybleZoneFacility\.from\s*\(' --type=javaLength of output: 3157
ES 변환 후 FacilityResponseDto 매핑 위치 확인 및 반영 요청
전체 코드베이스에서 EsWaybleZoneFacility.from(...) → FacilityResponseDto.from(...) 호출 패턴이 아래 위치에서 발견되었습니다. 리팩터링 시 동일한 매핑 로직 변경이 누락되지 않도록 반드시 반영해 주세요.
- src/main/java/com/wayble/server/explore/dto/common/WaybleZoneInfoResponseDto.java – 49행
- src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepositoryImpl.java – 51행, 98행
- src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendMysqlRepository.java – 142행
위 파일들의 해당 라인에서 FacilityResponseDto.from(EsWaybleZoneFacility.from(...)) 부분이 모두 동일하게 업데이트되었는지 검증 후 반영 바랍니다.
🤖 Prompt for AI Agents
In
src/main/java/com/wayble/server/explore/dto/common/WaybleZoneInfoResponseDto.java
around lines 37–51, the Facility mapping still uses
FacilityResponseDto.from(EsWaybleZoneFacility.from(waybleZone.getFacility()));
update this line to match the new, refactored mapping used elsewhere in the
codebase (the same replacement applied in WaybleZoneRepositoryImpl and
WaybleZoneQueryRecommendMysqlRepository), ensuring the mapping is null-safe and
uses the canonical EsWaybleZoneFacility→FacilityResponseDto flow introduced by
the refactor; verify the exact new method call/signature in the other files and
apply it here, then run compilation/tests to confirm no signature or
null-handling regressions.
| public List<WaybleFacilityResponseDto> findNearbyFacilitiesByType( | ||
| WaybleFacilityConditionDto condition) { | ||
|
|
||
| // Haversine 거리 계산식 (QueryDSL Expression) | ||
| NumberExpression<Double> distanceExpression = calculateHaversineDistance( | ||
| condition.latitude(), condition.longitude()); | ||
|
|
||
| // 조건 빌더 | ||
| BooleanBuilder whereClause = new BooleanBuilder(); | ||
|
|
||
| // 시설 타입 조건 추가 | ||
| if (condition.facilityType() != null) { | ||
| whereClause.and(waybleFacilityMySQL.facilityType.eq(condition.facilityType())); | ||
| } | ||
|
|
||
| // 반경 10km 이내 필터링 | ||
| whereClause.and(distanceExpression.loe(10.0)); | ||
|
|
||
| List<WaybleFacilityMySQL> facilities = queryFactory | ||
| .selectFrom(waybleFacilityMySQL) | ||
| .where(whereClause) | ||
| .orderBy(distanceExpression.asc()) | ||
| .limit(LIMIT) | ||
| .fetch(); | ||
|
|
||
| return facilities.stream() | ||
| .map(WaybleFacilityResponseDto::fromEntity) | ||
| .toList(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
반경/Limit 하드코딩 → 조건/설정으로 외부화 권장 + 사전 바운딩 박스 필터 추가
- 반경 10km, LIMIT 50은 하드코딩되어 있습니다. 비교 실험에서는 동일 파라미터를 ES와 MySQL에 모두 적용해야 합니다.
WaybleFacilityConditionDto에 radiusKm/limit가 있다면 이를 사용하고, 없다면 추가를 검토하세요. - Haversine는 인덱스를 타지 못하므로, 먼저 바운딩 박스로 대략 필터링(위도±Δ, 경도±Δ) 후 Haversine로 정밀 필터/정렬하는 이단계 전략이 MySQL에서 유효합니다.
- whereClause.and(distanceExpression.loe(10.0));
+ double radius = condition.radiusKm() != null ? condition.radiusKm() : 10.0;
+ // 1) 바운딩 박스(≈간단 필터)
+ double lat = condition.latitude();
+ double lon = condition.longitude();
+ double latDelta = radius / 111.0;
+ double lonDelta = radius / (111.0 * Math.cos(Math.toRadians(lat)));
+ whereClause.and(waybleFacilityMySQL.latitude.between(lat - latDelta, lat + latDelta));
+ whereClause.and(waybleFacilityMySQL.longitude.between(lon - lonDelta, lon + lonDelta));
+ // 2) 정밀 반경 필터
+ whereClause.and(distanceExpression.loe(radius));
...
- .limit(LIMIT)
+ .limit(condition.limit() != null ? condition.limit() : LIMIT)Committable suggestion skipped: line range outside the PR's diff.
| List<WaybleZoneVisitLog> visitLogs = queryFactory | ||
| .selectFrom(waybleZoneVisitLog) | ||
| .where(waybleZoneVisitLog.visitedAt.goe(thirtyDaysAgo)) | ||
| .limit(10000) | ||
| .fetch(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
방문 로그 조회 범위 과도: 후보 zoneId로 축소해 I/O/연산량 감소
현재 최근 30일 로그를 최대 10,000건까지 전부 읽습니다. 1차 후보(반경 50km, 상위 100개)에 대해서만 zoneId IN 필터를 적용하면 메모리/CPU 사용량이 크게 줄어듭니다.
다음 패치를 적용하세요:
- List<WaybleZoneVisitLog> visitLogs = queryFactory
- .selectFrom(waybleZoneVisitLog)
- .where(waybleZoneVisitLog.visitedAt.goe(thirtyDaysAgo))
- .limit(10000)
- .fetch();
+ List<Long> candidateZoneIds = nearbyZones.stream()
+ .map(WaybleZone::getId)
+ .toList();
+
+ List<WaybleZoneVisitLog> visitLogs = queryFactory
+ .selectFrom(waybleZoneVisitLog)
+ .where(
+ waybleZoneVisitLog.visitedAt.goe(thirtyDaysAgo)
+ .and(waybleZoneVisitLog.zoneId.in(candidateZoneIds))
+ )
+ .fetch();추가 최적화: 유저와 동일한 연령대/성별만 먼저 필터링하면 가중치 계산 비용도 줄일 수 있습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| List<WaybleZoneVisitLog> visitLogs = queryFactory | |
| .selectFrom(waybleZoneVisitLog) | |
| .where(waybleZoneVisitLog.visitedAt.goe(thirtyDaysAgo)) | |
| .limit(10000) | |
| .fetch(); | |
| // before: unfiltered 30-day logs up to 10,000 records | |
| - List<WaybleZoneVisitLog> visitLogs = queryFactory | |
| - .selectFrom(waybleZoneVisitLog) | |
| - .where(waybleZoneVisitLog.visitedAt.goe(thirtyDaysAgo)) | |
| - .limit(10000) | |
| // first compute the candidate zone IDs (e.g. top-100 nearby zones) | |
| List<Long> candidateZoneIds = nearbyZones.stream() | |
| .map(WaybleZone::getId) | |
| .toList(); | |
| // then restrict the visit-log query to those zones within the last 30 days | |
| List<WaybleZoneVisitLog> visitLogs = queryFactory | |
| .selectFrom(waybleZoneVisitLog) | |
| .where( | |
| waybleZoneVisitLog.visitedAt.goe(thirtyDaysAgo) | |
| .and(waybleZoneVisitLog.zoneId.in(candidateZoneIds)) | |
| ) | |
| .fetch(); |
| .orderBy(distanceExpression.asc()) // 거리순 정렬 | ||
| .offset((long) pageable.getPageNumber() * fetchSize) | ||
| .limit(fetchSize) | ||
| .fetch(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
페이지네이션 offset 계산 버그: fetchSize를 곱하면 페이지 스킵 발생
over-fetch(+1) 전략은 limit에만 적용해야 합니다. offset에 fetchSize를 곱으면 2페이지부터 1개씩 건너뛰어 누락/중복이 생깁니다. tie-breaker가 없어도 순서가 흔들릴 수 있으므로 거리 동일 시 id 추가 정렬을 권장합니다.
다음 패치를 적용하세요:
- .orderBy(distanceExpression.asc()) // 거리순 정렬
- .offset((long) pageable.getPageNumber() * fetchSize)
+ .orderBy(distanceExpression.asc(), waybleZone.id.asc()) // 거리순 정렬 + 안정적 tie-breaker
+ .offset((long) pageable.getPageNumber() * pageable.getPageSize())
.limit(fetchSize)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .orderBy(distanceExpression.asc()) // 거리순 정렬 | |
| .offset((long) pageable.getPageNumber() * fetchSize) | |
| .limit(fetchSize) | |
| .fetch(); | |
| .orderBy(distanceExpression.asc(), waybleZone.id.asc()) // 거리순 정렬 + 안정적 tie-breaker | |
| .offset((long) pageable.getPageNumber() * pageable.getPageSize()) | |
| .limit(fetchSize) | |
| .fetch(); |
🤖 Prompt for AI Agents
In
src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchMysqlRepository.java
around lines 66 to 69, the offset is incorrectly computed using fetchSize which
includes the over-fetch (+1) and causes skipped/duplicated items; change the
offset calculation to use the actual page size (pageable.getPageSize())
multiplied by pageable.getPageNumber() so only limit uses fetchSize, and add a
stable secondary sort (e.g., id ascending) after distanceExpression.asc() to
ensure deterministic ordering when distances tie.
| private static final double RADIUS = 50.0; | ||
|
|
||
| private static final Long SAMPLES = 100L; | ||
| private static final Long SAMPLES = 10000L; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
SAMPLES 10,000 + @beforeeach(매 테스트 시 재생성) → 테스트 시간/리소스 급증 위험
- 현재 매 테스트마다 문서 10k + 사용자 10k + 방문로그(최대 20/유저) + 추천로그(약 3.3k) 생성/삭제가 반복됩니다. CI 타임아웃/불안정 가능성이 큽니다.
- 성능 테스트와 기능 테스트를 분리하거나, 데이터 볼륨을 환경변수/프로파일로 제어하는 방식을 권장합니다. 기본값은 소량, perf 프로파일에서만 대량 생성이 바람직합니다.
다음과 같이 환경변수로 건드릴 수 있도록 수정 제안:
- private static final Long SAMPLES = 10000L;
+ private static final long SAMPLES =
+ Long.parseLong(System.getProperty("WAYBLE_PERF_SAMPLES", "1000"));CI 기본값은 100~1000, 로컬/성능 전용 파이프라인에서만 10000으로 올리는 운용을 권장합니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private static final Long SAMPLES = 10000L; | |
| private static final long SAMPLES = | |
| Long.parseLong(System.getProperty("WAYBLE_PERF_SAMPLES", "1000")); |
🤖 Prompt for AI Agents
In
src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java
around line 79, the static SAMPLES value of 10000 causes heavy per-test data
setup; make it configurable so CI uses a small default and perf runs can opt-in
to large volumes. Replace the hard-coded constant with a value read from an
environment variable or system property (e.g., System.getenv("TEST_SAMPLES") /
System.getProperty("test.samples")), parse to Long with a safe default of
100-1000 when unset, and document that perf/profile runs should set
TEST_SAMPLES=10000; additionally, consider switching large-data setup to
@BeforeAll or gating the heavy setup behind the env check so per-test setup only
creates large data when explicitly requested.
| private String token; | ||
|
|
||
| private static final int SAMPLES = 1000; | ||
| private static final int SAMPLES = 10000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
SAMPLES 10,000: 기본 실행에는 과합니다. 프로파일/시스템 프로퍼티로 제어를 권장
기능 검증과 성능 검증을 분리하세요. CI 기본은 500~1000, 로컬 perf 실행에서만 10000으로 올리면 안정적입니다.
- private static final int SAMPLES = 10000;
+ private static final int SAMPLES =
+ Integer.parseInt(System.getProperty("WAYBLE_SEARCH_SAMPLES", "1000"));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private static final int SAMPLES = 10000; | |
| private static final int SAMPLES = | |
| Integer.parseInt(System.getProperty("WAYBLE_SEARCH_SAMPLES", "1000")); |
🤖 Prompt for AI Agents
In
src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java
around line 83, the static constant SAMPLES set to 10000 is too large for
default CI runs; change it to read from a system property or test profile so
tests can use smaller defaults in CI and larger counts only for local perf.
Replace the hardcoded value with logic that reads a system property or
environment variable (e.g., System.getProperty("samples") or
System.getenv("SAMPLES")), parse it to int with a safe fallback (use 500–1000 as
the CI default), and document that to run perf locally set the property to 10000
or use a dedicated test profile/tag to trigger the higher sample count.
✔️ 연관 이슈
📝 작업 내용
스크린샷 (선택)
Summary by CodeRabbit
신기능
성능/개선
테스트