[VISION] 종 추정 정확도 및 UX 개선#16
Conversation
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Summary by CodeRabbit
Walkthrough이 변경사항은 AWS Rekognition을 활용한 반려동물 이미지 분석 기능을 애플리케이션에 도입합니다. 새로운 Spring 설정 클래스(AwsConfig)가 추가되어 AWS 자격 증명과 리전을 기반으로 Rekognition 클라이언트를 빈으로 제공합니다. VisionController와 VisionService가 새롭게 도입되어, 이미지 업로드, 종 판별, 상세 분석 리포트 생성을 위한 엔드포인트와 비즈니스 로직을 구현합니다. 프론트엔드 템플릿(visionUpload.html)도 대폭 리뉴얼되어, 업로드, 결과 표시, 비동기 처리 및 에러 핸들링을 지원합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Frontend (visionUpload.html)
participant VisionController
participant VisionService
participant AWS Rekognition
participant Gemini API
participant Together Vision API
User->>Frontend (visionUpload.html): 이미지 업로드 및 이름 입력
Frontend->>VisionController: POST /vision/species (파일, 이름)
VisionController->>VisionService: getInterimMessage(파일, 이름)
VisionService->>AWS Rekognition: detectLabels(이미지)
AWS Rekognition-->>VisionService: 라벨 결과
VisionService-->>VisionController: 종 판별 결과
VisionController-->>Frontend: interim 메시지 반환
User->>Frontend: 분석 요청 클릭
Frontend->>VisionController: POST /vision/analyze (파일, 이름)
VisionController->>VisionService: createFinalReport(파일, 이름)
VisionService->>Gemini API: 이미지+프롬프트로 분석 요청
alt Gemini 실패
VisionService->>Together Vision API: 이미지+프롬프트로 분석 요청
Together Vision API-->>VisionService: 분석 결과
else Gemini 성공
Gemini API-->>VisionService: 분석 결과
end
VisionService-->>VisionController: 최종 리포트
VisionController-->>Frontend: 리포트 반환
Possibly related PRs
Poem
✨ Finishing Touches
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:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
src/main/resources/templates/visionUpload.html (1)
73-108: 동일 이미지를 두 번 전송하여 네트워크·서버 부하가 2배 발생
/vision/species,/vision/analyze에 각각FormData전체(이미지 포함)를 전송하고 있습니다.
첫 호출에서 종 추정을 완료한 뒤 같은 파일을 다시 업로드하지 말고,
- 백엔드에서 두 로직을 한 엔드포인트로 합치거나
/vision/species응답에 서버 내 임시 경로/식별자를 반환 → 두 번째 호출에서는 파일 대신 ID만 보내는 방식이 효율적입니다.src/main/java/io/github/petty/vision/controller/VisionController.java (1)
21-33: REST 응답 형식을 명시적으로 관리하세요현재 두 POST 메서드가
String만 반환하여 항상 200 OK로 응답합니다.
실패 시에도 200이 전달되면 프론트엔드에서 오류를 구분하기 어렵습니다.@PostMapping("/species") public ResponseEntity<String> getSpeciesInterim(...) { try { return ResponseEntity.ok(visionService.getInterimMessage(file, petName)); } catch(Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("중간 분석 실패"); } }같이
ResponseEntity로 상태 코드를 명시해 주세요.src/main/java/io/github/petty/vision/service/VisionService.java (1)
100-116: 키워드 매칭 로직 성능 개선 가능현재 20 라벨 × 16 키워드 = 최대 320 비교가 이루어집니다.
파일당 호출 빈도가 높다면label.name().toLowerCase()결과를SPECIES_KEYWORDS의 key Set 포함 여부로 한 번만 확인하도록 개선할 수 있습니다. 다만 심각한 병목은 아니므로 우선순위는 낮습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/main/java/io/github/petty/config/AwsConfig.java(1 hunks)src/main/java/io/github/petty/vision/controller/VisionController.java(1 hunks)src/main/java/io/github/petty/vision/service/VisionService.java(1 hunks)src/main/resources/templates/visionUpload.html(1 hunks)
🔇 Additional comments (1)
src/main/java/io/github/petty/vision/service/VisionService.java (1)
24-27: RestTemplate 빈 정의 여부 확인 필요
RestTemplate를 생성자 주입 받고 있지만, 프로젝트 전역에@Bean RestTemplate정의가 없으면 애플리케이션이 시작되지 않습니다. 존재 여부를 재확인해 주세요.
| @Bean | ||
| public RekognitionClient rekognitionClient() { | ||
| AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey); | ||
| return RekognitionClient.builder() | ||
| .credentialsProvider(StaticCredentialsProvider.create(credentials)) | ||
| .region(Region.of(region)) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
RekognitionClient 자원 해제 누락
RekognitionClient는 AutoCloseable을 구현합니다. 빈이 싱글턴으로 유지되더라도 애플리케이션 종료 시 close()를 호출하지 않으면 채널/스레드 풀 누수가 발생합니다.
@PreDestroy
public void shutdown() {
rekognitionClient().close();
}같은 @Bean(destroyMethod = "close") 혹은 별도 @PreDestroy 메서드를 추가해주세요.
| @Value("${aws.accessKeyId}") | ||
| private String accessKeyId; | ||
|
|
||
| @Value("${aws.secretAccessKey}") | ||
| private String secretAccessKey; | ||
|
|
||
| @Value("${aws.region}") | ||
| private String region; | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
애플리케이션 프로퍼티에 정적 자격 증명 사용은 보안 취약점을 초래할 수 있습니다.
@Value("${aws.accessKeyId}"), @Value("${aws.secretAccessKey}") 방식으로 코드에 접근 키를 주입하면
- git 커밋 실수로 키가 노출될 위험이 높고
- 운영/개발 환경 전환 시 별도 관리 포인트가 늘어납니다.
AWS SDK v2는 DefaultCredentialsProvider(환경 변수, EC2/컨테이너 메타데이터, IAM Role 등)를 자동 탐색하므로 이를 우선 사용하고, 로컬 개발만 별도 프로퍼티 파일을 읽도록 분리하는 편이 안전합니다.
-@Value("${aws.accessKeyId}")
-private String accessKeyId;
-@Value("${aws.secretAccessKey}")
-private String secretAccessKey;
...
- AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey);
- return RekognitionClient.builder()
- .credentialsProvider(StaticCredentialsProvider.create(credentials))
+ return RekognitionClient.builder()
+ .credentialsProvider(DefaultCredentialsProvider.create())
.region(Region.of(region))
.build();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Value("${aws.accessKeyId}") | |
| private String accessKeyId; | |
| @Value("${aws.secretAccessKey}") | |
| private String secretAccessKey; | |
| @Value("${aws.region}") | |
| private String region; | |
| // only keep the region from properties | |
| @Value("${aws.region}") | |
| private String region; | |
| @Bean | |
| public RekognitionClient rekognitionClient() { | |
| return RekognitionClient.builder() | |
| // use the SDK’s default provider chain instead of static creds | |
| .credentialsProvider(DefaultCredentialsProvider.create()) | |
| .region(Region.of(region)) | |
| .build(); | |
| } |
| try { | ||
| const res1 = await fetch('/vision/species', { method: 'POST', body: fd }); | ||
| const text1 = await res1.text(); | ||
| interim.textContent = text1; | ||
| result.style.display = 'block'; | ||
| } catch (e) { | ||
| interim.textContent = 'AWS 분석 중 오류가 발생했습니다…'; | ||
| console.error(e); | ||
| } | ||
|
|
There was a problem hiding this comment.
HTTP 오류 코드 미처리
fetch() 결과를 await res.text()로 바로 읽고 있는데, 4xx/5xx 상태도 텍스트로 처리해 버립니다.
response.ok를 확인하여 사용자에게 정확한 오류 메시지를 전달해 주세요.
-const text1 = await res1.text();
-if (!res1.ok) throw new Error(text1);
+if (!res1.ok) {
+ const err = await res1.text();
+ throw new Error(err || res1.status);
+}
+const text1 = await res1.text();Committable suggestion skipped: line range outside the PR's diff.
| @Autowired | ||
| private VisionService visionService; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
필드 주입 대신 생성자 주입 권장
@Autowired 필드 주입은
- final 불가 → 불변성 약화
- 순환 참조 감지가 늦어짐
- 테스트 시 목 객체 주입이 번거로움
@RequiredArgsConstructor를 사용한 생성자 주입으로 전환하면 개선됩니다.
| private String callTogetherVision(byte[] imageData, String petName) { | ||
| Map<String, Object> payload = createTogetherPayload(imageData, petName); | ||
|
|
||
| HttpHeaders headers = new HttpHeaders(); | ||
| headers.setContentType(MediaType.APPLICATION_JSON); | ||
| headers.setBearerAuth(togetherApiKey); | ||
|
|
||
| HttpEntity<Map<String, Object>> req = new HttpEntity<>(payload, headers); | ||
| ResponseEntity<JsonNode> res = restTemplate.exchange( | ||
| togetherApiUrl, | ||
| HttpMethod.POST, | ||
| req, | ||
| JsonNode.class | ||
| ); | ||
|
|
||
| if (res.getStatusCode() == HttpStatus.OK && res.getBody() != null) { | ||
| return res.getBody() | ||
| .path("choices").get(0) | ||
| .path("message").path("content") | ||
| .asText(); | ||
| } | ||
| throw new RuntimeException("Together API 비정상 응답: " + res.getStatusCode()); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Together API 응답 파싱 시 Null/빈 배열 예외 가능
choices.get(0) 호출 전 배열 크기를 확인하지 않으면 API 변경·오류 시 IndexOutOfBoundsException이 발생합니다.
- if (res.getStatusCode() == HttpStatus.OK && res.getBody() != null) {
- return res.getBody()
- .path("choices").get(0)
- .path("message").path("content")
- .asText();
- }
+ if (res.getStatusCode() == HttpStatus.OK && res.getBody() != null) {
+ JsonNode choices = res.getBody().path("choices");
+ if (choices.isArray() && choices.size() > 0) {
+ return choices.get(0).path("message").path("content").asText();
+ }
+ log.error("Together 응답에 choices가 없습니다: {}", res.getBody());
+ }
throw new RuntimeException("Together API 비정상 응답: " + res.getStatusCode());📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private String callTogetherVision(byte[] imageData, String petName) { | |
| Map<String, Object> payload = createTogetherPayload(imageData, petName); | |
| HttpHeaders headers = new HttpHeaders(); | |
| headers.setContentType(MediaType.APPLICATION_JSON); | |
| headers.setBearerAuth(togetherApiKey); | |
| HttpEntity<Map<String, Object>> req = new HttpEntity<>(payload, headers); | |
| ResponseEntity<JsonNode> res = restTemplate.exchange( | |
| togetherApiUrl, | |
| HttpMethod.POST, | |
| req, | |
| JsonNode.class | |
| ); | |
| if (res.getStatusCode() == HttpStatus.OK && res.getBody() != null) { | |
| return res.getBody() | |
| .path("choices").get(0) | |
| .path("message").path("content") | |
| .asText(); | |
| } | |
| throw new RuntimeException("Together API 비정상 응답: " + res.getStatusCode()); | |
| } | |
| private String callTogetherVision(byte[] imageData, String petName) { | |
| Map<String, Object> payload = createTogetherPayload(imageData, petName); | |
| HttpHeaders headers = new HttpHeaders(); | |
| headers.setContentType(MediaType.APPLICATION_JSON); | |
| headers.setBearerAuth(togetherApiKey); | |
| HttpEntity<Map<String, Object>> req = new HttpEntity<>(payload, headers); | |
| ResponseEntity<JsonNode> res = restTemplate.exchange( | |
| togetherApiUrl, | |
| HttpMethod.POST, | |
| req, | |
| JsonNode.class | |
| ); | |
| if (res.getStatusCode() == HttpStatus.OK && res.getBody() != null) { | |
| JsonNode choices = res.getBody().path("choices"); | |
| if (choices.isArray() && choices.size() > 0) { | |
| return choices.get(0) | |
| .path("message") | |
| .path("content") | |
| .asText(); | |
| } | |
| log.error("Together 응답에 choices가 없습니다: {}", res.getBody()); | |
| } | |
| throw new RuntimeException("Together API 비정상 응답: " + res.getStatusCode()); | |
| } |
| private String callGemini(byte[] imageData, String petName, String species) { | ||
| String prompt = createEnhancedPromptForReport(petName, species); | ||
| String base64Image = Base64.getEncoder().encodeToString(imageData); | ||
|
|
||
| Map<String,Object> payload = Map.of( | ||
| "contents", List.of(Map.of( | ||
| "parts", List.of( | ||
| Map.of("text", prompt), | ||
| Map.of("inline_data", Map.of( | ||
| "mime_type", "image/jpeg", | ||
| "data", base64Image | ||
| )) | ||
| ) | ||
| )) | ||
| ); | ||
|
|
||
| HttpHeaders headers = new HttpHeaders(); | ||
| headers.setContentType(MediaType.APPLICATION_JSON); | ||
| String url = geminiApiUrl + "?key=" + geminiApiKey; | ||
|
|
There was a problem hiding this comment.
Gemini API 호출 Payload에 model 누락
공식 문서에 따르면 contents 외에 model 필드(예: "gemini-pro-vision")가 필요합니다.
누락 시 400 Bad Request가 반환됩니다.
Map<String,Object> payload = Map.of(
- "contents", List.of(Map.of(
+ "model", "gemini-pro-vision",
+ "contents", List.of(Map.of(
"parts", List.of(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private String callGemini(byte[] imageData, String petName, String species) { | |
| String prompt = createEnhancedPromptForReport(petName, species); | |
| String base64Image = Base64.getEncoder().encodeToString(imageData); | |
| Map<String,Object> payload = Map.of( | |
| "contents", List.of(Map.of( | |
| "parts", List.of( | |
| Map.of("text", prompt), | |
| Map.of("inline_data", Map.of( | |
| "mime_type", "image/jpeg", | |
| "data", base64Image | |
| )) | |
| ) | |
| )) | |
| ); | |
| HttpHeaders headers = new HttpHeaders(); | |
| headers.setContentType(MediaType.APPLICATION_JSON); | |
| String url = geminiApiUrl + "?key=" + geminiApiKey; | |
| Map<String,Object> payload = Map.of( | |
| "model", "gemini-pro-vision", | |
| "contents", List.of(Map.of( | |
| "parts", List.of( | |
| Map.of("text", prompt), | |
| Map.of("inline_data", Map.of( | |
| "mime_type", "image/jpeg", | |
| "data", base64Image | |
| )) | |
| ) | |
| )) | |
| ); |
VectorStoreService 관련하여 어떤 이슈가 예상되는지 궁금합니다 ==> 논의 예정 |
✅ 리뷰
🔧 제안 및 아이디어
|
리뷰
제안
|
[VISION] 종 추정 정확도 및 UX 개선
📜 PR 내용 요약
AwsConfig) 안정성 점검 및 명확화visionUpload.html) 리팩토링 및 UX 개선⚒️ 작업 및 변경 내용(상세하게)
VisionService.javaVisionController.javaAwsConfig.javavisionUpload.html📚 기타 참고 사항
controller,service)는 기존과 동일하게 유지VectorStoreService관련 이슈는 별도 확인 필요