Skip to content

Conversation

@KiSeungMin
Copy link
Member

@KiSeungMin KiSeungMin commented Aug 26, 2025

✔️ 연관 이슈

📝 작업 내용

  • 담당한 기능을 MySQL로 동일하게 구현해, 각 기능별 성능 테스트를 진행했습니다.
  • Elastic Search가 특정 단어 포함, 거리순 등의 조회에서 특히 좋은 성능을 보여주었습니다.

스크린샷 (선택)

image image

Summary by CodeRabbit

  • 신기능

    • MySQL 기반 주변 편의시설 조회 추가로 거리 순 결과 제공
    • 웨이블 존 검색에 엔터티 기반 매핑 경로 추가로 데이터 일관성 개선
    • 유사명 검색 및 텍스트 유사도 판단으로 명칭 검색 정확도 향상
    • 개인화 추천 로직 도입으로 거리·유사도·최근성 점수를 반영한 추천 제공
  • 성능/개선

    • 하버사인 기반 거리 계산과 정렬로 근접 결과 정확도 및 응답성 개선
  • 테스트

    • 대용량 데이터 시드 및 성능 측정 추가
    • 테스트 수명주기 개선 및 데이터 정리 로직 보강

@KiSeungMin KiSeungMin self-assigned this Aug 26, 2025
@KiSeungMin KiSeungMin added the 💡 feature 기능 구현 및 개발 label Aug 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 26, 2025

Walkthrough

MySQL 기반 엔티티/리포지토리 추가, DTO에 fromEntity 팩토리 추가, MySQL/QueryDSL을 활용한 검색·추천·주변시설 조회 리포지토리 신설, 통합 테스트에서 데이터 규모 확대와 성능 측정 로깅 추가, 일부 테스트 라이프사이클 변경.

Changes

Cohort / File(s) Summary
DTO 팩토리 확장
src/main/java/.../dto/common/WaybleZoneInfoResponseDto.java, src/main/java/.../dto/facility/WaybleFacilityResponseDto.java, src/main/java/.../dto/common/FacilityResponseDto.java, src/main/java/.../dto/search/response/WaybleZoneSearchResponseDto.java
WaybleZone/WaybleFacilityMySQL 엔티티로부터 생성하는 fromEntity 메서드 추가; 검색 응답 DTO에 fromEntity 오버로드 추가; FacilityResponseDto에 불필요한 import 추가.
MySQL 엔티티/리포지토리 추가
src/main/java/.../entity/WaybleFacilityMySQL.java, src/main/java/.../repository/facility/WaybleFacilityMySQLRepository.java
JPA 엔티티 WaybleFacilityMySQL 신설 및 CRUD용 Spring Data JPA 리포지토리 추가.
주변시설 MySQL 검색
src/main/java/.../repository/facility/WaybleFacilityQuerySearchMysqlRepository.java
QueryDSL로 하버사인 거리 계산, 유형 필터, 10km 반경, 거리 오름차순, 최대 50건 조회 로직 추가. 결과를 WaybleFacilityResponseDto로 매핑.
웨이블존 MySQL 검색
src/main/java/.../repository/search/WaybleZoneQuerySearchMysqlRepository.java
QueryDSL 하버사인 기반 조건 검색(Slice 페이지네이션) 및 이름 유사 항목 단건 탐색(정규화/레벤슈타인/자카드) 로직 추가.
웨이블존 추천(MySQL)
src/main/java/.../repository/recommend/WaybleZoneQueryRecommendMysqlRepository.java
사용자 속성·방문로그 가중치·거리/유사도/최근성 스코어를 합산하는 추천 파이프라인 추가. QueryDSL 후보 조회와 Java 하버사인 보조 함수 포함.
테스트: 시설 API
src/test/java/.../WaybleFacilityApiIntegrationTest.java
MySQL 시설 데이터 시드 및 정리 추가, SAMPLES 100→1000, 테스트별 setup/teardown, 램프/엘리베이터 조회 성능 측정 타이밍 출력.
테스트: 추천 API
src/test/java/.../WaybleZoneRecommendApiIntegrationTest.java
SAMPLES 100→10000, per-test 시드/정리, 호출시간 측정 출력 추가, 데이터 생성 분포 조정.
테스트: 검색 API
src/test/java/.../WaybleZoneSearchApiIntegrationTest.java
SAMPLES 1000→10000, per-test 시드/정리, 다수 테스트에 성능 측정 타이밍 추가, 일부 검증/출력 정리.

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>
Loading
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)
Loading
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>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
MySQL로 검색창, 거리순 웨이블 검색 기능 구현 (#153)
두 기능의 성능 비교 코드 작성 (#153) 테스트에 성능 측정은 추가되었으나 ES와 MySQL 결과를 동시 비교·보고하는 명시적 비교 로직/출력 여부가 불분명함.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
웨이블존 개인화 추천 파이프라인 추가 (.../repository/recommend/WaybleZoneQueryRecommendMysqlRepository.java) linked issue는 검색 기능 및 성능 비교에 한정됨. 추천 로직 추가는 범위를 넘어섬.
주변 시설 MySQL 조회 리포지토리 추가 (.../repository/facility/WaybleFacilityQuerySearchMysqlRepository.java) 이슈 목표는 웨이블 검색/거리순 중심. 시설 검색은 명시된 범위에 없음.
신규 JPA 엔티티/리포지토리 (시설) 추가 (.../entity/WaybleFacilityMySQL.java, .../repository/facility/WaybleFacilityMySQLRepository.java) 성능 비교 목적과 직접적 연관이 불분명. 테스트 보조일 수 있으나 요구사항에 명시되지 않음.
추천 API 통합 테스트의 대규모 수정 및 성능 로깅 (.../WaybleZoneRecommendApiIntegrationTest.java) 이슈는 검색/비교에 초점. 추천 API 성능 측정은 범위 밖으로 보임.

Possibly related PRs

Suggested labels

🤔 test

Suggested reviewers

  • zyovn
  • seung-in-Yoo
  • wonjun-lee-fcwj245

Poem

새벽 쿼리 톡톡, 토끼 귀가 깜빡—
하버사인 춤추고, 점수들이 삭삭.
MySQL 길 따라, 추천도 척척!
시계는 똑딱, 성능은 번쩍—
데이터 들판에 발자국 콩닥🐇✨

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/seungmin

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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 엔티티를 받는 오버로드를 추가하면 의존/오버헤드가 줄고 가독성도 좋아집니다. (이미 WaybleZoneFacility import가 존재)

 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:23
  • WaybleZoneSearchService에서 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 간 성능 비교 자동화 및 런타임 전환이 불가능하므로, 아래와 같은 구성을 권장드립니다:

  1. @Profile("mysql")@Profile("es") 어노테이션으로 프로파일별 빈 등록
  2. 또는 Spring Cloud Feature Toggle 및 @ConditionalOnProperty 활용
  3. 공통 인터페이스(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.

📥 Commits

Reviewing files that changed from the base of the PR and between c8de428 and 692464b.

📒 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)을 적용해 롱테일 감소를 제어할 수 있습니다.

Comment on lines +37 to +51
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();
}
Copy link

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=java

Length 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.

Comment on lines +28 to +56
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();
}
Copy link

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.

Comment on lines +59 to +64
List<WaybleZoneVisitLog> visitLogs = queryFactory
.selectFrom(waybleZoneVisitLog)
.where(waybleZoneVisitLog.visitedAt.goe(thirtyDaysAgo))
.limit(10000)
.fetch();

Copy link

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.

Suggested change
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();

Comment on lines +66 to +69
.orderBy(distanceExpression.asc()) // 거리순 정렬
.offset((long) pageable.getPageNumber() * fetchSize)
.limit(fetchSize)
.fetch();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

페이지네이션 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.

Suggested change
.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;
Copy link

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.

Suggested change
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;
Copy link

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.

Suggested change
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.

@KiSeungMin KiSeungMin merged commit 8f56366 into develop Aug 26, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💡 feature 기능 구현 및 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants