Skip to content

♻️Refactor: 내주변 오디오관광지 수정#113

Merged
joonyee merged 1 commit intodevelopfrom
refactor/popular-spots
Sep 22, 2025
Merged

♻️Refactor: 내주변 오디오관광지 수정#113
joonyee merged 1 commit intodevelopfrom
refactor/popular-spots

Conversation

@joonyee
Copy link
Contributor

@joonyee joonyee commented Sep 22, 2025

불필요한 외부 api호출 삭제 -> 외부API, 내부 쿼리 1번씩 호출로 속도 개선

Summary by CodeRabbit

  • Refactor
    • 근처 오디오 가이드 목록 로딩을 단순화해 불필요한 추가 조회를 제거했습니다.
    • 목록 표시 속도를 개선하고 로딩 지연·타임아웃 발생 가능성을 낮췄습니다.
    • 네트워크 트래픽과 데이터 사용량을 줄여 배터리 소모를 완화했습니다.
    • 콘텐츠 제목·이미지 표시의 일관성을 유지하면서 응답 안정성을 높였습니다.

@joonyee joonyee self-assigned this Sep 22, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 22, 2025

Walkthrough

getNearbySpotsWithAudioGuides에서 각 스팟마다 수행하던 getSpotByContentId 호출과 상세 조회 로직을 제거하고, SpotResponse의 필드만으로 NearbyAudioSpotResponse를 바로 구성하도록 내부 매핑을 단순화했습니다. 퍼블릭 API 시그니처 변경은 없습니다.

Changes

Cohort / File(s) Summary
스팟 오디오 가이드 매핑 단순화
src/main/java/com/yfive/gbjs/domain/spot/service/SpotServiceImpl.java
per-spot 상세 조회(getSpotByContentId) 제거, SpotResponse의 contentId/title/imageUrl로 NearbyAudioSpotResponse 직접 생성. 불필요한 상세 데이터 접근 및 거리 처리 로직 제거. 공개 API 시그니처 변화 없음.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant S as SpotServiceImpl
  participant R as SpotRepository
  participant D as DetailService (이전)

  rect rgb(245,245,255)
  note over C,S: 이전 흐름 (per-spot 상세 조회 포함)
  C->>S: getNearbySpotsWithAudioGuides()
  S->>R: findNearbySpots(...)
  R-->>S: List<SpotResponse>
  loop for each spot
    S->>D: getSpotByContentId(contentId)
    D-->>S: SpotDetailResponse
    S-->>C: (누적) NearbyAudioSpotResponse 항목 생성
  end
  end
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant S as SpotServiceImpl
  participant R as SpotRepository

  rect rgb(240,255,240)
  note over C,S: 변경 후 흐름 (직접 매핑)
  C->>S: getNearbySpotsWithAudioGuides()
  S->>R: findNearbySpots(...)
  R-->>S: List<SpotResponse>
  S-->>C: SpotResponse만으로 NearbyAudioSpotResponse 리스트 구성
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested labels

♻️ refactor, 🟠 priority: medium

Poem

툭, 당근 한 입 베어 물고 코를 씰룩—
스팟 길 묻던 발걸음, 이제 곧장 쓱쓱!
돌아가던 길을 접고, 지도만 보고 찍!
상세는 잠시 안녕, 응답은 더 또렷하게.
폴짝 폴짝—빠른 길로! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목 "♻️Refactor: 내주변 오디오관광지 수정"은 PR의 핵심인 내주변 오디오관광지 기능의 리팩터링(불필요한 외부 API 호출 제거 및 성능 개선)을 짧고 명확하게 요약하고 있으며 PR 설명과 일치합니다. 제목이 간결해 변경의 주된 목적을 빠르게 전달하므로 히스토리를 훑는 동료가 주요 변경사항을 이해하기 쉽습니다. 이모지 사용이 약간의 시각적 노이즈를 추가하나 의미 전달에는 지장이 없습니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/popular-spots

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@joonyee joonyee added the ♻️ refactor 리팩토링 작업 label Sep 22, 2025
@github-actions
Copy link

Test Results

0 tests   0 ✅  0s ⏱️
0 suites  0 💤
0 files    0 ❌

Results for commit 5c68d04.

Copy link
Contributor

@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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/yfive/gbjs/domain/spot/service/SpotServiceImpl.java (1)

518-520: N+1 내부 쿼리: existsByContentId 반복 호출 제거 필요

getNearbySpotsWithAudioGuides에서 이미 배치 조회(audioGuideRepository.findContentIdsWithAudioGuidesByContentIdIn)를 수행(SpotServiceImpl.java:436-439)하는데도 fetchLocationBasedSpots → mapJsonNodeToSpotResponse에서 각 item마다 audioGuideRepository.existsByContentId(...)를 호출해 N+1 쿼리가 발생합니다 (exists 호출 위치: SpotServiceImpl.java:152, 518-520). fetchLocationBasedSpots 호출부(라인 ~428-431)와 정의(라인 ~458-492), mapJsonNodeToSpotResponse 정의(라인 ~502-520)를 확인해 조치하세요.

권장: ttsExist 계산을 옵셔널 플래그로 만들고, getNearbySpotsWithAudioGuides에서는 조회를 비활성화(또는 contentIds 세트를 전달해 메모리 체크)하여 내부 쿼리를 제거.

-  private List<SpotResponse> fetchLocationBasedSpots(
-      Double latitude, Double longitude, String radius) {
+  private List<SpotResponse> fetchLocationBasedSpots(
+      Double latitude, Double longitude, String radius) {
+    // 기존 시그니처는 보존하고, 내부에서 includeTtsExist=false를 전달하는 오버로드 추가를 아래에 제안합니다.

오버로드 및 플래그 전파:

@@
   public List<NearbyAudioSpotResponse> getNearbySpotsWithAudioGuides(
       Double latitude, Double longitude) {
-    List<SpotResponse> allSpots = fetchLocationBasedSpots(latitude, longitude, "10000");
+    List<SpotResponse> allSpots = fetchLocationBasedSpots(latitude, longitude, "10000", false);
@@
+  // 필요 시 ttsExist 계산 포함 여부를 제어하는 오버로드
+  private List<SpotResponse> fetchLocationBasedSpots(
+      Double latitude, Double longitude, String radius, boolean includeTtsExist) {
+    String endpoint = "/locationBasedList2";
+    UriComponentsBuilder uriBuilder =
+        UriComponentsBuilder.fromUriString(spotApiUrl + endpoint)
+            .queryParam("serviceKey", serviceKey)
+            .queryParam("numOfRows", 1000)
+            .queryParam("pageNo", 1)
+            .queryParam("MobileOS", "WEB")
+            .queryParam("MobileApp", "gbjs")
+            .queryParam("_type", "JSON")
+            .queryParam("arrange", "O")
+            .queryParam("mapX", longitude)
+            .queryParam("mapY", latitude)
+            .queryParam("radius", radius);
+
+    String response =
+        restClient.get().uri(uriBuilder.build(true).toUri()).retrieve().body(String.class);
+
+    validateApiResponse(response);
+
+    try {
+      JsonNode root = objectMapper.readTree(response);
+      JsonNode items = root.path("response").path("body").path("items").path("item");
+
+      List<SpotResponse> spotResponses = new ArrayList<>();
+      for (JsonNode item : items) {
+        JsonNode imageNode = item.get("firstimage");
+        if (imageNode == null || imageNode.isNull() || imageNode.asText().isEmpty()) {
+          continue;
+        }
+        SpotResponse spotResponse = mapJsonNodeToSpotResponse(item, latitude, longitude, includeTtsExist);
+        spotResponses.add(spotResponse);
+      }
+      return spotResponses;
+    } catch (Exception e) {
+      log.error("위치 기반 관광지 파싱 실패", e);
+      throw new CustomException(SpotErrorStatus.SPOT_API_ERROR);
+    }
+  }

기존 호출자는 그대로 사용 가능하게 기존 메서드는 오버로드 호출:

   private List<SpotResponse> fetchLocationBasedSpots(
       Double latitude, Double longitude, String radius) {
-    String endpoint = "/locationBasedList2";
-    ...
-    // (기존 본문 전체)
+    return fetchLocationBasedSpots(latitude, longitude, radius, true);
   }

mapJsonNodeToSpotResponse를 플래그로 조건부 조회:

-  private SpotResponse mapJsonNodeToSpotResponse(JsonNode item, Double latitude, Double longitude)
+  private SpotResponse mapJsonNodeToSpotResponse(
+      JsonNode item, Double latitude, Double longitude, boolean includeTtsExist)
       throws com.fasterxml.jackson.core.JsonProcessingException {
     SpotResponse spotResponse = objectMapper.treeToValue(item, SpotResponse.class);
@@
-    boolean ttsExist = audioGuideRepository.existsByContentId(item.get("contentid").asLong());
-    spotResponse.setTtsExist(ttsExist);
+    if (includeTtsExist) {
+      boolean ttsExist = audioGuideRepository.existsByContentId(item.get("contentid").asLong());
+      spotResponse.setTtsExist(ttsExist);
+    }
     return spotResponse;

수정 위치 참고: src/main/java/com/yfive/gbjs/domain/spot/service/SpotServiceImpl.java (getNearbySpotsWithAudioGuides 호출: ~라인 428-431, 배치 조회: ~라인 436-439, fetchLocationBasedSpots 정의: ~라인 458-492, mapJsonNodeToSpotResponse: ~라인 502-520).

🧹 Nitpick comments (5)
src/main/java/com/yfive/gbjs/domain/spot/service/SpotServiceImpl.java (5)

441-447: 정렬 전에 필터링하여 불필요한 비교 줄이기

오디오가이드가 있는 스팟만 먼저 걸러낸 뒤 정렬하면 비교 횟수/메모리 사용을 줄일 수 있습니다(최대 1000건 기준 체감은 작지만 안전한 미세 최적화).

-    return allSpots.stream()
-        .sorted(
-            Comparator.comparing(
-                SpotResponse::getDistance, Comparator.nullsLast(Double::compareTo)))
-        .filter(
-            spot -> contentIdsWithAudioGuides.contains(spot.getSpotId()))
+    return allSpots.stream()
+        .filter(spot -> contentIdsWithAudioGuides.contains(spot.getSpotId()))
+        .sorted(Comparator.comparing(SpotResponse::getDistance, Comparator.nullsLast(Double::compareTo)))
         .limit(5)
         .map(

433-439: IN 절 축소: spotId 중복 제거 및 null 방지

중복 spotId가 존재하면 불필요하게 큰 IN 쿼리가 생성됩니다. distinct(+null 필터)를 권장합니다.

-    List<Long> allSpotIds =
-        allSpots.stream().map(SpotResponse::getSpotId).collect(Collectors.toList());
+    List<Long> allSpotIds = allSpots.stream()
+        .map(SpotResponse::getSpotId)
+        .filter(java.util.Objects::nonNull)
+        .distinct()
+        .toList();

distinct 적용 후 리포지토리 메서드가 빈 리스트 입력을 안전하게 처리하는지 확인 부탁드립니다.


430-436: 빈 결과 빠른 반환으로 불필요한 처리 회피

후속 쿼리/정렬을 건너뛸 수 있습니다.

   public List<NearbyAudioSpotResponse> getNearbySpotsWithAudioGuides(
       Double latitude, Double longitude) {
-    List<SpotResponse> allSpots = fetchLocationBasedSpots(latitude, longitude, "10000");
+    List<SpotResponse> allSpots = fetchLocationBasedSpots(latitude, longitude, "10000");
+    if (allSpots.isEmpty()) {
+      return List.of();
+    }

358-363: 페이지네이션에서 항목당 외부 API 2회 호출(상세+분류)로 인한 비용

paginateSpotResponses는 각 항목마다 getSpotByContentId를 호출하고, 내부에서 다시 fetchSpotType까지 호출되어 외부 API가 항목당 최대 2회 발생합니다. 페이지 크기에 비례해 지연이 커집니다.

  • 단기: 유형(type) 표시가 목록 필수 요구가 아니라면 지연 로딩 또는 비동기 보강을 고려.
  • 중기: contentTypeId+cat1/2/3 조합별로 fetchSpotType 결과를 캐시(예: Map<Triple, String>), 또는 고정 사전 테이블로 대체.

필요 시 캐시 스케치 제공 가능합니다.


496-499: 예외 로깅 누락

파싱 실패 시 로그가 없어 트러블슈팅이 어렵습니다. 다른 메서드들처럼 에러 로그를 남겨 주세요.

-    } catch (Exception e) {
-      throw new CustomException(SpotErrorStatus.SPOT_API_ERROR);
+    } catch (Exception e) {
+      log.error("위치 기반 관광지 파싱 실패", e);
+      throw new CustomException(SpotErrorStatus.SPOT_API_ERROR);
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f6cebf and 5c68d04.

📒 Files selected for processing (1)
  • src/main/java/com/yfive/gbjs/domain/spot/service/SpotServiceImpl.java (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build and Test
🔇 Additional comments (1)
src/main/java/com/yfive/gbjs/domain/spot/service/SpotServiceImpl.java (1)

449-454: 확인: SpotResponse.contentid 매핑은 OK — NearbyAudioSpotResponse에 distance/type 미포함 (검증 필요)

  • SpotResponse.spotId는 @JsonProperty("contentid")로 매핑되어 있어 spot.getSpotId()를 contentId로 사용하는 것은 적절합니다. (src/main/java/com/yfive/gbjs/domain/spot/dto/response/SpotResponse.java)
  • NearbyAudioSpotResponse는 현재 contentId, title, imageUrl만 선언되어 있어 distance/type 필드가 없습니다. (src/main/java/com/yfive/gbjs/domain/spot/dto/response/NearbyAudioSpotResponse.java)
  • 계약상 distance(또는 type)가 필요하면 DTO에 필드 추가 후 SpotServiceImpl 매핑에 아래처럼 추가하십시오:
             spot ->
                 NearbyAudioSpotResponse.builder()
                     .contentId(spot.getSpotId())
                     .title(spot.getTitle())
                     .imageUrl(spot.getImageUrl())
+                    .distance(spot.getDistance())  // DTO에 필드가 있을 때만
                     .build())

@joonyee joonyee merged commit 9f1870d into develop Sep 22, 2025
5 checks passed
@joonyee joonyee deleted the refactor/popular-spots branch September 24, 2025 13:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

♻️ refactor 리팩토링 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant