refactor: 동일 가게 내 중복 메뉴 검색 시 최저가 메뉴만 반환하도록 수정#26
Conversation
WalkthroughStoreController에 OpenAPI 주석(@tag, @operation, @ApiResponses 등)과 getStoreDetails의 404 응답 스키마/예제가 추가되었고, StoreService의 검색 로직은 메뉴 단위에서 매장별 최저가 메뉴 단위로 재구성되어 매장당 최대 한 번만 결과에 노출되도록 변경되었습니다. 시그니처 변경은 없습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant C as Client
participant Ctr as StoreController
participant Svc as StoreService
C->>Ctr: GET /stores/search?menu=...&bbox=...
Ctr->>Svc: searchStores(menu, bbox, ...)
activate Svc
Note over Svc: 1) 메뉴 조회 → 매장별 그룹화\n2) 각 매장 최저가 메뉴 선택\n3) 매장 기준 bbox 필터링\n4) (Store, 최저가 메뉴)로 DTO 매핑 및 거리/정렬
Svc-->>Ctr: List<StoreDto> (매장당 최대 1개)
deactivate Svc
Ctr-->>C: 200 OK (JSON)
sequenceDiagram
autonumber
participant C as Client
participant Ctr as StoreController
C->>Ctr: GET /stores/{id}
alt 매장 존재
Ctr-->>C: 200 OK (StoreDetailDto)
else 매장 없음
Note over Ctr: 문서화에 404 ErrorResponse 스키마/예제 추가\n(런타임 로직 변화 없음)
Ctr-->>C: 404 Not Found (ErrorResponse)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
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. 📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/kotlin/com/eodigo/domain/restaurant/controller/StoreController.kt (1)
69-72: latitude/longitude는 @RequestParam 명시 권장단순 타입의 비어있는 어노테이션은 바인딩 소스가 모호할 수 있습니다. 쿼리 파라미터로 명시하면 의도가 분명해지고 스웨거 문서에도 잘 드러납니다.
- fun getStoreDetails( - @Valid @PathVariable storeId: Long, - latitude: Double, - longitude: Double, - ): ResponseEntity<StoreDetailDto> { + fun getStoreDetails( + @Valid @PathVariable storeId: Long, + @RequestParam latitude: Double, + @RequestParam longitude: Double, + ): ResponseEntity<StoreDetailDto> {원하면
@Parameter로 예제/설명까지 추가해 드릴 수 있습니다.
🧹 Nitpick comments (8)
src/main/kotlin/com/eodigo/domain/restaurant/service/StoreService.kt (6)
32-39: N+1 가능성: Menu→Store 접근이 다수 발생이 구간에서
it.store및 이후 필터/매핑 과정에서store의 여러 필드를 접근합니다.Menu.store가 LAZY라면 메뉴 수만큼 추가 SELECT가 발생할 수 있습니다. 조회 성능을 위해 fetch join 쿼리로 메뉴와 스토어를 함께 가져오도록 Repository 메서드 추가를 고려하세요. 또한 가능하면 DB 레벨에서 경계/카테고리/이름 조건을 모두 반영해 전송량을 줄이는 게 좋습니다.예시(JPQL):
@Query(""" select m from Menu m join fetch m.store s where lower(m.name) like concat('%', lower(:name), '%') and s.category = :category and s.latitude between :swLat and :neLat and s.longitude between :swLng and :neLng """) fun findWithStoreByNameCategoryAndBounds( @Param("name") name: String, @Param("category") category: Category, @Param("swLat") swLat: Double, @Param("neLat") neLat: Double, @Param("swLng") swLng: Double, @Param("neLng") neLng: Double ): List<Menu>이후 현재 PR의 “가게별 최저가 선택”만 메모리에서 수행하면 됩니다.
32-39: groupBy 키를 엔티티 대신 식별자(id)로 변경 검토
groupBy { it.store }는 엔티티 인스턴스 동일성에 의존합니다. 동일 Store가 서로 다른 프록시 인스턴스로 로드되는 경우(특정 컨텍스트/캐시 상황) 같은 가게가 다른 키로 분리될 수 있습니다. 보다 견고하게groupBy { requireNotNull(it.store.id) }로 묶고,menusInStore.first().store로 대표 Store를 사용하는 방식을 권장합니다.
42-47: 지도 경계값 정규화로 가드 추가 제안경계 좌표(sw ↔ ne)가 클라이언트에서 뒤바뀌어 오는 케이스를 가드하면 예외 상황에 강해집니다.
min/max로 한 번 정규화한 뒤 비교하세요.예시:
val swLat = min(request.southWestLat, request.northEastLat) val neLat = max(request.southWestLat, request.northEastLat) val swLng = min(request.southWestLng, request.northEastLng) val neLng = max(request.southWestLng, request.northEastLng) val locationFilteredMenus = cheapestMenuByStore.filter { (store, _) -> store.latitude in swLat..neLat && store.longitude in swLng..neLng }
50-70: 거리 단위를 반올림하여 표현 정밀도 개선
distance.toInt()는 내림(절삭)이라 근소한 차이가 왜곡될 수 있습니다.roundToInt()로 반올림하면 UX가 자연스럽습니다.적용 예시:
+import kotlin.math.roundToInt ... - distance = distance.toInt(), + distance = distance.roundToInt(),
73-76: 정렬 시 2차 키 추가로 결정성 확보가격/거리 동일값이 많은 경우 정렬 결과의 결정성이 떨어질 수 있습니다. 2차 키를 추가해 안정적 결과를 보장하세요.
- return when (request.sort) { - SortType.PRICE -> storeDtoList.sortedBy { it.price } - SortType.DISTANCE -> storeDtoList.sortedBy { it.distance } - } + return when (request.sort) { + SortType.PRICE -> storeDtoList.sortedWith(compareBy<StoreDto> { it.price }.thenBy { it.distance }) + SortType.DISTANCE -> storeDtoList.sortedWith(compareBy<StoreDto> { it.distance }.thenBy { it.price }) + }
27-31: 카테고리·이름 필터 옵션화 여부 확인
request.category나request.menuName이 선택값(옵셔널)이라면 현재 구현은 값이 비어도 필터에 걸려 결과가 0이 될 수 있습니다. 요구사항이 “항상 필수”가 아니라면, 각 필터를 조건부로 적용하는 분기 처리 필요 여부를 확인해 주세요.원하시면
SearchRequest의 제약(@NotBlank/@NotNull)과 함께 조건부 필터링 코드 스니펫을 제공하겠습니다.Also applies to: 40-47, 50-70
src/main/kotlin/com/eodigo/domain/restaurant/controller/StoreController.kt (2)
26-33: 검색 API의 400/422 케이스 문서화도 고려검증 실패(예: 좌표 범위, 필수값 누락)에 대한 400/422 응답 스펙을 추가하면 클라이언트 구현이 더 수월합니다. 예시/스키마는
ErrorResponse재사용이 가능합니다.
41-66: ExampleObject.value에 Kotlin raw string 사용 권장
ExampleObject.value가 현재 이스케이프된 단일 문자열로 작성되어 있어 가독성이 떨어집니다. Kotlin의 raw string("""…""")을 사용하면 JSON 본문을 직관적으로 확인할 수 있어 유지보수에 유리합니다.
추가로,StoreNotFoundException이 내부적으로CustomException(ErrorCode.STORE_NOT_FOUND)를 사용하며,ErrorCode.STORE_NOT_FOUND에 정의된 코드"S001"과 메시지가("해당 매장을 찾을 수 없습니다.") 실제 매핑과 일치함을 확인했습니다.
- 적용 예시:
- ExampleObject( - name = "Store Not Found", - description = "존재하지 않는 매장 ID", - value = "{\"status\": 404, \"code\": \"S001\", \"message\": \"해당 매장을 찾을 수 없습니다.\"}" - ) + ExampleObject( + name = "Store Not Found", + description = "존재하지 않는 매장 ID", + value = """ + { + "status": 404, + "code": "S001", + "message": "해당 매장을 찾을 수 없습니다." + } + """ + )
src/main/kotlin/com/eodigo/domain/restaurant/exception/StoreNotFoundException.kt
class StoreNotFoundException : CustomException(ErrorCode.STORE_NOT_FOUND)확인됨.src/main/kotlin/com/eodigo/common/exception/ErrorCode.kt
STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "S001", "해당 매장을 찾을 수 없습니다.")정의 확인됨.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/main/kotlin/com/eodigo/domain/restaurant/controller/StoreController.kt(2 hunks)src/main/kotlin/com/eodigo/domain/restaurant/service/StoreService.kt(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.kt
⚙️ CodeRabbit configuration file
src/**/*.kt: ## 1. Kotlin 코딩 컨벤션
- Kotlin 공식 코딩 컨벤션을 준수해야 합니다.
- 클래스는 PascalCase, 함수/변수는 camelCase, 상수는 UPPER_SNAKE_CASE로 작성하세요.
- 변경 불가능한 데이터는 val을, 변경이 필요한 경우에만 var를 사용하세요.
- Null Pointer Exception을 유발하는 !! 연산자 사용을 금지합니다.
- 의미를 알 수 없는 숫자나 문자열(매직 넘버) 대신 명명된 상수를 사용하세요.
2. Spring Boot
- Controller-Service-Repository 계층형 아키텍처의 책임을 분리하세요.
- Controller의 요청/응답에는 반드시 DTO를 사용하고, 엔티티를 직접 노출하지 마세요.
- 민감한 설정 정보(DB 계정, API 키 등)는 코드에 하드코딩하지 말고 외부에서 주입하세요.
- @Autowired 필드 주입 대신 생성자 주입을 사용하세요.
3. JPA 및 데이터베이스
- @entity 클래스에 Setter 사용을 지양하고, 비즈니스 메서드를 통해 상태를 변경하세요.
- 데이터를 변경하지 않는 조회용 Service 메서드에는 @transactional(readOnly = true)를 적용하세요.
- N+1 문제가 발생하는 쿼리가 있는지 확인하고, Fetch Join으로 최적화하세요.
- 엔티티의 양방향 연관관계에서 toString()을 사용할 경우 순환 참조가 발생하지 않도록 주의하세요.
4. 예외 처리
- @RestControllerAdvice를 사용하여 예외를 전역에서 일관되게 처리하세요.
- RuntimeException 대신, 의미가 명확한 커스텀 예외(예: MemberNotFoundException)를 정의하여 사용하세요.
Files:
src/main/kotlin/com/eodigo/domain/restaurant/service/StoreService.ktsrc/main/kotlin/com/eodigo/domain/restaurant/controller/StoreController.kt
🔇 Additional comments (3)
src/main/kotlin/com/eodigo/domain/restaurant/controller/StoreController.kt (3)
3-3: 에러 응답 스키마 import 적절
ErrorResponse도입으로 404 문서화가 명확해집니다. 사용처가 문서 한정이므로 사이드 이펙트도 없습니다.
8-15: OpenAPI 어노테이션 도입 👍필요한 스키마/미디어/태그 import 구성이 깔끔합니다.
20-20: API 그룹 태깅 적절카탈로그성 컨트롤러에 태그를 부여해 탐색성이 좋아졌습니다.
📝 작업 내용
메뉴 검색 API(GET /api/v1/stores/search)의 응답 결과 개선
동일한 가게 내에서는 검색 키워드와 일치하는 메뉴 중 가장 저렴한 메뉴 하나만을 대표로 반환하도록 로직 변경
⚡ 주요 변경사항
StoreService의 searchStores 메서드 로직 수정
📌 리뷰 포인트
📋 체크리스트
feat: 기능1 추가)Summary by CodeRabbit
버그 수정
문서