Conversation
Walkthrough레시피 저장소에 카테고리 필터와 LIMIT를 사용하는 랜덤 레시피 조회 네이티브 쿼리 메서드가 추가되었습니다. OpenAiClient와 관련 구성/클래스(OpenAiClient, OpenAiClientConfig 등)가 제거되고, GeminiClient 및 GeminiClientConfig가 새로 도입되어 WebClient 기반의 Gemini API 호출로 교체되었습니다. RecipeRecommendServiceImpl 및 RecipeServiceImpl에서 OpenAI 의존성이 GeminiClient로 대체되고 프롬프트, 응답 파싱, 로깅(ENABLE_VERBOSE_LOG) 및 샘플 크기 등이 조정되었습니다. ErrorCode의 AI 관련 메시지가 공급자 중립적으로 변경되었습니다. Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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 (2)
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java (2)
748-759: OpenAI 전용 JSON 파싱 잔존: 점수 파싱 경로 간소화 필요GeminiClient는 평문 텍스트를 반환하도록 구현되어 있어
choices[0].message.content파싱은 무의미하며 기본값(5.0)으로 치우치는 리스크가 있습니다. 코드블록/마크다운 제거 후 숫자만 추출하는 단일 경로로 단순화하세요.아래처럼 간소화하면 공급자 종속성이 사라지고 실패율이 낮아집니다.
- // JSON 응답인 경우 처리 - if (scoreStr.startsWith("{")) { - try { - JsonNode jsonNode = objectMapper.readTree(scoreStr); - String content = jsonNode.path("choices").path(0).path("message").path("content").asText(); - if (!content.isEmpty()) { - scoreStr = content.trim(); - } - } catch (Exception e) { - log.warn("JSON 파싱 실패: {}", e.getMessage()); - return 5.0; - } - } + // 코드블록/마크다운 제거(필요 시) 후 텍스트에서 숫자만 추출 + scoreStr = extractJsonFromAIResponse(scoreStr);추가로, 필요하다면 Gemini JSON(있을 경우)에 한해
candidates[0].content.parts[0].text를 읽는 보조 로직을 넣되 실패해도 숫자 정규식 추출 경로로 항상 수렴하도록 유지하는 것을 권장합니다.
2098-2100: 예외 매핑 일관성(재료 기반 생성)위와 동일한 맥락으로 이 구간도
BusinessException은 그대로 던지고, 일반 예외만 구체 코드로 래핑하는 패턴으로 통일하세요.- } catch (Exception e) { - log.error("AI 재료 기반 레시피 생성 실패: {}", e.getMessage(), e); - throw new BusinessException(ErrorCode.OPENAI_INVALID_RESPONSE); + } catch (BusinessException e) { + log.error("AI 재료 기반 레시피 생성 실패(Business): {}", e.getMessage()); + throw e; + } catch (Exception e) { + log.error("AI 재료 기반 레시피 생성 중 예상치 못한 오류 발생: {}", e.getMessage(), e); + throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR); }
🧹 Nitpick comments (20)
src/main/java/com/mumuk/global/apiPayload/code/ErrorCode.java (1)
68-77: 에러 코드명과 메시지의 불일치 문제에러 코드 상수명은 여전히
OPENAI_*로 되어 있지만, 메시지는 제공자 중립적인 "AI"로 변경되었습니다. 이는 코드의 일관성과 유지보수성 측면에서 혼란을 야기할 수 있습니다.향후 리팩토링 시 에러 코드 상수명도
AI_*로 변경하는 것을 고려해보세요.src/main/java/com/mumuk/domain/recipe/repository/RecipeRepository.java (1)
85-96: RANDOM() 함수 사용 시 성능 고려사항
ORDER BY RANDOM()은 전체 테이블을 스캔한 후 정렬하므로 대용량 데이터에서 성능 이슈가 발생할 수 있습니다. 현재 코드에는 이미 PK 범위를 이용한 대안 메서드(findRandomRecipesByPkRange)가 있으므로, 데이터 양이 증가하면 이를 활용하는 것이 좋습니다.대용량 테이블에서는 다음과 같은 최적화 방안을 고려해보세요:
- 테이블 샘플링 기법 사용 (
TABLESAMPLE)- 미리 계산된 랜덤 값 컬럼 활용
- 인덱스를 활용한 랜덤 샘플링
src/main/java/com/mumuk/global/config/GeminiClientConfig.java (1)
12-16: 민감한 설정 값 보안 강화 필요API 키가 application properties에서 직접 로드되고 있습니다. 프로덕션 환경에서는 보안을 위해 다음과 같은 방법을 고려하세요:
- AWS Secrets Manager, Azure Key Vault 등의 비밀 관리 서비스 사용
- 환경 변수를 통한 주입
- 암호화된 설정 파일 사용
src/main/java/com/mumuk/global/client/GeminiClient.java (3)
27-36: 비동기 처리에서 블로킹 호출 방지 필요
chat메서드가Mono<String>을 반환하는데, 이를 호출하는 서비스 레이어에서.block()을 사용하고 있습니다. 이는 리액티브 프로그래밍의 이점을 무효화합니다.서비스 레이어에서도 리액티브 방식으로 처리하거나, 동기 방식이 필요하다면
RestTemplate사용을 고려하세요.
57-64: 안전 설정이 하드코딩되어 있음Gemini API의 안전 설정이 하드코딩되어 있으며, 하나의 카테고리(
HARM_CATEGORY_HARASSMENT)만 설정되어 있습니다.다음과 같이 개선하는 것을 제안합니다:
- // 안전 설정 추가 - Map<String, Object> safetySettings = new HashMap<>(); - safetySettings.put("category", "HARM_CATEGORY_HARASSMENT"); - safetySettings.put("threshold", "BLOCK_NONE"); - - List<Map<String, Object>> safetySettingsList = new ArrayList<>(); - safetySettingsList.add(safetySettings); + // 안전 설정 추가 - 모든 카테고리에 대해 설정 + List<Map<String, Object>> safetySettingsList = new ArrayList<>(); + String[] categories = { + "HARM_CATEGORY_HARASSMENT", + "HARM_CATEGORY_HATE_SPEECH", + "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "HARM_CATEGORY_DANGEROUS_CONTENT" + }; + + for (String category : categories) { + Map<String, Object> setting = new HashMap<>(); + setting.put("category", category); + setting.put("threshold", "BLOCK_NONE"); + safetySettingsList.add(setting); + }
109-136: JSON 추출 로직의 엣지 케이스 처리 부족JSON 블록 추출 로직이 중첩된 백틱이나 잘못된 포맷을 처리하지 못할 수 있습니다.
정규식을 사용한 더 견고한 추출 로직을 제안합니다:
private String extractJsonFromGeminiResponse(String response) { if (response == null || response.trim().isEmpty()) { throw new BusinessException(ErrorCode.OPENAI_INVALID_RESPONSE); } String trimmedResponse = response.trim(); + // 정규식을 사용한 코드블록 추출 + Pattern pattern = Pattern.compile("```(?:json)?\\s*([\\s\\S]*?)```"); + Matcher matcher = pattern.matcher(trimmedResponse); + + if (matcher.find()) { + return matcher.group(1).trim(); + } - // ```json으로 시작하는 경우 - if (trimmedResponse.startsWith("```json")) { - int startIndex = trimmedResponse.indexOf("```json") + 7; - int endIndex = trimmedResponse.lastIndexOf("```"); - if (endIndex > startIndex) { - return trimmedResponse.substring(startIndex, endIndex).trim(); - } - } - - // ```으로 시작하는 경우 (json 태그 없음) - if (trimmedResponse.startsWith("```")) { - int startIndex = trimmedResponse.indexOf("```") + 3; - int endIndex = trimmedResponse.lastIndexOf("```"); - if (endIndex > startIndex) { - return trimmedResponse.substring(startIndex, endIndex).trim(); - } - } // 코드블록이 없는 경우 그대로 반환 return trimmedResponse; }src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java (1)
483-484: 블로킹 호출과 타임아웃 설정 개선 필요현재
geminiClient.chat(prompt).block(Duration.ofSeconds(15))처럼 블로킹 호출에 하드코딩된 타임아웃을 사용하고 있습니다. 또한 다른 서비스(RecipeRecommendServiceImpl)에서는 30초로 설정되어 있어 일관성이 없습니다. 아래를 검토해 주세요:
- 타임아웃 값을 코드에 직접 하드코딩하지 말고,
• application.yml 또는 환경 변수로 외부에서 설정
•@Value나ConfigurationProperties로 주입- 서비스별 요구사항에 맞게 서로 다른 타임아웃이 필요하다면,
• 명확한 프로퍼티 이름(e.g.gemini.timeout.chat)으로 분리- 설정값의 기본값 및 최대 허용값을 문서화하여 유지보수성 강화
주요 위치:
- src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java:484 (15초)
- src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java:989 (30초)
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java (13)
84-85: ENABLE_VERBOSE_LOG를 환경설정으로 치환 권장운영/스테이징 전환 시 빌드 없이 로그 레벨을 조절할 수 있도록 Spring 프로퍼티로 제어하는 것을 권장합니다.
예시:
// 필드로 주입 @Value("${ai.recommend.verbose:false}") private boolean enableVerboseLog;그리고 기존
ENABLE_VERBOSE_LOG참조부를enableVerboseLog로 대체하면 운영 편의성이 좋아집니다.
986-994: Gemini 호출 타임아웃 하드코딩 해제 및 예외 구분 처리 제안현재 30초 타임아웃이 고정이며 모든 예외가 동일한 ErrorCode로 래핑됩니다. 타임아웃과 기타 오류를 구분하면 관측/대응이 쉬워집니다. 또한 타임아웃을 프로퍼티로 관리하는 편이 낫습니다.
적용 예시(해당 범위 내 변경):
- return geminiClient.chat(prompt).block(Duration.ofSeconds(30)); + return geminiClient.chat(prompt).block(GEMINI_TIMEOUT); } catch (Exception e) { log.error("Gemini API 호출 실패: {}", e.getMessage()); throw new BusinessException(ErrorCode.OPENAI_API_ERROR); }상단 상수 정의(선택):
// 클래스 상단 상수 섹션에 추가 private static final Duration GEMINI_TIMEOUT = Duration.ofSeconds(30);추후
@Value("${ai.gemini.timeout-seconds:30}")로 치환하면 더 유연해집니다.
1048-1053: 프롬프트 오탈자 및 문구 품질 개선
- "… 제외 (… 아님)n"의 말미 "n"은 오타로 보입니다.
- "OTHER은 유일해야 함함"에서 "함"이 중복되었습니다.
다음과 같이 정정하세요.
- "- 선택사항인 식재료는 제외 (연어 포케에 올리브도 들어갈 수 있지만 필수는 아님)n" + + "- 선택사항인 식재료는 제외 (연어 포케에 올리브도 들어갈 수 있지만 필수는 아님)\n" + "- 다양한 재료를 이용할 수 있는 경우 큰 틀로 작성 (앞다리살, 삼겹살, 목살 -> 돼지고기, 상추, 로메인, 샐러리 -> 샐러드 채소)"+ "※ 카테고리 선택:\n" + "- 각 요리의 특성에 맞는 카테고리를 적절하게 선택, 여러개 선택 가능, 애매하다 싶으면 추가\n" + - "- 마땅히 없다면 OTHER 선택 (OTHER은 유일해야 함함)\n\n" + + "- 마땅히 없다면 OTHER 선택 (OTHER는 유일해야 함)\n\n" +추가 제안: 본문 하단에서 "summary를 … 작성" 안내가 있으나 JSON 스키마에 summary 필드가 없습니다. 혼선을 줄이려면 안내를 삭제하거나 JSON 예시에
"summary": "요약"필드를 추가하는 쪽으로 일관성을 맞춰 주세요.
1101-1103: 사용자 재료/프롬프트 길이 로그는 verbose로 가드실서비스에서 재료 목록은 사용자 데이터로 간주될 수 있습니다. verbose 옵션으로 가드하거나 debug 레벨로 낮추는 것을 권장합니다.
- log.info("프롬프트 길이: {} characters", prompt.length()); - log.info("전달된 재료: {} (중복제거 후: {}개)", String.join(", ", uniqueIngredients), uniqueIngredients.size()); + if (ENABLE_VERBOSE_LOG) { + log.info("프롬프트 길이: {} characters", prompt.length()); + log.info("전달된 재료: {} (중복제거 후: {}개)", String.join(", ", uniqueIngredients), uniqueIngredients.size()); + }
1116-1123: 랜덤 프롬프트 생성부의 로그 노이즈 절감주제(topic) 관련 로그와 프롬프트 길이 로그는 운영 시 노이즈가 될 수 있어 verbose 가드를 권장합니다.
- if (topic == null) { - log.info("주제가 null이므로 완전 랜덤 레시피 생성"); - } else if (topic.trim().isEmpty()) { - log.info("주제가 빈 문자열이므로 완전 랜덤 레시피 생성"); - } else { - log.info("주제 '{}' 기반 레시피 생성", topic.trim()); + if (ENABLE_VERBOSE_LOG) { + if (topic == null) { + log.info("주제가 null이므로 완전 랜덤 레시피 생성"); + } else if (topic.trim().isEmpty()) { + log.info("주제가 빈 문자열이므로 완전 랜덤 레시피 생성"); + } else { + log.info("주제 '{}' 기반 레시피 생성", topic.trim()); + } } @@ - log.info("생성된 프롬프트 길이: {} 문자", finalPrompt.length()); + if (ENABLE_VERBOSE_LOG) { + log.info("생성된 프롬프트 길이: {} 문자", finalPrompt.length()); + }Also applies to: 1133-1136
487-491: 배치 평가 로그 레벨/가드 일관화배치 응답 전문/점수 로그는 현재 일부만 verbose 가드가 적용되어 있습니다.
파싱된 점수등 고빈도 로그도 동일하게 가드하거나 debug로 낮추면 운영 노이즈/코스트를 줄일 수 있습니다.가드 적용/레벨 조정으로 운영 로그를 슬림하게 가져가길 권장합니다.
Also applies to: 496-500, 507-507
2146-2149: 프롬프트 헤더 오탈자/용어 통일"===최우선 고려사항 알러지 정보보 ==="는 오탈자이며, 본문에서는 '알레르기' 표기를 주로 사용합니다. 용어를 통일하고 가독성을 높여 주세요.
- promptBuilder.append("===최우선 고려사항 알러지 정보보 ===\n"); + promptBuilder.append("=== 최우선 고려사항: 알레르기 정보 ===\n");
2179-2191: 용어 통일(알레르기) 및 문장 다듬기프롬프트 내 ‘알러지/알레르기’ 혼용을 ‘알레르기’로 통일하는 것이 좋습니다. 또한 띄어쓰기 및 어휘를 약간 다듬으면 모델 응답 품질에도 도움이 됩니다.
- return "=== 적합도 평가 기준 (우선순위 순) ===\n" + - "1. 알러지 성분 포함 여부 (최우선, 포함시 0점)\n" + + return "=== 적합도 평가 기준 (우선순위 순) ===\n" + + "1. 알레르기 성분 포함 여부 (최우선, 포함 시 0점)\n" + "2. 건강 목표 달성 도움 정도\n" + "3. 현재 건강 상태 적합성\n" + "4. 보유 재료 활용도\n\n" + "점수 기준:\n" + - "- 9-10점: 모든 조건을 완벽하게 만족 (건강 목표 최적, 건강 상태 적합, 재료 완벽)\n" + - "- 7-8점: 대부분의 조건을 만족 (건강 목표 적합, 건강 상태 적합, 재료 충분)\n" + - "- 5-6점: 주요 조건을 만족 (건강 목표 보통, 건강 상태 보통, 재료 가능)\n" + - "- 3-4점: 일부 조건만 만족 (건강 목표 부적합, 건강 상태 부적합, 재료 부족)\n" + + "- 9-10점: 모든 조건을 완벽하게 만족 (건강 목표 최적, 건강 상태 적합, 재료 충분)\n" + + "- 7-8점: 대부분의 조건을 만족 (건강 목표/건강 상태 적합, 재료 충분)\n" + + "- 5-6점: 주요 조건을 일부 만족 (건강 목표/건강 상태 보통, 재료 가능)\n" + + "- 3-4점: 일부 조건만 만족 (건강 목표/건강 상태 부적합, 재료 부족)\n" + "- 1-2점: 대부분의 조건을 만족하지 못함\n" + - "- 0점: 알러지 성분 포함 (절대 추천 불가)\n\n" + - "적합도 점수만 숫자로 응답해주세요 (예: 8.5)"; + "- 0점: 알레르기 성분 포함 (절대 추천 불가)\n\n" + + "적합도 점수만 숫자로 응답해 주세요 (예: 8.5)";
1939-1944: 배치 파싱 로그 레벨/가드 일관화(HealthGoal)HealthGoal 배치 점수 파싱부도 응답/점수 로그가 info로 노출됩니다. 상단과 동일하게 verbose 가드 적용 또는 debug 레벨 권장입니다.
Also applies to: 1950-1956, 1972-1972
1835-1840: 배치 파싱 로그 레벨/가드 일관화(Combined)통합 파싱부 역시 동일 제안입니다. 대량 트래픽에서 로그비용을 낮출 수 있습니다.
Also applies to: 1846-1852, 1868-1868
39-39: 미사용 import 정리 제안
org.springframework.web.reactive.function.client.WebClient는 현재 파일에서 사용되지 않습니다. 빌드 설정에서 자동 정리가 없다면 제거를 권장합니다.
1226-1265: 배치 점수 JSON의 키로 '레시피 제목' 사용하는 구조의 취약성제목 문자열은 공백/특수문자/중복 등으로 인해 키 매칭 실패가 발생하기 쉽습니다(모델이 제목을 약간 변형해 반환하는 경우 포함). 배열 형태로
{ title, score }항목 리스트를 요구하는 포맷으로 바꾸면 견고성이 크게 향상됩니다.예:
"scores": [ { "title": "된장찌개", "score": 8.5 }, ... ]형태로 응답을 유도하고, 파싱은 리스트 순회로 처리하는 방식을 권장합니다. 필요 시 프롬프트와parseBatch*들에 대한 변경안 제공 가능합니다.원하시면 프롬프트/파서 양쪽을 안전한 배열 포맷으로 맞춘 패치안을 드리겠습니다.
814-829: 이미지 미확보 시 전량 스킵 정책 재검토현재 이미지 URL이 유효하지 않으면 해당 레시피를 DB에 저장하지 않습니다. UX 상 이미지가 중요하다면 유지 가능하나, 콘텐츠 확보를 우선하고 이미지는 비동기 보강(또는 기본 이미지)으로 가는 전략도 고려해볼 만합니다.
간단 대안:
- imageUrl 검증 실패 시 기본 이미지를 세팅하고 저장
- 별도 비동기 잡으로 이미지 재시도
📜 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
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
src/main/java/com/mumuk/domain/recipe/repository/RecipeRepository.java(1 hunks)src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java(40 hunks)src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java(4 hunks)src/main/java/com/mumuk/global/apiPayload/code/ErrorCode.java(1 hunks)src/main/java/com/mumuk/global/client/GeminiClient.java(1 hunks)src/main/java/com/mumuk/global/client/OpenAiClient.java(0 hunks)src/main/java/com/mumuk/global/config/GeminiClientConfig.java(1 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/com/mumuk/global/client/OpenAiClient.java
🔇 Additional comments (6)
src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java (3)
428-459: 프롬프트 개선 사항이 잘 반영됨대체 가능 조건을 세분화하고 명확한 예시를 추가한 것이 좋습니다. 특히 용도별 대체 가능성을 구분한 것이 실용적입니다.
304-310: Fallback 결과를 캐싱하지 않는 로직 추가됨AI 실패 시 생성되는 fallback 결과를 캐싱하지 않도록 한 것은 좋은 판단입니다. 이렇게 하면 일시적인 API 오류로 인한 잘못된 결과가 캐시되는 것을 방지할 수 있습니다.
552-567: 사용자 재료 검증 로직 강화replaceable 파싱 시 사용자 재료 존재 여부를 검증하는 로직이 추가되어 AI가 잘못된 대체 재료를 제시하는 경우를 방지합니다. 이는 데이터 무결성을 보장하는 좋은 개선사항입니다.
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java (3)
21-21: GeminiClient로의 의존성 전환 LGTMOpenAiClient → GeminiClient 전환과 필드/생성자 변경이 일관되게 반영되었습니다. 내부 호출부도 geminiClient로 교체되어 방향성 맞습니다.
Also applies to: 51-51
76-76: 랜덤 샘플 크기 상향(8→12) 합리적GET 기반 추천에서 표본을 키운 것은 다양성 확보 측면에서 타당합니다. 성능 이슈 시에도 DB 랜덤 샘플링 → PK 범위 → 메모리 셔플의 3단계 폴백이 있어 리스크 낮습니다.
793-799: AI 응답 JSON 추출 로직 범용성 확보코드블록 제거 후 중괄호 구간만 추출하는 접근은 안전합니다. 다만 Gemini가 순수 텍스트(예: 숫자 한 줄)만 반환하는 경우에도 문제 없도록, 추출 결과가 JSON이 아니면 그대로 사용하도록 이미 처리되어 있어 괜찮습니다. 현재 구현은 적절합니다.
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (7)
src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java (7)
428-433: 프롬프트에 “JSON만 반환”/문자열 일치 규칙을 명시해 파싱 안정성 강화 제안현재
extractJsonFromAIResponse로 JSON 블록을 추출하고 있으나, 모델이 서두 설명/코드펜스를 섞어 내놓으면 파싱 실패 리스크가 남습니다. 프롬프트에 다음 규칙을 추가해 응답 표준을 엄격하게 만들어 주세요. 특히 레시피/사용자 재료 문자열을 “목록에 나온 그대로” 사용할 것을 강제하면, 후처리에서 membership 체크가 더 견고해집니다.아래 패치를 텍스트 블록 내 “절대 규칙” 바로 뒤에 추가해 주세요:
절대 규칙: - 레시피 재료는 사용자 재료에 대해서 각각 match, mismatch, replaceable 중 오직 하나에만 속함 - 최종적으로 응답에 레시피 재료가 모두 포함되어 있어야 함 + + 응답 규칙: + - 반드시 순수 JSON만 반환(코드블록/설명/주석 금지) + - match, mismatch, replaceable.recipeIngredient 값은 "레시피 재료" 목록의 문자열을 정확히 그대로 사용 + - replaceable.userIngredient 값은 "사용자 재료" 목록에 실제로 존재하는 항목만 사용 + - 각 레시피 재료는 응답 전체에서 정확히 한 번만 등장해야 함Also applies to: 464-466
443-445: 사소한 용어/오타 수정 제안
- 문맥상 “찌개용” 항목은 “같은 찌개용도”가 자연스럽습니다.
- “케네프리유”는 오타로 보이며 “카놀라유”가 맞습니다. 모델 안내 품질에도 영향을 줍니다.
- - 돼지고기(찌개용) ↔ 목살(찌개용) (같은 돼지고기, 같은 구이용도) + - 돼지고기(찌개용) ↔ 목살(찌개용) (같은 돼지고기, 같은 찌개용도)- - 식용유 ↔ 올리브유, 포도씨유, 케네프리유 (식용유 종류) + - 식용유 ↔ 올리브유, 포도씨유, 카놀라유 (식용유 종류)Also applies to: 451-451
453-456: “2칸 이상 차이나는 경우” 표현이 모호합니다 — 계층/단계로 명확화 권장모델이 “칸”의 의미를 불명확하게 해석할 수 있습니다. “계층/단계” 용어로 바꾸면 오해를 줄일 수 있습니다.
- - 2칸 이상 차이나는 경우 (면 ↔ 파스타 ↔ 링귀니 : 면 링귀니는 대체불가, 돼지 등심(돈까스용) ↔ 돼지고기 ↔ 앞다리살(탕용)) + - 계층이 2단계 이상 떨어지는 경우 (면 ↔ 파스타 ↔ 링귀니: 상위-하위 관계에서 2단계 이상 차이나면 대체 불가. 예: 돼지 등심(돈까스용) ↔ 돼지고기 ↔ 앞다리살(탕용))
486-491: AI 실패 처리 일관성: 예외 대신 null 반환하여 상위 fallback 경로 사용 권장
analyzeIngredientsWithAI가 예외를 잡고 fallback JSON을 생성하므로,callAI에서 굳이BusinessException을 던질 필요가 없습니다. null 반환으로 통일하면 흐름이 단순해집니다.- log.error("AI 호출 실패", e); // 스택트레이스 포함 로깅 - throw new BusinessException(ErrorCode.OPENAI_API_ERROR); + log.error("AI 호출 실패", e); // 스택트레이스 포함 로깅 + return null;
489-489: 에러코드 명칭이 공급자와 불일치합니다 — OPENAI→AI(또는 GEMINI)로 정리 권장Gemini로 전환했는데 여전히
OPENAI_API_ERROR를 사용 중입니다. 에러코드 명칭을 공급자 중립(예:AI_API_ERROR/LLM_API_ERROR) 또는GEMINI_API_ERROR로 맞추는 것이 유지보수에 유리합니다.가능하면 아래처럼 변경해 주세요(코드베이스의 ErrorCode 정의 확인 필요):
- throw new BusinessException(ErrorCode.OPENAI_API_ERROR); + throw new BusinessException(ErrorCode.AI_API_ERROR);
291-293: Redis 캐시 키 충돌 최소화: List.hashCode 대신 안정적 해시 지문 사용 제안
List#hashCode()는 충돌 가능성이 존재합니다. 내용 기반의 SHA-256 지문을 사용하면 키 충돌 리스크를 실무적으로 제거할 수 있습니다.- String cacheKey = String.format("ai-match:%d:%d:%d:%d", - userId, recipeId, normUser.hashCode(), normRecipe.hashCode()); + String cacheKey = String.format("ai-match:%d:%d:%s", + userId, recipeId, stableFingerprint(normUser, normRecipe));아래 유틸리티 메서드를 클래스에 추가해 주세요(필요 import 포함):
// imports: // import java.nio.charset.StandardCharsets; // import java.security.MessageDigest; // import java.util.Base64; private static String stableFingerprint(List<String> normUser, List<String> normRecipe) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); // 구분자를 명시적으로 넣어 충돌 가능성 추가 감소 String payload = String.join("\u0001", normUser) + "\u0002" + String.join("\u0001", normRecipe); byte[] digest = md.digest(payload.getBytes(StandardCharsets.UTF_8)); // 키 길이를 너무 길게 만들지 않기 위해 Base64 URL-safe로 축약 return Base64.getUrlEncoder().withoutPadding().encodeToString(digest).substring(0, 32); } catch (Exception e) { // 실패 시 기존 해시로 폴백(실패 확률 극히 낮음) return String.valueOf(Objects.hash(normUser, normRecipe)); } }
303-304: AI 응답 전문 로깅은 debug 레벨로 낮추기 권장응답이 길어질 수 있어 info 레벨 상시 출력은 노이즈/비용이 큽니다. 필요 시에만 확인 가능하도록 debug로 내리는 것이 좋습니다.
- log.info("AI 분석 결과: {}", aiAnalysis); + if (log.isDebugEnabled()) { + log.debug("AI 분석 결과: {}", aiAnalysis); + }
📜 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
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java(4 hunks)
🔇 Additional comments (2)
src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java (2)
23-23: GeminiClient 전환 LGTM의존성/필드/생성자 주입 모두 일관되게 교체되었습니다. 서비스 내부 호출도
geminiClient로 정상 전환되어 보입니다.Also applies to: 46-46, 51-51, 56-56
414-479: 프롬프트 확장 전반은 도메인 친화적이고 명확합니다대체 기준(상위/하위, 용도, 조미료)과 배제 기준을 구체적으로 제시해 모델 일관성을 높인 점 좋습니다. 이후 제안한 “응답 규칙” 추가까지 적용되면 파싱 실패율을 더 낮출 수 있을 것으로 보입니다.
🎋 이슈 및 작업중인 브랜치
-#140
🔑 주요 내용
-모델 변경 및 프롬프트 개선

-재료 매칭 프롬프트 개선
Check List
Summary by CodeRabbit
New Features
Refactor
Chores