Skip to content

[REFACTOR] Google Place 호출 구조 최적화 및 배치 전용 모듈 도입#146

Merged
parkmineum merged 7 commits intodevfrom
refactor/#145_matzip
Jan 11, 2026
Merged

[REFACTOR] Google Place 호출 구조 최적화 및 배치 전용 모듈 도입#146
parkmineum merged 7 commits intodevfrom
refactor/#145_matzip

Conversation

@parkmineum
Copy link
Member

@parkmineum parkmineum commented Jan 11, 2026

🎋 이슈 및 작업중인 브랜치

🔑 주요 내용

@nitronium102

Google Places API 호출 최적화

  • Text Search (New) 도입: 기존 Text Search + Place Details 2단계 호출 구조를 Text Search (New) 1단계로 통합
  • Field Mask 최적화: 단일 호출에서 사진, 위치, 평점, 영업시간 정보를 모두 가져오도록 Field Mask를 설정하여 건당 호출 비용을 약 83% 절감 ($0.160 → $0.035)
  • 이미지 보안 및 캐싱: API Key 노출 방지를 위해 이미지 프록시 로직을 유지하고, 클라이언트에게는 완성된 서버 사이드 URL을 제공

배치(Batch) 전용 모듈 신설(ssolv-api-place 모듈 의존)

  • ssolv-batch 모듈 추가: API 서버와 배치 작업 서버를 물리적으로 분리하여 배포 및 성능을 관리할 수 있도록 구조를 개선

캐싱 및 구글 약관(ToS) 준수

  • 30일 유효기간 적용: DB에 저장된 장소 정보는 30일간만 유효하게 관리하며, 30일 이내의 정보는 구글 API 재호출 없이 DB 데이터를 활용하도록 캐싱 효율을 높임
  • 자동 Cleanup 로직: 배치 모듈을 통해 매일 새벽 3시, 30일이 경과한 데이터를 자동으로 정리

Check List

  • Assignees 등록을 하였나요?
  • 라벨(Label) 등록을 하였나요?
  • PR 머지하기 전 반드시 CI가 정상적으로 작동하는지 확인해주세요!

Summary by CodeRabbit

  • New Features

    • 주기적 스케줄러로 오래된 장소 데이터를 자동 삭제하도록 추가되었습니다.
  • Refactor

    • 장소 검색 흐름을 텍스트 검색 기반의 로컬 저장 중심으로 단순화하고 외부 상세 호출 경로를 정리했습니다.
    • 사진 처리 경로를 프록시 기반 URL 생성으로 정비했습니다.
  • Chores

    • 배치 모듈 및 빌드 설정이 추가되어 배치 배포 구성이 향상되었습니다.
    • .gitignore에 전역 bin 디렉터리 패턴이 추가되었습니다.
  • Tests

    • 장소 검색 관련 테스트가 새로운 흐름에 맞게 수정·정비되었습니다.

✏️ Tip: You can customize this high-level summary in your review settings.

@parkmineum parkmineum self-assigned this Jan 11, 2026
@parkmineum parkmineum added the ♻️ REFACTOR 리팩토링 관련 라벨 label Jan 11, 2026
@parkmineum parkmineum linked an issue Jan 11, 2026 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Jan 11, 2026

Walkthrough

장소 검색을 상세정보 기반에서 텍스트검색 기반 워크플로우로 전환하고, 새로운 ssolv-batch 모듈 및 정기 삭제 스케줄러를 추가하며, PlaceAddressResolver·PlaceDetailsProcessor 등 관련 인프라/테스트 코드를 대거 제거·단순화했습니다.

Changes

코호트 / 파일(s) 변경 요약
구성 변경
\.gitignore, settings.gradle.kts
*/bin 글로벌 무시 패턴 추가 및 ssolv-batch 모듈 포함
애플리케이션 설정
ssolv-api-core/.../CoreApiApplication.kt
@EnableScheduling 어노테이션 제거
배치 모듈 추가
ssolv-batch/build.gradle.kts, ssolv-batch/src/main/kotlin/.../PlaceDataCleanupScheduler.kt, ssolv-batch/src/main/kotlin/.../MeetingPlaceSearchCleanupScheduler.kt
새 모듈 추가, Jib 플러그인 및 MySQL 런타임 의존성 설정, 스케줄러(정기 삭제) 구현 및 패키지 이동
API-Place 모듈 리팩토링
ssolv-api-place/build.gradle.kts, ssolv-api-place/.../ExecutePlaceSearchService.kt, ssolv-api-place/.../PlacesSearchController.kt
ssolv-batch 의존성 추가, PlaceDetailsProcessor 제거 → DB에 저장된 텍스트검색 결과 기반 처리로 변경, GooglePlacesApiProperties 주입, OpenAPI 설명 문구 수정
API-Place 테스트 변경
ssolv-api-place/.../ExecutePlaceSearchServiceTest.kt, ssolv-api-place/.../PlaceTestDataFactory.kt
테스트에서 PlaceDetailsProcessor 제거 및 GooglePlacesApiProperties 모킹 추가, FakePlaceQuery 확장, 테스트 데이터에 위치·사진 정보 추가 및 기대값 조정
인프라: PlaceQuery 리팩토링
ssolv-infrastructure/.../PlaceQuery.kt
병렬 상세 조회·주소 해석 로직 제거, savePlacesFromTextSearch() 추가, PlaceAddressResolver 의존성 제거, 공개 API 축소 및 내부 헬퍼 다수 제거
인프라: GooglePlacesClient 단순화
ssolv-infrastructure/.../client/GooglePlacesClient.kt
Place Details 및 Nearby 검색 메서드 제거, 텍스트검색 필드 마스크에 places.photos·places.location 추가, retry/error 분기 간소화
인프라: 저장소·모델 변경
ssolv-infrastructure/.../place/PlaceJpaRepository.kt, ssolv-infrastructure/.../model/PlacesTextSearchResponse.kt
deleteByUpdatedAtBefore(LocalDateTime) 추가(단일 조회 메서드 제거), 텍스트검색 응답 모델에 location·photos 등 필드 추가
인프라: 컴포넌트 삭제
ssolv-infrastructure/.../util/PlaceAddressResolver.kt, ssolv-infrastructure/.../util/PlaceDetailsProcessor.kt
두 파일 완전 제거(주소 해석 및 상세정보 병렬 처리 로직 삭제)
테스트 정리/전환
ssolv-infrastructure/src/test/kotlin/.../PlaceQueryRetryTest.kt, .../client/GooglePlacesClientRetryTest.kt, .../client/GooglePlacesClientTest.kt
병렬 상세조회 관련 테스트 삭제 또는 텍스트검색 중심으로 단순화/전환

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 주요 변경 사항인 Google Place API 호출 구조 최적화 및 배치 모듈 도입을 명확하게 요약하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#145_matzip

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.

@github-actions
Copy link

github-actions bot commented Jan 11, 2026

🧪 테스트 결과

147 tests   147 ✅  31s ⏱️
 32 suites    0 💤
 32 files      0 ❌

Results for commit f998d90.

♻️ This comment has been updated with latest results.

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: 5

Caution

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

⚠️ Outside diff range comments (1)
ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/client/GooglePlacesClient.kt (1)

210-221: currentOpeningHours가 Field Mask에서 누락됨

buildTextSearchFieldMask()에서 places.currentOpeningHours가 제거되었지만, PlaceQuery.savePlacesFromTextSearch()에서는 여전히 place.currentOpeningHours?.openNow를 사용하고 있습니다 (Line 62). 이로 인해 openNow 값이 항상 null이 됩니다.

🐛 Field Mask에 currentOpeningHours 추가
 private fun buildTextSearchFieldMask(): String {
     return listOf(
         "places.id",
         "places.displayName",
         "places.formattedAddress",
         "places.rating",
         "places.userRatingCount",
         "places.photos",
         "places.location",
-        "places.types"
+        "places.types",
+        "places.currentOpeningHours"
     ).joinToString(",")
 }
🤖 Fix all issues with AI agents
In @ssolv-api-place/build.gradle.kts:
- Line 7: ssolv-api-place currently declares
implementation(project(":ssolv-batch")), which reverses the typical dependency
direction; confirm whether ssolv-api-place should depend on ssov-batch or
whether ssov-batch should depend on ssolv-api-place instead. If the batch module
should consume services/repositories from the API module, remove
implementation(project(":ssolv-batch")) from the ssov-api-place build.gradle.kts
and add implementation(project(":ssolv-api-place")) to the ssov-batch module,
then run a full build to verify no cycles and update module documentation to
reflect the intended architecture.

In
@ssolv-api-place/src/main/kotlin/org/depromeet/team3/place/application/execution/ExecutePlaceSearchService.kt:
- Around line 109-151: The mapping in ExecutePlaceSearchService's
savedEntities.mapNotNull assumes entity.address is non-null and calls
entity.address.replace(...), which can throw an NPE; update the mapping to
null-safely handle address (e.g., use entity.address?.replace("대한민국 ", "") ?: ""
or a sensible fallback) before passing into PlacesSearchResponse.PlaceItem, and
similarly ensure other fields used without safe calls (like converting
ratings/text) are guarded or defaulted; locate this logic inside the lambda that
builds PlacesSearchResponse.PlaceItem in ExecutePlaceSearchService and apply the
null-safe transformations/fallbacks.

In
@ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/model/PlacesTextSearchResponse.kt:
- Around line 21-28: The PlacesTextSearchResponse data model declares the
location property as non-nullable but Google Places Text Search can omit
location; change the declaration of location in PlacesTextSearchResponse to be
nullable (location: Location?) and update any constructors/usages and JSON
(de)serialization handling to accept nulls; ensure the nested data class
Location remains unchanged but any code that dereferences
PlacesTextSearchResponse.location (e.g., mapping, distance calculations or
serialization code) is null-checked or guarded to avoid NPEs.
- Around line 37-41: AuthorAttribution's fields are non-nullable but the Google
Places API can omit them; change the three properties in the data class
AuthorAttribution (displayName, uri, photoUri) from String to nullable String?
so the JSON deserializer won't fail when those keys are missing, and consider
leaving them without defaults or adding explicit null defaults if you prefer
(e.g., displayName: String?).
🧹 Nitpick comments (10)
.gitignore (1)

68-68: 배치 모듈 아티팩트 무시 패턴 추가 - 승인됨

새로운 */bin 패턴이 배치 모듈 및 기타 모듈에서 생성되는 bin 디렉토리를 올바르게 제외합니다. PR 목표와 완벽하게 부합합니다.

선택 사항: 파일 내 다른 디렉토리 패턴들(예: build/, out/)과의 스타일 일관성을 위해 후행 슬래시를 추가하는 것을 고려해보세요 (*/bin/). 두 형식 모두 동작하지만, 디렉토리임을 더 명확하게 나타냅니다.

ssolv-batch/src/main/resources/application.yml (3)

5-9: 배치 작업을 위한 커넥션 풀 설정을 추가하세요.

배치 모듈은 스케줄된 작업을 수행하며 장시간 실행될 수 있습니다. 현재 커넥션 풀 관련 설정이 없어 커넥션 고갈이나 타임아웃 문제가 발생할 수 있습니다. HikariCP 설정을 추가하여 안정적인 데이터베이스 연결 관리를 권장합니다.

♻️ 권장 커넥션 풀 설정 추가
   datasource:
     url: jdbc:mysql://localhost:3306/${DEFAULT_SCHEMA}
     username: ${DB_USERNAME}
     password: ${DB_PASSWORD}
     driver-class-name: com.mysql.cj.jdbc.Driver
+    hikari:
+      maximum-pool-size: 10
+      connection-timeout: 30000
+      idle-timeout: 600000
+      max-lifetime: 1800000

11-14: 배치 처리 최적화를 위한 JPA 설정 개선을 고려하세요.

현재 설정에 몇 가지 개선 사항을 제안합니다:

  1. show-sql: true는 배치 작업 중 대량의 SQL 로그를 생성할 수 있어 성능과 로그 관리에 영향을 줄 수 있습니다.
  2. 배치 처리에서 일반적인 대량 작업(bulk insert/update)을 위한 batch-size 설정이 누락되어 있습니다.
♻️ 배치 최적화 설정 제안
   jpa:
     hibernate:
       ddl-auto: none
-    show-sql: true
+    show-sql: false
+    properties:
+      hibernate:
+        jdbc:
+          batch_size: 50
+        order_inserts: true
+        order_updates: true

16-17: 환경별 설정 관리를 위해 Spring Profile 활용을 권장합니다.

app.env 값이 "local"로 하드코딩되어 있어 다른 환경(dev, staging, production)에 배포 시 혼란을 야기할 수 있습니다. Spring Profile을 활용하거나 환경 변수로 외부화하여 환경별 설정을 명확히 관리하는 것을 권장합니다.

♻️ 환경 설정 개선 방안

방안 1: 환경 변수로 외부화

 app:
-  env: local
+  env: ${APP_ENV:local}

방안 2: Spring Profile 활용

현재 파일을 application.yml로 유지하고, 환경별 파일 추가:

  • application-local.yml
  • application-dev.yml
  • application-prod.yml

그리고 실행 시 -Dspring.profiles.active={profile} 옵션 사용

ssolv-api-place/src/main/kotlin/org/depromeet/team3/place/controller/PlacesSearchController.kt (1)

48-51: OpenAPI 설명 텍스트가 불완전해 보입니다.

라인 50의 description 값이 "반환합니다. "로 끝나며 의미 있는 내용이 없습니다. PR 요약에서 서버 측 이미지 프록시 로직이 유지된다고 했으므로, 설명을 더 구체적으로 보완하는 것을 권장합니다. 예를 들어:

  • "구글 플레이스 사진을 서버 프록시를 통해 반환합니다."
  • "구글 플레이스 사진을 프록시하여 반환합니다. 응답은 30일 동안 캐시됩니다."
ssolv-infrastructure/src/test/kotlin/org/depromeet/team3/place/client/GooglePlacesClientTest.kt (1)

33-66: 테스트 커버리지 보완 권장

텍스트 검색 성공 시나리오만 테스트되고 있습니다. 다음 케이스들에 대한 추가 테스트를 고려해 주세요:

  • 빈 결과 반환 시 (places = emptyList())
  • places = null 반환 시
  • 위치 바이어스(locationBias) 파라미터 사용 시
ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/PlaceQuery.kt (1)

48-51: 30일 캐시 검증 로직 개선 권장

LocalDateTime.now()를 반복 호출하면 각 place마다 미세하게 다른 시간이 사용될 수 있습니다. 일관성을 위해 메서드 시작 시점에 한 번만 호출하는 것이 좋습니다.

♻️ 개선 제안
 ): List<PlaceEntity> = withContext(Dispatchers.IO) {
     if (places.isEmpty()) return@withContext emptyList()
+    
+    val now = LocalDateTime.now()
+    val cacheThreshold = now.minusDays(30)

     val googlePlaceIds = places.map { it.id }
     val existingPlaces = placeJpaRepository.findByGooglePlaceIdIn(googlePlaceIds)
         .associateBy { it.googlePlaceId }

     val entities = places.map { place ->
         val existing = existingPlaces[place.id]
         val lastUpdated = existing?.updatedAt ?: LocalDateTime.MIN

         // 30일 이내 데이터가 있으면 업데이트 스킵
-        if (existing != null && lastUpdated.isAfter(LocalDateTime.now().minusDays(30))) {
+        if (existing != null && lastUpdated.isAfter(cacheThreshold)) {
             return@map existing
         }
ssolv-api-place/src/main/kotlin/org/depromeet/team3/place/application/execution/ExecutePlaceSearchService.kt (1)

499-499: 에러 메시지의 상세 정보 개선

에러 상황에서 detail 맵에 error를 포함시키고 있습니다. 일관성을 위해 다른 예외 처리 부분(Line 479)과 동일한 키 구조를 사용하는 것이 좋습니다.

에러 detail 키가 일관되게 사용되고 있는지 확인하고, 필요시 통일하는 것을 고려하세요.

ssolv-batch/src/main/kotlin/org/depromeet/team3/batch/scheduler/PlaceDataCleanupScheduler.kt (1)

22-33: @Transactional@Scheduled 조합 시 주의 필요

@Scheduled 메서드에서 @Transactional을 직접 사용하면 프록시 우회 문제가 발생할 수 있습니다. Spring의 @Scheduled가 같은 빈 내부에서 메서드를 호출하면 프록시를 거치지 않아 트랜잭션이 적용되지 않을 수 있습니다.

또한, try-catch 블록이 @Transactional 메서드 내부에 있어서 예외 발생 시 트랜잭션 롤백이 이루어지지 않습니다. deleteByUpdatedAtBefore가 부분 삭제 후 실패하면 일부 데이터만 삭제된 상태로 커밋될 수 있습니다.

♻️ 권장 수정안
 @Component
 class PlaceDataCleanupScheduler(
     private val placeJpaRepository: PlaceJpaRepository
 ) {
     private val logger = LoggerFactory.getLogger(PlaceDataCleanupScheduler::class.java)

     @Scheduled(cron = "0 0 3 * * *")
-    @Transactional
     fun cleanupStalePlaceData() {
         val thirtyDaysAgo = LocalDateTime.now().minusDays(30)

         try {
-            val deletedCount = placeJpaRepository.deleteByUpdatedAtBefore(thirtyDaysAgo)
+            val deletedCount = doCleanup(thirtyDaysAgo)
             logger.info("30일 경과 장소 데이터 삭제 완료: {}건", deletedCount)
         } catch (e: Exception) {
             logger.error("장소 데이터 정리 중 오류 발생", e)
         }
     }
+
+    @Transactional
+    fun doCleanup(cutoffDate: LocalDateTime): Int {
+        return placeJpaRepository.deleteByUpdatedAtBefore(cutoffDate)
+    }
 }
ssolv-api-place/src/test/kotlin/org/depromeet/team3/place/application/execution/ExecutePlaceSearchServiceTest.kt (1)

3-5: 사용되지 않는 import 확인 필요

DispatcherswithContext가 import되었지만 이 테스트 파일에서 직접 사용되지 않는 것으로 보입니다. 실제로 사용되지 않는다면 제거를 고려하세요.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba58044 and eac22a3.

📒 Files selected for processing (22)
  • .gitignore
  • settings.gradle.kts
  • ssolv-api-core/src/main/kotlin/org/depromeet/team3/CoreApiApplication.kt
  • ssolv-api-place/build.gradle.kts
  • ssolv-api-place/src/main/kotlin/org/depromeet/team3/place/application/execution/ExecutePlaceSearchService.kt
  • ssolv-api-place/src/main/kotlin/org/depromeet/team3/place/controller/PlacesSearchController.kt
  • ssolv-api-place/src/test/kotlin/org/depromeet/team3/place/application/execution/ExecutePlaceSearchServiceTest.kt
  • ssolv-api-place/src/test/kotlin/org/depromeet/team3/place/util/PlaceTestDataFactory.kt
  • ssolv-batch/build.gradle.kts
  • ssolv-batch/src/main/kotlin/org/depromeet/team3/BatchApplication.kt
  • ssolv-batch/src/main/kotlin/org/depromeet/team3/batch/scheduler/MeetingPlaceSearchCleanupScheduler.kt
  • ssolv-batch/src/main/kotlin/org/depromeet/team3/batch/scheduler/PlaceDataCleanupScheduler.kt
  • ssolv-batch/src/main/resources/application.yml
  • ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/PlaceJpaRepository.kt
  • ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/PlaceQuery.kt
  • ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/client/GooglePlacesClient.kt
  • ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/model/PlacesTextSearchResponse.kt
  • ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/util/PlaceAddressResolver.kt
  • ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/util/PlaceDetailsProcessor.kt
  • ssolv-infrastructure/src/test/kotlin/org/depromeet/team3/place/PlaceQueryRetryTest.kt
  • ssolv-infrastructure/src/test/kotlin/org/depromeet/team3/place/client/GooglePlacesClientRetryTest.kt
  • ssolv-infrastructure/src/test/kotlin/org/depromeet/team3/place/client/GooglePlacesClientTest.kt
💤 Files with no reviewable changes (4)
  • ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/util/PlaceDetailsProcessor.kt
  • ssolv-infrastructure/src/test/kotlin/org/depromeet/team3/place/PlaceQueryRetryTest.kt
  • ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/util/PlaceAddressResolver.kt
  • ssolv-api-core/src/main/kotlin/org/depromeet/team3/CoreApiApplication.kt
⏰ 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: test
🔇 Additional comments (23)
ssolv-batch/src/main/resources/application.yml (1)

1-3: 애플리케이션 이름 설정이 적절합니다.

배치 모듈명과 일치하며 명확하게 구성되어 있습니다.

ssolv-api-place/src/main/kotlin/org/depromeet/team3/place/controller/PlacesSearchController.kt (2)

59-62: 캐싱 전략이 PR 요구사항과 일치합니다.

30일 최대 캐시 설정이 PR 요약의 "DB에 저장된 장소 정보는 30일 유효기간 적용" 정책과 일관성 있게 구현되었습니다. cachePublic() 설정도 적절합니다.


53-57: PlacePhotoService를 통한 프록시 구현이 올바른 구조입니다.

photoName 경로 매개변수는 @GetMapping("/photos/{photoName:.+}") 정규식으로 Google Places 리소스 이름의 전체 경로(places/ChIJ.../photos/AbCdEf 형식)를 정확히 캡처하며, PlacePhotoService에서 "places/" 접두사 검증과 함께 올바르게 처리됩니다. ExecutePlaceSearchService에서 PlaceFormatter를 통해 생성된 프록시 URL은 {proxyBaseUrl}/v1/places/photos/{photoName} 형식으로, 컨트롤러가 추출한 photoName이 원본 Google Places 리소스 이름을 그대로 포함하므로 매핑이 정상 동작합니다. 서버 측 이미지 프록시 로직 유지 요구사항이 잘 지켜지고 있습니다.

ssolv-batch/src/main/kotlin/org/depromeet/team3/batch/scheduler/MeetingPlaceSearchCleanupScheduler.kt (1)

22-33: LGTM!

스케줄러 로직이 적절하게 구현되어 있습니다. 트랜잭션 처리와 예외 처리가 잘 되어 있습니다.

ssolv-batch/src/main/kotlin/org/depromeet/team3/BatchApplication.kt (1)

1-13: LGTM!

배치 모듈의 진입점이 적절하게 구성되어 있습니다. @EnableScheduling이 API 모듈에서 배치 모듈로 이동한 것이 명확합니다.

ssolv-infrastructure/src/test/kotlin/org/depromeet/team3/place/client/GooglePlacesClientRetryTest.kt (2)

37-60: LGTM!

500 에러 후 재시도 성공 시나리오가 올바르게 테스트되고 있습니다. POST 기반 텍스트 검색 플로우에 맞게 mock이 적절히 구성되었습니다.


62-85: LGTM!

최대 재시도 횟수 초과 시나리오가 올바르게 테스트되고 있습니다. times(3) 검증이 maxRetries = 3 설정과 일치합니다.

ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/client/GooglePlacesClient.kt (1)

182-205: fetchPhoto 예외 처리 시 null 반환 확인

fetchPhoto에서 모든 예외를 catch하고 null을 반환합니다. 이 동작이 의도된 것인지 확인해 주세요. 재시도 로직 내부에서 예외가 발생해도 null이 반환되어 재시도가 무의미해질 수 있습니다.

ssolv-api-place/src/test/kotlin/org/depromeet/team3/place/util/PlaceTestDataFactory.kt (1)

22-35: 테스트 데이터 확장 승인

PlacesTextSearchResponse.Place의 새로운 구조에 맞춰 locationphotos 필드가 테스트 데이터에 추가되었습니다. 이는 Text Search API 응답 모델 변경사항과 일치합니다.

테스트 데이터가 실제 API 응답 구조를 정확히 반영하고 있어 좋습니다.

settings.gradle.kts (1)

10-10: ssolv-batch 모듈이 올바르게 추가되었습니다

모듈 디렉토리, build.gradle.kts, 그리고 src 디렉토리가 모두 존재하며 settings.gradle.kts에 올바르게 등록되었습니다. PR 목표와 일치합니다.

ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/PlaceJpaRepository.kt (2)

9-10: findByGooglePlaceId 메서드 제거 마이그레이션 완료 확인됨

단일 장소 조회 메서드 findByGooglePlaceId가 제거되고 findByGooglePlaceIdIn (배치 조회)로 완전히 마이그레이션되었습니다. 제거된 메서드의 모든 사용처가 새로운 메서드로 전환되어 있으므로 추가 조치가 필요하지 않습니다.


5-5: 배치 정리 메서드 구현 확인 완료

deleteByUpdatedAtBefore 메서드가 올바르게 구현되었습니다:

  • ✓ 배치 스케줄러(PlaceDataCleanupScheduler)에서 정확하게 호출됨 (매일 새벽 3시)
  • @Transactional 어노테이션으로 트랜잭션 안전성 보장
  • ✓ 예외 처리 및 로깅으로 안정적인 운영 준비 완료
  • ✓ 기존 findByGooglePlaceId 메서드 제거 및 findByGooglePlaceIdIn으로 완전히 마이그레이션됨

PR의 "30일 유효기간 적용" 목표가 모두 충족되었습니다.

ssolv-api-place/src/main/kotlin/org/depromeet/team3/place/application/execution/ExecutePlaceSearchService.kt (5)

12-12: GooglePlacesApiProperties 의존성 추가가 올바르게 구성됨

GooglePlacesApiPropertiesapiKey, baseUrl, proxyBaseUrl 속성을 가진 데이터 클래스로 정의되어 있으며, proxyBaseUrl은 기본값 "https://api.ssolv.site"를 갖습니다. 이 속성은 PlaceFormatter.generatePhotoUrl()에서 사진 URL 생성 시 올바르게 사용되고 있습니다. PlaceDetailsProcessor 제거와 함께 이 의존성 추가는 Text Search 단일 호출 방식으로의 전환에 적절합니다.


141-141: Text Search API에서 priceRange를 null로 설정하는 것은 올바른 구현입니다.

PlaceItem에서 priceRange는 이미 nullable(PriceRange?)로 정의되어 있으며, Text Search API가 가격대 정보를 제공하지 않기 때문에 null로 설정하는 것이 적절합니다. 테스트 코드에서도 동일하게 priceRange = null로 처리하고 있어, 클라이언트는 이를 선택적 필드로 인식합니다.


76-88: savePlacesFromTextSearch의 트랜잭션 안전성 및 중복 처리 확인됨

savePlacesFromTextSearch 메서드는 @Transactional 어노테이션으로 보호되어 있으며, 트랜잭션 안전성이 확보되어 있습니다. 중복 저장 처리도 적절히 구현되어 있습니다:

  • 기존 장소를 googlePlaceId로 조회하여 맵에 저장 (O(1) 조회)
  • 30일 이내 업데이트된 데이터는 업데이트 스킵 (비용 최적화)
  • 새로운 또는 오래된 데이터만 저장/업데이트
  • saveAll로 일괄 처리하여 데이터베이스 레벨에서 충돌 처리

122-128: 사진 프록시 URL 생성 로직은 올바르게 구현되어 있습니다.

PlaceFormatter.generatePhotoUrl이 정확한 프록시 URL 형식({proxyBaseUrl}/v1/places/photos/{photoName})을 생성하고 있으며, 프록시 엔드포인트(/photos/{photoName:.+})가 PlacesSearchController에 구현되어 있습니다. PlacePhotoService에서 photoName이 "places/"로 시작하는지 검증하고, GooglePlacesClient는 재시도 로직과 타임아웃을 포함한 안정적인 사진 조회를 수행합니다. 응답에 30일 public 캐시 제어가 설정되어 있어 이미지 노출 방지 및 성능 측면에서도 적절합니다.


47-47: keywordFetchSize 증가에 대한 비용 영향 확인 완료 - 문제 없음

Google Places API Text Search 가격 정책 검증 결과, keywordFetchSize를 5에서 20으로 증가시키는 것은 비용에 영향을 주지 않습니다. API 청구는 결과 개수가 아닌 요청 단위로 이루어지므로, 5개 결과를 요청하든 20개 결과를 요청하든 동일한 요청 요금이 적용됩니다. 코드 주석의 "최대 20개까지 요금 동일"이라는 설명이 정확합니다.

ssolv-batch/src/main/kotlin/org/depromeet/team3/batch/scheduler/PlaceDataCleanupScheduler.kt (1)

10-20: LGTM!

Google 약관 준수를 위한 스케줄러 도입이 적절합니다. 문서화가 명확하고, 30일 데이터 보존 정책이 PR 목표에 부합합니다.

ssolv-batch/build.gradle.kts (2)

1-36: LGTM!

배치 모듈의 Gradle 구성이 적절합니다. Jib을 통한 컨테이너화, JVM 플래그 설정, 의존성 구성이 잘 되어 있습니다.


25-28: 프로젝트 버전 설정 확인 완료

project.version은 루트 build.gradle.kts에서 "0.0.1-SNAPSHOT"으로 올바르게 설정되어 있습니다. ssolv-batch 모듈은 부모 빌드 설정으로부터 버전을 상속받으므로 Docker 이미지 태그는 "latest""0.0.1-SNAPSHOT"으로 올바르게 생성됩니다.

Likely an incorrect or invalid review comment.

ssolv-api-place/src/test/kotlin/org/depromeet/team3/place/application/execution/ExecutePlaceSearchServiceTest.kt (3)

70-125: LGTM!

저장된 결과 반환 시나리오 테스트가 잘 구성되어 있습니다. 테스트 데이터와 assertion이 명확합니다.


127-178: LGTM!

새로운 텍스트 검색 기반 플로우를 잘 테스트하고 있습니다. savePlacesFromTextSearch 경로를 통한 데이터 저장 및 반환 시나리오가 적절히 검증됩니다.


181-221: LGTM!

FakePlaceQuery 구현이 잘 되어 있습니다. 테스트 더블 패턴을 적절히 활용하고, stub 메서드들이 유연하게 설계되어 있어 다양한 테스트 시나리오를 지원합니다.

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: 1

🤖 Fix all issues with AI agents
In @ssolv-batch/build.gradle.kts:
- Around line 1-3: The Jib plugin is declared but lacks configuration; add a jib
{ ... } block in build.gradle.kts configuring at minimum jib.from.image (base
image), jib.to.image (target image name/tag), and jib.container settings
(entrypoint or mainClass, jvmFlags, ports, and appRoot) so the batch module can
build a runnable container; reference the jib block and keys jib.from.image,
jib.to.image, and jib.container.mainClass/jib.container.entrypoint to locate
where to add these settings and ensure the image name uniquely identifies the
batch module (e.g., project name and version).
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eac22a3 and c4c1eae.

📒 Files selected for processing (1)
  • ssolv-batch/build.gradle.kts
⏰ 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: test
🔇 Additional comments (2)
ssolv-batch/build.gradle.kts (2)

5-10: 의존성 구조가 올바르게 설정되어 있습니다.

ssolv-api-place가 ssolv-batch에 의존하고 있으며, 이는 올바른 구조입니다. ssolv-batch는 ssolv-api-place에 의존할 필요가 없습니다. 현재 빌드 파일의 의존성 구조는 PR 목표와 일치합니다.


12-20: 이 모듈의 jar/bootJar 설정은 현재 아키텍처에 맞게 올바르게 구성되어 있습니다.

ssolv-batch는 라이브러리 모듈이며, ssolv-api-place에 의존성으로 포함되어 실행됩니다. 따라서 jar 활성화, bootJar 비활성화는 이 모듈의 성격에 적합합니다:

  • ssolv-batch는 @SpringBootApplication 없이 @scheduled @component만 포함
  • ssolv-api-place가 PlaceApiApplication을 통해 실행 가능한 jar 생성
  • 배치 작업(MeetingPlaceSearchCleanupScheduler 등)은 ssolv-api-place의 Spring 컨텍스트 내에서 실행

"물리적으로 분리하여 배포"는 ssolv-api-core와 ssolv-api-place를 별도 컨테이너로 배포하는 것으로 달성되며, 현재 설정이 그 목표를 충족합니다.

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: 2

🤖 Fix all issues with AI agents
In
@ssolv-api-place/src/main/kotlin/org/depromeet/team3/place/application/execution/ExecutePlaceSearchService.kt:
- Line 120: The address normalization in ExecutePlaceSearchService (assignment
to address) currently uses replace("대한민국 ", "") which only removes "대한민국"
followed by a space; update it to remove a leading "대한민국" with any following
whitespace (or the exact word alone) using a regex anchored to start, e.g.,
replace(Regex("^대한민국\\s*"), "") or trim after conditional removal, so an address
equal to "대한민국" becomes an empty string; locate the address = entity.address?
line and replace the literal-space replace with the regex-based removal and
maintain the existing null-coalescing behavior.
🧹 Nitpick comments (4)
ssolv-infrastructure/src/test/kotlin/org/depromeet/team3/place/client/GooglePlacesClientRetryTest.kt (1)

35-58: 재시도 로직이 올바르게 테스트되고 있습니다.

500 에러 발생 후 재시도하여 성공하는 시나리오를 정확하게 검증합니다. Mock 체이닝과 검증 횟수(2회)가 재시도 로직과 일치합니다.

💡 선택적 개선: 테스트 데이터 강화

현재는 emptyList()를 사용하고 있지만, 실제 장소 데이터를 포함한 응답을 사용하면 통합 테스트에 더 가까워질 수 있습니다:

-val mockResponse = PlacesTextSearchResponse(emptyList())
+val mockResponse = PlacesTextSearchResponse(
+    places = listOf(
+        PlacesTextSearchResponse.Place(
+            id = "test-place-id",
+            displayName = PlacesTextSearchResponse.DisplayName(text = "테스트 맛집", languageCode = "ko")
+        )
+    )
+)

다만 현재의 재시도 로직 검증 목적에는 빈 리스트로도 충분합니다.

ssolv-infrastructure/src/test/kotlin/org/depromeet/team3/place/client/GooglePlacesClientTest.kt (1)

33-65: 테스트 커버리지 보완 권장

현재 성공 케이스만 테스트하고 있습니다. 다음 시나리오 추가를 고려해보세요:

  • API 호출 실패 (예외 처리)
  • 빈 결과 반환
  • 필수 필드 누락 처리

테스트 신뢰성을 높이고 엣지 케이스에 대한 방어 로직을 검증할 수 있습니다.

ssolv-api-place/src/main/kotlin/org/depromeet/team3/place/application/execution/ExecutePlaceSearchService.kt (1)

124-129: 사진 이름 공백 처리 추가 권장

split(",")로 분리한 사진 이름에 공백이 포함될 수 있습니다. PlaceFormatter.generatePhotoUrl이 내부적으로 처리하지 않는다면 URL 생성에 실패할 수 있습니다.

안전하게 처리하려면 다음과 같이 trim을 추가하는 것을 고려해보세요:

♻️ 제안하는 수정
-photos = entity.photos?.split(",")?.map { photoName ->
+photos = entity.photos?.split(",")?.map { photoName ->
+    val trimmedName = photoName.trim()
     // PlaceFormatter를 사용하여 프록시 URL 생성
     org.depromeet.team3.place.util.PlaceFormatter.generatePhotoUrl(
-        photoName, 
+        trimmedName, 
         googlePlacesApiProperties.proxyBaseUrl
     )
 },
ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/PlaceQuery.kt (1)

44-51: 30일 체크 기준 시점을 반복문 밖으로 이동 권장

현재 LocalDateTime.now().minusDays(30)이 각 place마다 map 내부에서 호출됩니다. 배치 처리 중 시간이 변경될 수 있어 일관성이 떨어지고, 불필요한 반복 계산이 발생합니다.

♻️ 제안하는 수정
 suspend fun savePlacesFromTextSearch(
     places: List<PlacesTextSearchResponse.Place>
 ): List<PlaceEntity> {
     if (places.isEmpty()) return emptyList()
 
     val googlePlaceIds = places.map { it.id }
     val existingPlaces = placeJpaRepository.findByGooglePlaceIdIn(googlePlaceIds)
         .associateBy { it.googlePlaceId }
+    
+    val cacheExpirationTime = LocalDateTime.now().minusDays(30)
 
     val entities = places.map { place ->
         val existing = existingPlaces[place.id]
         val lastUpdated = existing?.updatedAt ?: LocalDateTime.MIN
 
         // 30일 이내 데이터가 있으면 업데이트 스킵 (비용 최적화 및 구글 약관 준수)
-        if (existing != null && lastUpdated.isAfter(LocalDateTime.now().minusDays(30))) {
+        if (existing != null && lastUpdated.isAfter(cacheExpirationTime)) {
             return@map existing
         }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4c1eae and f998d90.

📒 Files selected for processing (5)
  • ssolv-api-place/src/main/kotlin/org/depromeet/team3/place/application/execution/ExecutePlaceSearchService.kt
  • ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/PlaceQuery.kt
  • ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/model/PlacesTextSearchResponse.kt
  • ssolv-infrastructure/src/test/kotlin/org/depromeet/team3/place/client/GooglePlacesClientRetryTest.kt
  • ssolv-infrastructure/src/test/kotlin/org/depromeet/team3/place/client/GooglePlacesClientTest.kt
⏰ 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: test
🔇 Additional comments (5)
ssolv-infrastructure/src/test/kotlin/org/depromeet/team3/place/client/GooglePlacesClientRetryTest.kt (3)

1-33: 테스트 설정이 올바르게 구성되어 있습니다.

Text Search API로의 전환에 맞춰 import와 mock 설정이 적절하게 변경되었습니다. RestClient mock과 GooglePlacesApiProperties 설정이 테스트 목적에 부합합니다.


60-82: 최대 재시도 횟수 초과 시나리오가 정확하게 검증됩니다.

3회 호출(초기 시도 + 2회 재시도) 후 PlaceSearchException이 발생하는 것을 올바르게 검증하고 있습니다. 재시도 정책이 최대 2회임을 명확히 보여줍니다.


84-92: 헬퍼 메서드가 적절하게 구현되었습니다.

HttpClientErrorException.create() 팩토리 메서드를 사용하여 다양한 HTTP 상태 코드에 대한 예외를 생성할 수 있도록 범용적으로 설계되었습니다. 이전의 PlaceDetailsResponse 전용 빌더 로직을 제거하고 더 간결한 접근 방식을 채택한 것은 좋은 개선입니다.

ssolv-infrastructure/src/main/kotlin/org/depromeet/team3/place/PlaceQuery.kt (1)

34-37: @transactional과 suspend 함수 조합은 올바르게 작동합니다

Spring Boot 3.4.9는 Spring Framework 6.1+의 코루틴 트랜잭션 지원을 포함하며, 프로젝트에 kotlinx-coroutines-reactor 의존성이 이미 구성되어 있어 트랜잭션 컨텍스트가 올바르게 전파됩니다. 이 패턴은 PlaceLikeQuery, MeetingPlaceQuery 등 코드베이스 전반에서 일관되게 사용되고 있으므로 추가 조치가 필요하지 않습니다.

ssolv-api-place/src/main/kotlin/org/depromeet/team3/place/application/execution/ExecutePlaceSearchService.kt (1)

518-537: 제약 조건 존재 확인 - 예외 처리 필요

MeetingPlaceEntity(meeting_id, place_id) 조합에 대한 unique constraint uk_meeting_place가 이미 정의되어 있어 데이터베이스 수준에서 중복 저장을 방지합니다.

다만 현재 로직은 조회 후 저장하는 비트랜잭션 방식으로 인해 동시 요청 시 두 스레드가 같은 조합을 삽입하려 할 때 DataIntegrityViolationException이 발생할 수 있습니다. 라인 532의 saveAll 호출이 예외 처리 없이 진행되므로, 이 경우 에러가 호출자까지 전파됩니다.

@Transactional 추가 또는 saveAll 주변에 예외 처리를 추가하여 중복 삽입 시도에 대한 안전한 처리를 고려하세요.

rating = review.rating,
text = review.text
name = entity.name ?: "",
address = entity.address?.replace("대한민국 ", "") ?: "",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

주소 문자열 처리 엣지 케이스 확인 필요

replace("대한민국 ", "") 로직은 trailing 공백이 있는 경우만 처리합니다. 만약 주소가 정확히 "대한민국" (공백 없음)인 경우 빈 문자열로 처리되지 않고 그대로 유지됩니다.

Google Places API 응답에서 이런 케이스가 발생하는지 확인하고, 필요하다면 replace(Regex("^대한민국\\s*"), "") 같은 패턴 사용을 고려해보세요.

🤖 Prompt for AI Agents
In
@ssolv-api-place/src/main/kotlin/org/depromeet/team3/place/application/execution/ExecutePlaceSearchService.kt
at line 120, The address normalization in ExecutePlaceSearchService (assignment
to address) currently uses replace("대한민국 ", "") which only removes "대한민국"
followed by a space; update it to remove a leading "대한민국" with any following
whitespace (or the exact word alone) using a regex anchored to start, e.g.,
replace(Regex("^대한민국\\s*"), "") or trim after conditional removal, so an address
equal to "대한민국" becomes an empty string; locate the address = entity.address?
line and replace the literal-space replace with the regex-based removal and
maintain the existing null-coalescing behavior.

@parkmineum parkmineum merged commit 966a99a into dev Jan 11, 2026
3 checks passed
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.

[REFACTOR] 맛집 추천 알고리즘 개선

1 participant