Conversation
Walkthrough
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 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: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
src/main/java/com/mumuk/global/client/GeminiClient.java (2)
16-17: 중복 빈 생성 위험: @component 제거 필요
GeminiClientConfig에서@Bean으로 동일 타입을 제공하므로, 여기의@Component와 중복되어 빈 충돌이 납니다. 하나만 유지하세요. 구성 클래스를 유지할 계획이라면 아래처럼@Component를 제거하세요.-@Component public class GeminiClient {
38-47: HTTP 에러 상태를 BusinessException으로 매핑하고 응답 크기 제한을 추가하세요.현재
retrieve()는 4xx/5xx를 기본 예외로 던지며, 메시지 표준화가 부족합니다. 상태코드별 매핑과 페이로드 사이즈 제한을 권장합니다.- return webClient.post() + return webClient.post() .uri("/v1beta/models/" + modelName + ":generateContent") .bodyValue(body) - .retrieve() + .retrieve() + .onStatus(s -> s.is4xxClientError() || s.is5xxServerError(), + resp -> resp.bodyToMono(String.class) + .defaultIfEmpty("") + .map(msg -> new BusinessException(ErrorCode.OPENAI_API_ERROR))) .bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {}) .map(this::extractContent);또한 과도한 응답으로 인한 메모리 사용을 줄이기 위해 서버측
maxResponseSize설정(게이트웨이/프록시)도 검토해 주세요.src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java (1)
1195-1219: 배치 JSON 예시에서 '점수' 토큰 대신 숫자 예시를 제공하세요.현재 예시가
"\"레시피제목\": 점수"형태라 모델이 문자열"점수"를 그대로 내보낼 위험이 있습니다. 실제 숫자 예시(예: 7.5)를 넣어 파서의asDouble과 정합성을 높이세요. 또한 헤더 대체 문자열 불일치(🎯 포함 버전 사용)를 수정하세요.- promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon().replace("=== 🎯 적합도 평가 기준 (우선순위 순) ===", "평가 기준:")).append("\n\n") + promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon().replace("=== 적합도 평가 기준 (우선순위 순) ===", "평가 기준:")).append("\n\n") .append("반드시 다음 JSON 형태로만 응답해줘:\n") .append("{\n") .append(" \"scores\": {\n"); @@ - promptBuilder.append(" \"").append(recipe.getTitle()).append("\": 점수"); + promptBuilder.append(" \"").append(recipe.getTitle()).append("\": 7.5");src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java (1)
1195-1219: 배치 JSON 예시에서 '점수' 토큰 위험: 숫자 예시로 교체RecipeServiceImpl과 동일하게 여기서도
"점수"문자열을 숫자 예시로 바꾸세요. 또한 헤더 대체 문자열에서 🎯 이모지 버전을 사용하고 있어 실제 반환 문자열과 불일치합니다.- promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon().replace("=== 🎯 적합도 평가 기준 (우선순위 순) ===", "평가 기준:")).append("\n\n") + promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon().replace("=== 적합도 평가 기준 (우선순위 순) ===", "평가 기준:")).append("\n\n") @@ - promptBuilder.append(" \"").append(recipe.getTitle()).append("\": 점수"); + promptBuilder.append(" \"").append(recipe.getTitle()).append("\": 7.5");
🧹 Nitpick comments (8)
src/main/resources/application.yml (1)
98-98: 정확도 모델 프로퍼티 추가는 적절합니다만, 프로파일별 기본값과 비용/지연 영향도 고려해 주세요.
gemini.api.model_accurate: gemini-1.5-pro추가는 구성 의도가 명확합니다. 운영/개발 프로파일에서 서로 다른 값을 사용하도록 분리(application-prod.yml,application-dev.yml)하면 비용과 지연(time-to-first-token)이 커질 수 있는 pro 사용을 더 안전하게 통제할 수 있습니다. 또한 현재@Value("${gemini.api.model_accurate:${gemini.api.model}}")로 폴백이 설정되어 있어, 해당 키가 누락되어도 안전하게 동작합니다.src/main/java/com/mumuk/global/config/GeminiClientConfig.java (1)
18-24: WebClient 타임아웃/커넥션 설정을 명시적으로 추가하는 것을 권장합니다.서비스단에서
.block(Duration.ofSeconds(...))을 쓰더라도, 커넥션/읽기 타임아웃은 Netty 레벨에서 별도로 설정해야 예기치 않은 hang을 줄일 수 있습니다.@Bean public WebClient geminiWebClient() { - return WebClient.builder() + return WebClient.builder() .baseUrl(baseUrl) .defaultHeader("x-goog-api-key", apiKey) + .clientConnector(new reactor.netty.http.client.HttpClientConnector( + reactor.netty.http.client.HttpClient.create() + .responseTimeout(java.time.Duration.ofSeconds(20)) + .option(io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + )) .build(); }src/main/java/com/mumuk/global/client/GeminiClient.java (2)
23-28: 생성자 파라미터의 @value 제거로 책임을 단일화하세요.이 클래스는 구성 클래스에서 명시적으로 인자 주입을 받고 있으므로, 생성자 파라미터의
@Value는 혼란을 유발합니다(컴포넌트 스캔 경로 제거 시 불필요). 아래처럼 제거해 주세요.- public GeminiClient(WebClient webClient, @Value("${gemini.api.model}") String model, - @Value("${gemini.api.model_accurate:${gemini.api.model}}") String accurateModel) { + public GeminiClient(WebClient webClient, String model, String accurateModel) { this.webClient = webClient; this.model = model; this.accurateModel = accurateModel; }
67-76: SafetySettings는 카테고리별 배열 구성 권장현재 하나의 카테고리만
BLOCK_NONE으로 지정되어 있습니다. 정책에 맞게 여러 카테고리를 명시하거나, 설정 주입 가능하게 빼두면 운영 통제가 수월합니다.- 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<>(); + for (String cat : List.of("HARM_CATEGORY_HARASSMENT","HARM_CATEGORY_SEXUAL", + "HARM_CATEGORY_HATE_SPEECH","HARM_CATEGORY_DANGEROUS_CONTENT")) { + Map<String, Object> s = new HashMap<>(); + s.put("category", cat); + s.put("threshold", "BLOCK_NONE"); + safetySettingsList.add(s); + } body.put("safetySettings", safetySettingsList);src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java (1)
510-517: 정확도 모델로의 전환 적절합니다.
geminiClient.chatAccurate(..., 15s)로의 변경은 본 파일의 분석 태스크 특성상 타당합니다. 필요 시 15s 타임아웃은 호출 경로별로 주입 가능하게 분리해 재사용성을 높일 수 있습니다.src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java (3)
987-994: 정확도 모델 사용 검토: 생성/분류 경로별 모델을 명시적으로 선택하세요.본 서비스는 배치/개별 점수화, 레시피 생성 등 다양한 호출을 포함합니다. 구조화(JSON) 응답 신뢰도가 중요한 경로(배치 스코어링 등)는
chatAccurate, 경량 생성은chat을 명시적으로 선택하도록 분리하면 운영 제어가 수월합니다. 최소한 아래처럼callAI를 정확도 모델로 변경하거나,callAI(modelType)형태로 확장하는 것을 권장합니다.- private String callAI(String prompt) { + private String callAI(String prompt) { try { - return geminiClient.chat(prompt).block(Duration.ofSeconds(30)); + return geminiClient.chatAccurate(prompt).block(Duration.ofSeconds(30)); } catch (Exception e) { log.error("Gemini API 호출 실패: {}", e.getMessage()); throw new BusinessException(ErrorCode.OPENAI_API_ERROR); } }
1800-1803: 헤더 대체 문자열 불일치(🎯) 수정
buildPriorityBasedSuitabilityPromptCommon()의 헤더는 이모지 없이 정의되어 있습니다. 아래처럼 통일하세요.- promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon().replace("=== 🎯 적합도 평가 기준 (우선순위 순) ===", "평가 기준:")).append("\n\n") + promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon().replace("=== 적합도 평가 기준 (우선순위 순) ===", "평가 기준:")).append("\n\n")
2145-2149: 프롬프트 타이틀 오타 수정
"===최우선 고려사항 알러지 정보보 ==="→"=== 최우선 고려사항 알레르기 정보 ===". 모델 입력 품질을 위해 타이틀/띄어쓰기를 바로잡으세요.- promptBuilder.append("===최우선 고려사항 알러지 정보보 ===\n"); + promptBuilder.append("=== 최우선 고려사항 알레르기 정보 ===\n");
📜 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 (5)
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java(1 hunks)src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java(1 hunks)src/main/java/com/mumuk/global/client/GeminiClient.java(1 hunks)src/main/java/com/mumuk/global/config/GeminiClientConfig.java(1 hunks)src/main/resources/application.yml(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: CI
🔇 Additional comments (1)
src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java (1)
541-657: AI 응답 파서 정합성/보정 로직 LGTM
- 집합화로 상호 배타성 확보
- 누락 보정으로 레시피 재료 전체 커버
- replaceable 검증 시 사용자/레시피 집합 교차 검증
안정적으로 보입니다.
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java (1)
474-476: 이전 리뷰 지적된 불필요 문자(‘g’) 제거 확인 – OK과거 라인 인근의 stray 문자 ‘g’가 제거되어 프롬프트 노이즈 이슈가 해소되었습니다. 그대로 진행해도 됩니다.
🧹 Nitpick comments (3)
src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java (3)
416-503: JSON-only 출력 강제 및 입력을 JSON 데이터로 제시해 파싱·프롬프트 인젝션 리스크 추가 완화 제안extractJsonFromAIResponse로 앞·뒤 설명을 잘라내긴 하지만, 모델 편차를 최소화하려면 애초에 “JSON만 출력”을 강제하고, 사용자/레시피 재료를 규칙이 아닌 “데이터(JSON)”로 주입하는 편이 더 견고합니다. 또한 replaceable.userIngredient가 반드시 사용자 재료 집합에서만 선택되어야 함을 명시하면 파싱/정합성 보정 비용이 줄어듭니다.
아래처럼 보강을 제안드립니다.
@@ ## 5단계: JSON 출력 형식 @@ -* 반드시 아래 JSON 구조를 따른다. -* 모든 레시피 재료는 세 그룹 중 하나에 반드시 포함되어야 한다. +* 반드시 아래 JSON 구조를 따른다. +* 출력은 오직 아래 JSON만 반환한다. 설명/해설/마크다운/코드블록을 포함하지 않는다. +* 모든 레시피 재료는 세 그룹 중 하나에 반드시 포함되어야 한다. +* match·mismatch·replaceable.recipeIngredient 값은 반드시 레시피 재료 목록에서만 선택한다. +* replaceable.userIngredient 값은 반드시 사용자 재료 목록에서만 선택한다. +* 레시피/사용자 재료 표기는 입력 목록의 표기를 그대로 사용한다(대소문자·공백 포함). +* 키는 정확히 "match", "mismatch", "replaceable"만 사용하고 추가 필드는 금지한다. @@ -**사용자 재료: %s** -**레시피 재료: %s** +입력(JSON): +{ + "userIngredients": %s, + "recipeIngredients": %s +}
461-470: ‘유사적 대체’ 적용 조건을 보수적으로 명시하여 절대불가 규칙과의 충돌 여지 축소“모짜렐라 ↔ 체다 (요리 종류에 따라)”처럼 조건부 예시는 모델이 과감히 대체를 택하도록 유도할 수 있습니다. 레시피 국적/조리법이 명확하지 않으면 mismatch로 귀결되도록 가드라인을 한 줄 보강하세요.
**3. 유사적 대체 (Similarity-based):** 동일 소속 내에서 품종이나 가공 방식만 약간 다른 경우. @@ * `스파게티면` ↔ `링귀니면` + * 주의: 레시피의 국적/조리법이 명확하지 않거나 확신이 없으면 본 기준을 적용하지 말고 `mismatch` 처리한다.
511-515: AI 호출 타임아웃 상수화 및 에러 코드 명칭 통일 필요현재
RecipeServiceImpl(lines 511–515) 구간에서 15초 하드코딩된 타임아웃과OPENAI_API_ERROR에러 코드가 사용되고 있습니다. 운영 환경에서 튜닝과 용어 일관성을 고려해 아래와 같이 변경을 권장드립니다.
타임아웃 상수화
- return geminiClient.chatAccurate(prompt).block(java.time.Duration.ofSeconds(15)); + return geminiClient.chatAccurate(prompt).block(AI_CALL_TIMEOUT);파일 상단(다른 TTL 상수들과 같은 위치)에 다음 상수 추가:
private static final java.time.Duration AI_CALL_TIMEOUT = java.time.Duration.ofSeconds(15);에러 코드 명칭 통일
전체 코드베이스에서ErrorCode.OPENAI_API_ERROR가 5회 사용되고 있습니다:src/main/java/com/mumuk/global/client/GeminiClient.java:157 src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java:514 src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java:846, 992 src/main/java/com/mumuk/global/apiPayload/code/ErrorCode.java:69(스크립트:
rg -n "OPENAI_API_ERROR" -g '!**/build/**' -g '!**/out/**')
Gemini 클라이언트를 사용하는 만큼,OPENAI_API_ERROR대신 공용AI_API_ERROR혹은GEMINI_API_ERROR를 새로 정의한 후 일괄 대체해 주세요.
📜 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 (3)
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java(1 hunks)src/main/java/com/mumuk/domain/recipe/service/RecipeServiceImpl.java(1 hunks)src/main/java/com/mumuk/global/config/GeminiClientConfig.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/java/com/mumuk/global/config/GeminiClientConfig.java
- src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java
There was a problem hiding this comment.
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 (4)
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java (4)
1182-1193: JSON 키로 ‘레시피 제목’을 사용하는 설계는 충돌/파싱 실패를 유발합니다 → ‘레시피 ID’로 변경 권장
- 제목은 중복/변형/특수문자/길이 문제로 안정적 키가 아닙니다. 현재 파서도
scoresNode.path(recipe.getTitle())로 조회하여 키 미스매치 시 기본 5.0이 남발될 수 있습니다.- ID를 JSON 키로 사용하면 매칭이 결정적이고, 국제화/이스케이프 이슈가 사라집니다.
프롬프트와 파싱 모두를 아래처럼 최소 변경하세요.
- 배치 프롬프트들에서 레시피 라벨 및 JSON 키를 ID로 교체
- String recipeText = "레시피: " + recipe.getTitle() + "\n" + + String recipeText = "레시피(id:" + recipe.getId() + "): " + recipe.getTitle() + "\n" + "재료: " + recipe.getIngredients() + "\n\n";- promptBuilder.append(" \"").append(recipe.getTitle()).append("\": 점수"); + promptBuilder.append(" \"").append(recipe.getId()).append("\": 점수");위 두 가지 수정은 다음 블록들에 공통 적용됩니다.
- 재료 기반 배치: Lines 1182-1184, 1207-1213
- 건강정보 기반 배치: Lines 1600-1602, 1625-1631
- 통합 배치: Lines 1787-1789, 1812-1818
- HealthGoal 배치: Lines 1891-1893, 1916-1923
- 파서에서 키 조회를 ID로
- double score = scoresNode.path(recipe.getTitle()).asDouble(5.0); + double score = scoresNode.path(String.valueOf(recipe.getId())).asDouble(5.0);적용 위치:
- 재료 기반 파싱: Lines 1244-1246
- 건강정보 기반 파싱: Lines 1662-1664
- 통합 파싱: Lines 1849-1851
- HealthGoal 파싱: Lines 1953-1955
부가 제안(선택): 키를 ID로 바꾸되, 모델 친화성을 위해 “출력 형식 예시”에도 실제 숫자 예시를 넣어주면 더 정확한 응답을 유도할 수 있습니다. Also applies to: 1201-1216, 1600-1611, 1619-1632, 1787-1798, 1806-1819, 1891-1902, 1912-1923, 1243-1247, 1661-1665, 1848-1852, 1952-1956 --- `2146-2149`: **오타/용어 통일: “알러지 정보보” → “알레르기 정보” 및 전체 문구 정돈** - Line 2146: “정보보” 오타. - 파일 전반에서 “알러지/알레르기”가 혼재합니다. 사용자 메시지(프롬프트)에서는 용어를 일관되게 쓰는 것이 중요합니다. ```diff - promptBuilder.append("===최우선 고려사항 알러지 정보보 ===\n"); + promptBuilder.append("=== 최우선 고려사항: 알레르기 정보 ===\n"); promptBuilder.append(buildAllergyPrompt(allergyTypes)); - promptBuilder.append("위 알러지 성분이 포함된 요리는 절대 추천하지 마세요. (0점 처리)\n\n"); + promptBuilder.append("위 알레르기 성분이 포함된 요리는 절대 추천하지 마세요. (0점 처리)\n\n");아래 평가기준 문구도 용어 통일을 권장합니다.
- 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" + "- 1-2점: 대부분의 조건을 만족하지 못함\n" + - "- 0점: 알러지 성분 포함 (절대 추천 불가)\n\n" + + "- 0점: 알레르기 성분 포함 (절대 추천 불가)\n\n" + "적합도 점수만 숫자로 응답해주세요 (예: 8.5)";Also applies to: 2180-2191
1195-1195:replace대상 문자열의 이모지 불일치로 치환이 동작하지 않습니다
buildPriorityBasedSuitabilityPromptCommon()이 반환하는 헤더는 이모지 없이"=== 적합도 평가 기준 (우선순위 순) ==="이므로, 이모지 포함 검색 문자열을 사용하는 다음 두 곳에서는 치환이 아예 수행되지 않습니다:
- src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java:1195
- src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java:1800
수정 제안(diff):
--- a/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java +++ b/src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java @@ -1195,1 +1195,1 @@ - promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon() - .replace("=== 🎯 적합도 평가 기준 (우선순위 순) ===", "평가 기준:")) + promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon() + .replace("=== 적합도 평가 기준 (우선순위 순) ===", "평가 기준:")) @@ -1800,1 +1800,1 @@ - promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon() - .replace("=== 🎯 적합도 평가 기준 (우선순위 순) ===", "평가 기준:")) + promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon() + .replace("=== 적합도 평가 기준 (우선순위 순) ===", "평가 기준:"))대안(권장):
공통 메서드에 “헤더 포맷 옵션” 파라미터를 추가해, 호출부에서 직접 원하는 헤더를 구성하도록 변경하세요. 이모지 유무를 옵션화하면.replace()대신 포맷 제어가 명확해집니다.
1371-1375: 로그 레벨 최소화 및 상세 로그 게이트 적용 필요운영 환경의 기본 로그 레벨(info)에서 사용자의 재료·알레르기·건강정보가 노출되어 개인정보·민감정보 유출 위험이 있습니다. 아래 모든 블록에서
log.info를ENABLE_VERBOSE_LOG게이트(또는debug레벨)로 처리하도록 리팩터링하세요.– 개선 대상 위치
- src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java
• 392–395: 초기 재료 기반 적합도 평가 (=== 적합도 평가 시작 ===)
• 660–663: OCR 건강 정보 기반 평가 (=== 건강 정보 기반 적합도 평가 시작 ===)
• 1371–1375: 건강 목표 기반 평가 (=== HealthGoal 기반 적합도 평가 시작 ===)
• 1399–1404: 통합 평가 (=== 통합 적합도 평가 시작 ===)– (Optional) src/main/java/com/mumuk/domain/ocr/service/OcrServiceImpl.java
• 140–141: 건강 데이터 파싱 시작 로그
• 278: OCR 결과 저장 완료 로그- log.info("=== HealthGoal 기반 적합도 평가 시작 ==="); - log.info("사용자 보유 재료: {}", String.join(", ", availableIngredients)); - log.info("사용자 알레르기 정보: {}", allergyTypes.isEmpty() ? "없음" : String.join(", ", allergyTypes)); - log.info("사용자 건강 목표: {}", String.join(", ", healthGoals)); - log.info("전체 레시피 수: {}", recipes.size()); + if (ENABLE_VERBOSE_LOG) { + log.info("=== HealthGoal 기반 적합도 평가 시작 ==="); + log.info("사용자 보유 재료: {}", String.join(", ", availableIngredients)); + log.info("사용자 알레르기 정보: {}", allergyTypes.isEmpty() ? "없음" : String.join(", ", allergyTypes)); + log.info("사용자 건강 목표: {}", String.join(", ", healthGoals)); + log.info("전체 레시피 수: {}", recipes.size()); + }
🧹 Nitpick comments (8)
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java (8)
1043-1053: 프롬프트 지침 강화 반영 좋습니다. 단, 줄바꿈 누락 및 자잘한 문구 다듬기 권장
- Line 1050 끝에 개행이 없어 바로 이어지는 "※ 카테고리 선택:"과 붙습니다. 가독성과 파싱 안정성을 위해 개행 추가하세요.
- "- 다양한 재료를 이용할 수 있는 경우 큰 틀로 작성 (앞다리살/삼겹살/목살 → 돼지고기, 상추/로메인/샐러리 → 샐러드 채소)"+ + "- 다양한 재료를 이용할 수 있는 경우 큰 틀로 작성 (앞다리살/삼겹살/목살 → 돼지고기, 상추/로메인/샐러리 → 샐러드 채소)\n" +추가 제안(선택):
- “알레르기” 용어를 파일 전반에서 통일해 주세요(“알러지” 혼재).
- 예시 JSON 유도부에 실제 숫자 예시(예: 8.5)를 넣으면 모델이 “점수” 텍스트를 그대로 출력하는 리스크가 줄어듭니다.
231-233: 주석 숫자 ‘48개’가 상수와 불일치합니다.
- 실제 코드는
RANDOM_SAMPLE_SIZE(현재 12)를 사용합니다. 하드코딩된 “48개” 주석은 혼동을 줍니다.- // DB 레벨에서 랜덤 샘플링으로 48개 조회 + // DB 레벨에서 랜덤 샘플링으로 RANDOM_SAMPLE_SIZE개 조회
1101-1103: 프롬프트 길이/재료 목록info로그 → verbose 게이트 권장
- 프롬프트 길이와 재료 목록은 운영 기본 로그에 상시 남길 필요가 낮습니다. 상단의
ENABLE_VERBOSE_LOG와 정책을 일관시키세요.- 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()); + }
2028-2031: 예외 매핑의 의미 보존 필요(일괄 ‘INVALID_RESPONSE’로 래핑 시 원인 손실)
- 여기서는 어떤 예외든
OPENAI_INVALID_RESPONSE로 변환됩니다. DB 오류/네트워크 타임아웃 등도 동일 코드로 래핑되어 원인 추적이 어렵습니다.- 바로 위
callAIAndSaveRecipes는 이미BusinessException을 던지므로,BusinessException은 재래핑 없이 통과시키고 일반 예외만OPENAI_API_ERROR등으로 매핑하세요.- } 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(), e); + throw e; + } catch (Exception e) { + log.error("AI 랜덤 레시피 생성 실패(Unexpected): {}", e.getMessage(), e); + throw new BusinessException(ErrorCode.OPENAI_API_ERROR); }
1171-1173: 주석의 공급자 지칭(OpenAI) → 모델/플랫폼 중립 표현으로 정리
- 이제 Gemini Accurate 경로를 사용하므로 “OpenAI 토큰 제한 고려” 등 공급자 특정 표현은 혼란을 줄 수 있습니다.
- // OpenAI 토큰 제한 고려 (대략 1토큰 = 4글자) + // 모델 토큰/컨텍스트 제한 고려 (대략 1토큰 ≈ 4자 기준)Also applies to: 1576-1578, 1776-1778, 1880-1881
761-771: 점수 추출 정규식 보완 제안(다수 숫자 혼재 시 오인 파싱 가능)
- 현재
replaceAll("[^0-9.]", "")는 “8/10” → “8.10”, “10.0 (8점 권장)” → “10.08” 같이 합쳐질 수 있어Double.parseDouble실패 또는 오인값이 됩니다.- 첫 번째 부동소수점 숫자 캡처로 안전하게 추출하세요.
- scoreStr = scoreStr.replaceAll("[^0-9.]", ""); - if (scoreStr.isEmpty()) { + java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d{1,2}(?:\\.\\d+)?)").matcher(scoreStr); + if (!m.find()) { log.warn("점수 추출 실패, 기본값 5.0 사용"); return 5.0; } - double score = Double.parseDouble(scoreStr); + double score = Double.parseDouble(m.group(1));
38-38: 미사용 import 제거 권장: WebClient
RecipeRecommendServiceImpl.java파일에 선언된WebClientimport가 해당 파일 내에서 전혀 참조되지 않습니다. 빌드 시 경고를 방지하기 위해 아래와 같이 삭제를 권장합니다.파일:
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java
라인: 38제거할 코드:
- import org.springframework.web.reactive.function.client.WebClient;
986-994: 타임아웃 처리 및 예외 매핑 개선 제안callAI 메서드에서 Reactor 이벤트 루프 고착을 방지하고, 장애 유형별 관측성을 높이기 위해 다음 사항을 적용해 보세요.
- block 호출 대신 Reactor의
timeout오퍼레이터 사용- 타임아웃, HTTP 오류 등 예외 유형별로 로깅 메시지·ErrorCode 매핑
- 타임아웃 값을 프로퍼티로 외부화해 운영 중 조정 가능하도록 설정
수정 대상:
- src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java
callAI(라인 986 부근)@@ src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java:986 - return geminiClient.chatAccurate(prompt).block(Duration.ofSeconds(30)); + return geminiClient.chatAccurate(prompt) + .timeout(AI_TIMEOUT) // 설정된 Duration + .block(); @@ src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java:990 - log.error("Gemini Pro API 호출 실패: {}", e.getMessage()); - throw new BusinessException(ErrorCode.OPENAI_API_ERROR); + if (e.getCause() instanceof java.util.concurrent.TimeoutException) { + log.error("Gemini Pro API 호출 타임아웃: {}", e.getMessage()); + throw new BusinessException(ErrorCode.OPENAI_API_TIMEOUT); + } + log.error("Gemini Pro API 호출 실패", e); + throw new BusinessException(ErrorCode.OPENAI_API_ERROR);추가(선택): 클래스 상단에 프로퍼티 외부화 및 상수 정의
@Value("${gemini.api.timeout-seconds:30}") private long aiTimeoutSec; private final Duration AI_TIMEOUT = Duration.ofSeconds(aiTimeoutSec);
📜 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/RecipeRecommendServiceImpl.java(2 hunks)
There was a problem hiding this comment.
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 (6)
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java (6)
1218-1243: 배치 평가 프롬프트에서 출력 지시 충돌 및 replace 대상 불일치로 인한 모델 혼란 가능성현재 공통 기준 문자열(buildPriorityBasedSuitabilityPromptCommon) 끝에 "적합도 점수만 숫자로 응답" 지시가 포함되어 있는데, 배치 프롬프트에서는 이어서 "JSON만 응답"을 요구합니다. 상충 지시로 모델이 혼란을 겪거나 숫자만 반환할 위험이 있습니다. 또한 일부 위치에서
.replace("=== 🎯 적합도 평가 기준 (우선순위 순) ===", ...)처럼 실제 문자열과 불일치(🎯 이모지)하여 치환이 동작하지 않습니다.권장 수정:
- 공통 기준 메서드에서 "숫자만 응답" 문구 제거(기준만 제공).
- 배치 프롬프트에서는 JSON 지시만 포함.
- 단건 평가 프롬프트(create*SuitabilityPrompt 계열)에서만 "숫자만 응답"을 별도 추가.
- replace 오타(🎯) 제거 또는 단순히 기준을 그대로 붙이기.
필수 diff(출력 지시 제거 및 replace 대상 수정):
@@ private String buildPriorityBasedSuitabilityPromptCommon() { - return "=== 적합도 평가 기준 (우선순위 순) ===\n" + + return "=== 적합도 평가 기준 (우선순위 순) ===\n" + "1. 알러지 성분 포함 여부 (최우선, 포함시 0점)\n" + "2. 건강 목표 달성 도움 정도\n" + "3. 현재 건강 상태 적합성\n" + "4. 보유 재료 활용도\n\n" + - "점수 기준:\n" + + "점수 기준:\n" + "- 9-10점: 모든 조건을 완벽하게 만족 (건강 목표 최적, 건강 상태 적합, 재료 완벽)\n" + "- 7-8점: 대부분의 조건을 만족 (건강 목표 적합, 건강 상태 적합, 재료 충분)\n" + "- 5-6점: 주요 조건을 만족 (건강 목표 보통, 건강 상태 보통, 재료 가능)\n" + "- 3-4점: 일부 조건만 만족 (건강 목표 부적합, 건강 상태 부적합, 재료 부족)\n" + "- 1-2점: 대부분의 조건을 만족하지 못함\n" + - "- 0점: 알러지 성분 포함 (절대 추천 불가)\n\n" + - "적합도 점수만 숫자로 응답해주세요 (예: 8.5)"; + "- 0점: 알러지 성분 포함 (절대 추천 불가)\n"; }@@ private String createBatchIngredientSuitabilityPrompt(...) - promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon().replace("=== 🎯 적합도 평가 기준 (우선순위 순) ===", "평가 기준:")).append("\n\n") + promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon()).append("\n\n") .append("반드시 다음 JSON 형태로만 응답해줘:\n")@@ private String createBatchHealthSuitabilityPrompt(...) - promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon().replace("=== 적합도 평가 기준 (우선순위 순) ===", "평가 기준:")).append("\n\n") + promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon()).append("\n\n") .append("반드시 다음 JSON 형태로만 응답해줘:\n")@@ private String createBatchCombinedSuitabilityPrompt(...) - promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon().replace("=== 🎯 적합도 평가 기준 (우선순위 순) ===", "평가 기준:")).append("\n\n") + promptBuilder.append(buildPriorityBasedSuitabilityPromptCommon()).append("\n\n") .append("반드시 다음 JSON 형태로만 응답해줘:\n")@@ private String createBatchHealthGoalSuitabilityPrompt(...) - promptBuilder.append(buildHealthGoalSuitabilityPromptCommon().replace("위 정보를 바탕으로", "평가 기준:")).append("\n\n") + promptBuilder.append(buildHealthGoalSuitabilityPromptCommon()).append("\n\n") .append("반드시 다음 JSON 형태로만 응답해줘:\n")그리고 단건 평가 프롬프트들(createIngredientSuitabilityPrompt / createHealthSuitabilityPrompt / createHealthGoalSuitabilityPrompt / createCombinedSuitabilityPrompt) 끝에만 다음 한 줄을 추가해 주세요(배치 프롬프트에는 추가 금지).
// 단건 적합도 프롬프트 끝에만 추가 promptBuilder.append("\n적합도 점수만 숫자로 응답하세요 (예: 8.5)");Also applies to: 1636-1661, 1823-1849, 1927-1953, 2201-2215
744-779: 숫자 추출 로직이 부정확합니다(예: “9.7/10” → “9.710” 파싱 버그). JSON 스키마도 OpenAI 전용 필드에 의존현재
replaceAll("[^0-9.]", "")는 "9.7/10"을 "9.710"으로 만들어 오탐합니다. 또한{ choices[0].message.content }경로는 Gemini 응답 스키마와 맞지 않습니다. 첫 번째 실수(소수)만 안전하게 추출하거나 JSON 내 숫자 필드를 탐색하도록 수정해 주세요.적용 예시(diff):
@@ private double callAIForSuitabilityScore(String prompt) { - String scoreStr = response.trim(); + String scoreStr = response.trim(); @@ - // 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; - } - } + // JSON 응답일 가능성 우선 처리: {"score": 8.5} 또는 {"scores": {...}} + if (scoreStr.startsWith("{")) { + try { + JsonNode json = objectMapper.readTree(scoreStr); + if (json.has("score")) { + double v = json.path("score").asDouble(Double.NaN); + if (!Double.isNaN(v)) return clampScore(v); + } + if (json.has("value")) { + double v = json.path("value").asDouble(Double.NaN); + if (!Double.isNaN(v)) return clampScore(v); + } + JsonNode scoresNode = json.path("scores"); + if (scoresNode.isObject() && scoresNode.fieldNames().hasNext()) { + String firstKey = scoresNode.fieldNames().next(); + double v = scoresNode.path(firstKey).asDouble(Double.NaN); + if (!Double.isNaN(v)) return clampScore(v); + } + } catch (Exception e) { + log.warn("JSON 파싱 실패: {}", e.getMessage()); + // fall-through → 자유 텍스트에서 숫자 추출 + } + } @@ - // 숫자만 추출 (소수점 포함) - scoreStr = scoreStr.replaceAll("[^0-9.]", ""); - - if (scoreStr.isEmpty()) { + // 자유 텍스트에서 '첫 번째 실수'만 추출 + java.util.regex.Matcher m = + java.util.regex.Pattern.compile("([0-9]+(?:\\.[0-9]+)?)") + .matcher(response); + if (!m.find()) { log.warn("점수 추출 실패, 기본값 5.0 사용"); return 5.0; } - - double score = Double.parseDouble(scoreStr); - - // 점수 범위 검증 (0.0 ~ 10.0) - if (score < 0.0 || score > 10.0) { - log.warn("AI 응답의 점수가 범위를 벗어남: {}, 기본값 5.0 사용", score); - return 5.0; - } - - return score; + double score = Double.parseDouble(m.group(1)); + return clampScore(score);그리고 아래 보조 메서드를 클래스 내부(가까운 유틸 영역)에 추가하세요.
private double clampScore(double v) { if (Double.isNaN(v)) return 5.0; if (v < 0.0 || v > 10.0) { log.warn("AI 응답 점수 범위 초과: {}, 기본값 5.0 사용", v); return 5.0; } return v; }추가로,
java.util.regex임포트가 필요합니다.import java.util.regex.Matcher; import java.util.regex.Pattern;
1450-1476: 건강 목표/건강 정보 및 사용자 데이터 로그 민감도 조정 필요(PII/PHI 최소화)건강 목표/건강 상태, AI 원문 응답, 개별 점수 등 민감 정보가
info레벨로 무조건 로그됩니다. 운영 환경에서는 PHI에 해당할 수 있어 노출 위험이 큽니다.ENABLE_VERBOSE_LOG가드 또는debug레벨로 내리고, 상세 값은 마스킹/요약하세요.예시(diff):
@@ - log.info("HealthGoal 기반 배치 처리 시작 - 사용자 재료: {}, 알레르기: {}, 건강목표: {}", + if (ENABLE_VERBOSE_LOG) log.debug("HealthGoal 기반 배치 처리 시작 - 사용자 재료: {}, 알레르기: {}, 건강목표: {}", String.join(", ", availableIngredients), allergyTypes.isEmpty() ? "없음" : String.join(", ", allergyTypes), String.join(", ", healthGoals)); @@ - log.info("HealthGoal 기반 배치 프롬프트 생성 완료"); + if (ENABLE_VERBOSE_LOG) log.debug("HealthGoal 기반 배치 프롬프트 생성 완료"); @@ - log.info("AI 배치 응답: {}", batchResponse); + if (ENABLE_VERBOSE_LOG) log.debug("AI 배치 응답 수신 (length={} chars)", batchResponse != null ? batchResponse.length() : -1); @@ - log.info("파싱된 점수: {}", scores); + if (ENABLE_VERBOSE_LOG) log.debug("파싱된 점수 {}건", scores.size()); @@ - log.info("레시피 '{}' 적합도 점수: {}", recipe.getTitle(), score); + if (ENABLE_VERBOSE_LOG) log.debug("레시피 '{}' 적합도 점수: {}", recipe.getTitle(), score);동일 패턴을
processIndividualByHealthGoal,evaluateRecipeSuitabilityByHealthGoal등 건강 관련 로깅 전반에 적용해 주세요.Also applies to: 1495-1516, 1394-1409
2029-2055: AI 원격 호출을 트랜잭션 경계 안에서 수행 — 장기 트랜잭션/잠금 확대 리스크
@Transactional메서드에서 네트워크 호출(30초 타임아웃) 후 DB 쓰기가 수행됩니다. 실패 시 롤백은 되지만, 트랜잭션 유지 시간이 불필요하게 길어지고 DB 잠금·풀 점유 위험이 큽니다. AI 호출을 트랜잭션 밖으로 분리하고, 저장 단계만 짧은 트랜잭션으로 묶어주세요.권장 방향:
- public API 메서드에서는 프롬프트 생성 → AI 호출(비트랜잭션) → 파싱/검증.
- 저장 전용 메서드를
@Transactional(propagation = REQUIRES_NEW)로 분리하여 짧게 커밋.- 또는
TransactionTemplate로 DB 저장 구간만 감싸기.스케치 코드:
public List<RecipeResponse.DetailRes> createAndSaveRandomRecipes(Long userId, String topic) { getUser(userId); String prompt = buildRecipePostPromptRandom(topic); // (비트랜잭션) AI 호출 및 파싱 → transient Recipe 리스트 List<Recipe> transientRecipes = callAIAndBuildRecipesWithoutSaving(prompt); // (짧은 트랜잭션) 저장 List<Recipe> saved = persistRecipes(transientRecipes); // @Transactional(REQUIRES_NEW) return saved.stream().map(RecipeConverter::toDetailRes).toList(); }현재 구조에서는
callAIAndSaveRecipes내부에서 저장까지 수행하므로, 저장 부분을 분리(callAIAndBuildRecipesWithoutSaving+persistRecipes)하는 리팩터가 필요합니다. 원하시면 제가 안전한 단계별 리팩터링 패치를 제안드리겠습니다.Also applies to: 2069-2094, 2096-2124
925-953: 제목 기반 중복 체크의 정규화 미흡(대소문자/공백/유니코드 차이로 중복 누락 가능)현재 Redis/DB 조회 모두 원문 제목 그대로 비교합니다. 대소문자/공백/全角·半角 등 미세 차이로 중복을 통과할 수 있습니다. 키/조회 모두 동일한 정규화 규칙을 적용하세요.
적용 예시(diff):
@@ private boolean isDuplicateRecipe(Recipe recipe) { - String title = recipe.getTitle(); + String title = recipe.getTitle(); + String normTitle = normalizeTitle(title); @@ - Double score = redisTemplate.opsForZSet().score(RECIPE_TITLES_KEY, title); + Double score = redisTemplate.opsForZSet().score(RECIPE_TITLES_KEY, normTitle); @@ - boolean isDuplicate = recipeRepository.existsByTitle(title); + boolean isDuplicate = recipeRepository.existsByTitle(title) || recipeRepository.existsByTitleIgnoreCase(title); @@ - redisTemplate.opsForZSet().add(RECIPE_TITLES_KEY, title, System.currentTimeMillis()); + redisTemplate.opsForZSet().add(RECIPE_TITLES_KEY, normTitle, System.currentTimeMillis());@@ private void cacheRecipeTitle(String title) { - redisTemplate.opsForZSet().add(RECIPE_TITLES_KEY, title, System.currentTimeMillis()); + redisTemplate.opsForZSet().add(RECIPE_TITLES_KEY, normalizeTitle(title), System.currentTimeMillis());그리고 아래 정규화 유틸을 클래스에 추가하세요.
private String normalizeTitle(String s) { if (s == null) return ""; String t = s.trim().toLowerCase(); // 연속 공백 단일화 t = t.replaceAll("\\s+", " "); // NFC 정규화(필요 시) return java.text.Normalizer.normalize(t, java.text.Normalizer.Form.NFC); }Also applies to: 2130-2138
1218-1243: 배치 JSON 키로 '레시피 제목' 사용 → 매칭 취약(제목 변경/공백/따옴표 포함 시 파싱 실패)AI가 제목 철자를 바꾸거나 공백/따옴표가 포함되면 JSON 키 매칭이 깨질 수 있습니다. 안정적인 식별자인
recipe.getId()를 JSON 키로 사용하고, 파싱도 ID 기준으로 수행하는 것을 권장합니다.적용 방향(개략):
- 프롬프트 JSON 스켈레톤:
"scores": { "123": 8.5, "124": 7.0, ... }(키: 문자열화된 ID)- 파싱:
scoresNode.path(String.valueOf(recipe.getId()))필요시 제가 일괄 패치(diff) 제안드리겠습니다.
Also applies to: 1641-1661, 1828-1849, 1948-1953
♻️ Duplicate comments (1)
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java (1)
1037-1094: 프롬프트 문구 품질(오타·어색한 예시·용어 통일) 다듬기 제안전반적으로 잘 구조화된 프롬프트입니다. 다만 다음 문구들은 모델 일관성과 가독성을 위해 손봐 주세요. 과거 코멘트에서도 유사 지적이 있었던 영역으로, 이번 라운드에서 함께 정리되면 좋겠습니다.
- "코드 또는 햇살숭어"는 한국어 일반 표기와 예시로 어색합니다. "대구 또는 농어"처럼 흔한 식재료로 교체 권장.
- "(선택)", "(옵션)" 등 용어 표기를 일관되게.
- 일부 문장 간 군더더기 표현 정리.
적용 예시(diff):
@@ - "* **'A 또는 B' 해석:** 입력에 '또는', '/' 등이 포함된 경우, 이는 **별개의 선택지**임을 의미한다. '코드 또는 햇살숭어'가 주어지면, '코드'를 사용한 요리 **혹은** '햇살숭어'를 사용한 요리를 각각 추천해야 한다. **두 재료를 한 요리에 섞는 것은 절대 금지된다.**\n" + + "* **'A 또는 B' 해석:** 입력에 '또는' 또는 '/'가 포함되면 **서로 다른 선택지**로 해석한다. 예: '대구 또는 농어'라면 '대구' 요리 또는 '농어' 요리를 각각 추천한다. **두 재료를 한 요리에 혼합하지 않는다.**\n" + @@ - "* **'(선택)' 재료 해석:** '(선택)', '(옵션)' 등으로 표시된 재료는 필수가 아니다. 해당 재료가 없어도 완성되는 레시피를 우선적으로 고려해야 한다.\n" + + "* **'(선택)' 재료 해석:** '(선택)' 또는 '(옵션)' 표시는 **비필수**를 의미한다. 해당 재료 없이도 완성되는 레시피를 우선 고려한다.\n" +
🧹 Nitpick comments (6)
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java (6)
231-233: 주석과 실제 샘플 크기 상이(48 → RANDOM_SAMPLE_SIZE=12)"DB 레벨에서 랜덤 샘플링으로 48개 조회" 주석은 현재 상수값(12)과 불일치합니다. 주석을 상수 기반으로 갱신해 주세요.
- // DB 레벨에서 랜덤 샘플링으로 48개 조회 + // DB 레벨에서 랜덤 샘플링으로 RANDOM_SAMPLE_SIZE개 조회
2168-2195: 오타: "===최우선 고려사항 알러지 정보보 ===" → "=== 최우선 고려사항: 알레르기 정보 ==="헤더 오타와 띄어쓰기를 수정해 주세요. 동일 메서드 내에서 '알러지/알레르기' 표기도 통일(표준: 알레르기) 권장합니다.
- promptBuilder.append("===최우선 고려사항 알러지 정보보 ===\n"); + promptBuilder.append("=== 최우선 고려사항: 알레르기 정보 ===\n"); - promptBuilder.append("위 알러지 성분이 포함된 요리는 절대 추천하지 마세요. (0점 처리)\n\n"); + promptBuilder.append("위 알레르기 성분이 포함된 요리는 절대 추천하지 마세요. (0점 처리)\n\n");
1999-2026: 카테고리 기반 추천에서 이중 제한 및 잠재 중복 제거 필요
getRecipesByCategories에서 이미limit(MAX_RECOMMENDATIONS)후, 호출부(recommendRecipesByCategories)에서도 다시 limit를 적용합니다. 불필요한 이중 제한이며, 다중 카테고리 교집합 시 중복 레시피가 섞일 수도 있습니다. 리턴 전 distinct 처리와 한 곳에서만 limit 적용을 권장합니다.예시:
- return recipes.stream() - .limit(MAX_RECOMMENDATIONS) - .collect(Collectors.toList()); + return recipes.stream() + .distinct() + .collect(Collectors.toList());그리고 호출부에서만
limit(MAX_RECOMMENDATIONS)적용.Also applies to: 141-172
786-848: AI 원문 응답 전체 로깅(ENABLE_VERBOSE_LOG)도 길이 제한/해시 요약으로 축소 권장원문 응답은 길이·민감도 면에서 부담이 큽니다. 운영에서는 길이와 해시만 남기고 본문은 마스킹하는 편이 안전합니다.
- if (ENABLE_VERBOSE_LOG) log.info("AI 원본 응답: {}", response); + if (ENABLE_VERBOSE_LOG) { + String digest = Integer.toHexString(response.hashCode()); + log.info("AI 응답 수신(length={}, digest={})", response.length(), digest); + }
1099-1106: 알레르기 표기의 일관성(알러지 ↔ 알레르기) 통일사용자-facing 프롬프트에서는 표준 표기인 "알레르기"로 통일하세요.
- return "※ 사용자 알레르기 정보: " + String.join(", ", allergyTypes) + "\n" + - "※ 중요: 위 알레르기 성분이 포함된 요리는 절대 추천하지 마세요.\n"; + return "※ 사용자 알레르기 정보: " + String.join(", ", allergyTypes) + "\n" + + "※ 중요: 위 알레르기 성분이 포함된 요리는 절대 추천하지 마세요.\n";(메시지 상에는 이미 '알레르기' 표기를 사용 중이므로, 다른 영역의 '알러지' 표기들도 함께 정리 권장)
986-994: Gemini API 호출 로깅 및 예외 매핑 개선 제안
대상 메서드
src/main/java/com/mumuk/domain/recipe/service/RecipeRecommendServiceImpl.java의callAI(String prompt)(Lines 986–994)- 유사 구현부:
RecipeServiceImpl.java의callAI(참고)제안 사항
- 상관관계 ID(correlation ID) 도입
- 각 요청마다 UUID 등을 생성해 SLF4J MDC에 삽입한 뒤 로그 포맷에 포함하세요.
- 예시:
String correlationId = UUID.randomUUID().toString(); MDC.put("correlationId", correlationId); try { //… } finally { MDC.clear(); }- 이를 통해 호출 응답 간 추적성이 확보됩니다.
- 프롬프트 본문 로깅 금지
- AI 프롬프트에는 민감 정보가 포함될 수 있어, 로그에 남기지 않는 것을 권장합니다.
- 예외별 ErrorCode 매핑
- 현재
catch (Exception e)로 모든 예외를 잡아OPENAI_API_ERROR로 일괄 매핑하고 있어, 타임아웃(TimeoutException), 레이트 리밋(WebClientResponseException상태 429) 등 주요 예외를 구분하여 적절한ErrorCode로 던지면 문제 원인 파악에 도움이 됩니다.- 예시:
- } catch (Exception e) { - log.error("Gemini Pro API 호출 실패: {}", e.getMessage()); - throw new BusinessException(ErrorCode.OPENAI_API_ERROR); - } + } catch (TimeoutException te) { + log.error("Gemini Pro API 타임아웃 (30s 초과)", te); + throw new BusinessException(ErrorCode.OPENAI_TIMEOUT); + } catch (WebClientResponseException.TooManyRequests rte) { + log.error("Gemini Pro 레이트 리밋(429) 응답", rte); + throw new BusinessException(ErrorCode.OPENAI_RATE_LIMIT); + } catch (Exception e) { + log.error("Gemini Pro API 호출 예외", e); + throw new BusinessException(ErrorCode.OPENAI_API_ERROR); + }- 로그 일관성 유지
RecipeServiceImpl.callAI에서는 스택트레이스 전체를 로깅하나(log.error("AI 호출 실패", e)),RecipeRecommendServiceImpl에서는 메시지만 남기고 있어 통일을 권장합니다.위 개선은 운영 안정성 및 디버깅 생산성 향상을 위한 선택적 리팩터링입니다.
📜 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/RecipeRecommendServiceImpl.java(3 hunks)
🎋 이슈 및 작업중인 브랜치
🔑 주요 내용
Check List
Summary by CodeRabbit
New Features
Bug Fixes
Chores