[VISION] 이미지 유효성 검사·캐싱·프롬프트 최적화 통합 및 리팩토링#30
Conversation
Summary by CodeRabbit
Walkthrough이 변경사항은 이미지 업로드 시 파일의 유효성을 검사하는 ImageValidator를 도입하고, VisionController의 주요 엔드포인트에 이를 적용하여 유효하지 않은 이미지를 사전에 차단합니다. 또한, VisionServiceImpl에 캐시 기능을 추가하고, 캐시 키 생성 로직을 포함했습니다. PromptFactory와 관련된 코드의 문서화 및 가독성도 개선되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant VisionController
participant ImageValidator
participant VisionServiceImpl
participant RekognitionClient
User->>VisionController: POST /species or /analyze (MultipartFile)
VisionController->>ImageValidator: validate(file)
ImageValidator->>RekognitionClient: detectLabels(image bytes)
RekognitionClient-->>ImageValidator: 라벨 결과 반환
ImageValidator-->>VisionController: ValidationResult(valid/invalid)
alt 유효성 통과
VisionController->>VisionServiceImpl: interim/analyze 호출
VisionServiceImpl->>VisionServiceImpl: 캐시 키 생성 및 @Cacheable 적용
VisionServiceImpl-->>VisionController: 분석 결과 반환
VisionController-->>User: 결과 반환
else 유효성 실패
VisionController-->>User: 400 Bad Request + 메시지
end
Possibly related PRs
Poem
Note ⚡️ AI Code Reviews for VS Code, Cursor, WindsurfCodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback. Note ⚡️ Faster reviews with cachingCodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 30th. To opt out, configure ✨ 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:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. 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 (
|
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
src/main/java/io/github/petty/vision/service/VisionServiceImpl.java (2)
60-71:⚠️ Fallback 최종 메시지 작성 시 사용자 혼란 가능성
interim + "\n\n최종 분석 실패"는 사용자가 “중간 분석은 성공”으로 오해할 수 있습니다.
서비스 UX 상 “임시 결과이므로 신뢰도 낮음” 이나 “다시 시도해 달라” 같은 안내를 추가하면 좋겠습니다.
90-101: 📈 MessageDigest 인스턴스 재사용 고려
MessageDigest.getInstance("SHA-256")호출은 비용이 크지 않지만 빈번 호출 시 GC 부하가 생길 수 있습니다.
ThreadLocal<MessageDigest>로 캐싱하거나 Apache Commons Codec 의DigestUtils.sha256Hex()를 사용하면 코드 간결성과 성능이 모두 개선됩니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/main/java/io/github/petty/vision/adapter/in/VisionController.java(1 hunks)src/main/java/io/github/petty/vision/config/CacheConfig.java(1 hunks)src/main/java/io/github/petty/vision/helper/ImageValidator.java(1 hunks)src/main/java/io/github/petty/vision/helper/PromptFactory.java(1 hunks)src/main/java/io/github/petty/vision/service/VisionServiceImpl.java(1 hunks)
🔇 Additional comments (15)
src/main/java/io/github/petty/vision/config/CacheConfig.java (1)
9-18: 캐시 설정이 명확하고 간결합니다.Spring의 캐시 기능을 활성화하고 필요한 캐시 영역을 정의한 구현이 잘 되어 있습니다. 단일 인스턴스 환경에 적합한
ConcurrentMapCacheManager를 사용한 점이 좋습니다.다만, 향후 고려사항으로 다음을 검토해 보세요:
- 캐시 항목의 만료 시간(TTL) 설정
- 최대 캐시 항목 수 제한
- 다중 인스턴스 환경으로 확장 시 분산 캐시 솔루션 검토
src/main/java/io/github/petty/vision/adapter/in/VisionController.java (2)
20-20: 적절한 의존성 주입이 이루어졌습니다.
ImageValidator를 컨트롤러에 주입하여 사용자 요청을 검증하는 구조가 잘 설계되었습니다.
33-38: 이미지 유효성 검사 및 예외 처리가 잘 구현되었습니다.API 호출 전에 이미지 유효성을 검사하여 불필요한 처리를 방지하는 접근 방식이 효율적입니다.
ResponseStatusException을 사용하여 클라이언트에게 적절한 HTTP 상태 코드와 메시지를 반환하는 방식도 좋습니다.src/main/java/io/github/petty/vision/helper/ImageValidator.java (6)
23-26: 클래스 구조와 의존성 주입이 잘 설계되었습니다.Lombok 어노테이션을 활용하여 보일러플레이트 코드를 줄이고, 적절한 로깅 설정이 되어 있습니다.
30-40: 상수 정의가 명확하게 되어 있습니다.유효성 검사에 필요한 모든 상수가 클래스 상단에 명확하게 정의되어 있어 코드 가독성과 유지보수성이 좋습니다.
42-79: 포괄적인 이미지 유효성 검사가 잘 구현되었습니다.파일 존재 여부, 크기, 확장자, 시그니처, 해상도 등 다양한 측면에서 이미지 유효성을 검사하는 로직이 잘 구현되어 있습니다. 오류 처리와 사용자 친화적인 메시지 반환도 좋습니다.
다만, 예외 처리 시 더 구체적인 예외 유형을 로깅하면 디버깅에 도움이 될 수 있습니다:
- log.error("이미지 처리 중 오류 발생", e); + log.error("이미지 처리 중 오류 발생: {}", e.getMessage(), e);
81-101: AWS Rekognition을 활용한 동물 콘텐츠 검증이 인상적입니다.AWS Rekognition을 활용하여 이미지에 반려동물이 포함되어 있는지 확인하는 접근 방식이 매우 효과적입니다. 신뢰도 임계값을 설정하고 다양한 동물 관련 레이블을 검사하는 방식도 좋습니다.
추가 고려사항:
- API 호출 비용 최적화를 위해 로컬 검증이 모두 통과한 후에만 Rekognition을 호출하는 점은 좋습니다.
- 단, Rekognition API 호출이 실패하거나 오래 걸릴 경우 사용자 경험에 영향을 줄 수 있으므로, 타임아웃 설정이나 비동기 처리를 고려해볼 수 있습니다.
103-123: 파일 시그니처 검사 로직이 효율적입니다.Magic Number를 이용한 파일 시그니처 검사 방식이 단순하면서도 효과적입니다. 이를 통해 파일 확장자 스푸핑을 방지할 수 있습니다.
125-138: ValidationResult 클래스 설계가 깔끔합니다.불변(immutable) 내부 클래스와 팩토리 메소드를 사용하여 유효성 검사 결과를 표현하는 방식이 깔끔합니다. 이는 코드의 가독성과 유지보수성을 높여줍니다.
src/main/java/io/github/petty/vision/helper/PromptFactory.java (5)
11-13: 클래스 수준의 JavaDoc이 추가되었습니다.클래스의 목적과 책임을 명확히 설명하는 JavaDoc이 추가되어 코드 이해도가 향상되었습니다.
20-31: 메서드 JavaDoc이 추가되고 코드 가독성이 개선되었습니다.
interimMsg메서드에 명확한 JavaDoc이 추가되었고, 삼항 연산자에서 if-else 구문으로 변경되어 코드 가독성이 향상되었습니다.
34-44: Gemini API 요청 생성 메서드가 개선되었습니다.JavaDoc이 추가되고 프롬프트 텍스트가 변수로 추출되어 코드 가독성이 향상되었습니다.
47-62: Together API 요청 생성 메서드가 개선되었습니다.JavaDoc이 추가되고 프롬프트 텍스트가 변수로 추출되어 코드 가독성이 향상되었습니다. 빈 문자열을 species 매개변수로 전달하여 일관된 프롬프트 형식을 유지하는 점도 좋습니다.
65-86: detailedPrompt 메서드가 크게 개선되었습니다.
- JavaDoc이 추가되었습니다.
- 텍스트 블록을 사용하여 여러 줄의 프롬프트를 가독성 있게 표현했습니다.
- null 또는 빈 species 값을 처리하는 로직이 추가되었습니다.
이러한 개선 사항들은 코드의 가독성과 유지보수성을 크게 향상시켰습니다.
src/main/java/io/github/petty/vision/service/VisionServiceImpl.java (1)
47-52: 🛑 IOException → IllegalStateException 매핑의 의도 확인 요청
file.getBytes()실패 시IllegalStateException으로 래핑하면 호출 측에서 “호출 순서/상태 오류”로 오해할 수 있습니다.
UncheckedIOException또는 커스텀ImageReadException으로 의미를 명확히 하는 편이 좋습니다.
또한 메시지"이미지 읽기 실패"만으로는 원인 파악이 어려우니 파일명 또는 사이즈 등의 메타 정보를 포함해 보세요.
| // 파일 유효성 검사 | ||
| ValidationResult vr = imageValidator.validate(file); | ||
| if (!vr.isValid()) { | ||
| throw new ResponseStatusException(HttpStatus.BAD_REQUEST, vr.getMessage()); | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
코드 중복이 있습니다.
이미지 유효성 검사 로직이 두 엔드포인트에서 동일하게 반복됩니다. 이를 추출하여 재사용 가능한 private 메소드로 만들면 코드 중복을 줄이고 유지보수성을 향상시킬 수 있습니다.
- // 파일 유효성 검사
- ValidationResult vr = imageValidator.validate(file);
- if (!vr.isValid()) {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, vr.getMessage());
- }
+ validateImage(file);
...
+ /**
+ * 이미지 파일 유효성 검사
+ * @param file 검사할 이미지 파일
+ * @throws ResponseStatusException 유효하지 않은 이미지일 경우 발생
+ */
+ private void validateImage(MultipartFile file) {
+ ValidationResult vr = imageValidator.validate(file);
+ if (!vr.isValid()) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, vr.getMessage());
+ }
+ }📝 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.
| // 파일 유효성 검사 | |
| ValidationResult vr = imageValidator.validate(file); | |
| if (!vr.isValid()) { | |
| throw new ResponseStatusException(HttpStatus.BAD_REQUEST, vr.getMessage()); | |
| } | |
| // 파일 유효성 검사 | |
| - ValidationResult vr = imageValidator.validate(file); | |
| - if (!vr.isValid()) { | |
| - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, vr.getMessage()); | |
| - } | |
| + validateImage(file); | |
| ... | |
| + /** | |
| + * 이미지 파일 유효성 검사 | |
| + * @param file 검사할 이미지 파일 | |
| + * @throws ResponseStatusException 유효하지 않은 이미지일 경우 발생 | |
| + */ | |
| + private void validateImage(MultipartFile file) { | |
| + ValidationResult vr = imageValidator.validate(file); | |
| + if (!vr.isValid()) { | |
| + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, vr.getMessage()); | |
| + } | |
| + } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/adapter/in/VisionController.java around
lines 49 to 54, the image validation logic is duplicated across two endpoints.
Extract this validation code into a private method that accepts the file as a
parameter, performs the validation, and throws the ResponseStatusException if
invalid. Replace the duplicated code in both endpoints with calls to this new
private method to reduce redundancy and improve maintainability.
| * 상세 분석 캐시: MultipartFile과 petName 조합으로 키 생성 | ||
| */ | ||
| @Override | ||
| public String analyze(MultipartFile file, String pet) { | ||
| @Cacheable(value = "visionResults", | ||
| key = "#petName + '_' + T(io.github.petty.vision.service.VisionServiceImpl).generateCacheKey(#file)") | ||
| public String analyze(MultipartFile file, String petName) { | ||
| byte[] img; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
📦 MultipartFile을 두 번 읽는 중복 IO
SpEL에서 generateCacheKey(#file) 호출 시 file.getBytes()가 1회,
아래 img = file.getBytes() 로 2회 호출되어 대용량(최대 5 MB) 파일을 불필요하게 두 번 메모리에 올립니다.
① 캐시 키 생성을 analyze() 내부로 옮겨 지역 변수 img를 재사용하거나
② generateCacheKey(MultipartFile) 내부에서 file.getBytes() 를 호출하지 않고 file.getInputStream() 으로 스트리밍 처리 후 mark/reset 을 활용하는 방법을 고려해 주세요.
이로써 힙 사용량과 GC 부담을 줄일 수 있습니다.
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/service/VisionServiceImpl.java around
lines 40 to 46, the MultipartFile's getBytes() method is called twice: once in
the SpEL cache key expression and once inside the analyze() method, causing
redundant IO and increased memory usage. To fix this, move the cache key
generation logic inside the analyze() method after reading the file bytes once
into a local variable, then reuse this variable both for generating the cache
key and for further processing. This avoids reading the file twice and reduces
memory overhead.
| /** | ||
| * 중간 결과 캐시: 이미지 바이트와 petName 조합으로 키 생성 | ||
| */ | ||
| @Override | ||
| @Cacheable(value = "speciesResults", | ||
| key = "#petName + '_' + T(io.github.petty.vision.service.VisionServiceImpl).generateCacheKey(#image)") | ||
| public String interim(byte[] image, String petName) { | ||
| String species = detector.detect(image); | ||
| return prompt.interimMsg(petName, species); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
🔑 캐시 키에 petName을 직접 포함할 때 충돌·보안 가능성 고려 필요
petName이 그대로 키 문자열로 사용되면
- 사용자 입력에 따라 충돌(동명이인)·오염(공백‧특수문자) 가능성이 있고,
- 키 값이 그대로 로그/메트릭에 노출될 때 PII 유출 위험이 있습니다.
petName을 URLEncoder.encode() 하거나 별도 해시(예: SHA-1 8byte)로 축약해 넣는 방식을 권장합니다.
- key = "#petName + '_' + T(...).generateCacheKey(#image)"
+ key = "T(...).hashPetName(#petName) + '_' + T(...).generateCacheKey(#image)"추가 util 메서드만 있으면 SpEL에서도 쉽게 호출 가능합니다.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/service/VisionServiceImpl.java around
lines 28 to 37, the cache key directly includes the petName string, which risks
collisions and potential PII exposure. To fix this, encode petName using
URLEncoder.encode() or hash it (e.g., SHA-1 truncated to 8 bytes) before
including it in the cache key. Implement a utility method for this encoding or
hashing that can be called from the SpEL expression in the @Cacheable annotation
to ensure safe and unique cache keys.
| /** | ||
| * SHA-256 해시(Base64 URL-safe)로 캐시 키 생성 | ||
| * @param file MultipartFile | ||
| */ | ||
| @SuppressWarnings("unused") | ||
| public static String generateCacheKey(MultipartFile file) { | ||
| try { | ||
| byte[] data = file.getBytes(); | ||
| MessageDigest md = MessageDigest.getInstance("SHA-256"); | ||
| byte[] hash = md.digest(data); | ||
| return Base64.getUrlEncoder().encodeToString(hash); | ||
| } catch (IOException | NoSuchAlgorithmException e) { | ||
| return file.getOriginalFilename() + "_" + file.getSize(); | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
🔄 generateCacheKey(MultipartFile) 의 예외 분기 개선
-
SHA-256은 표준 JRE 에 항상 존재하므로NoSuchAlgorithmException은 사실상 발생하지 않습니다.
⇒ catch 블록에서 해당 예외를 별도 처리할 필요가 없습니다. -
Fallback 키:
file.getOriginalFilename()은null가능성이 있어 NPE 위험- 동일한 파일명이 달라진 내용(사이즈 동일)으로 업로드되면 충돌 발생
아래처럼 수정하면 안전성이 높아집니다.
-return file.getOriginalFilename() + "_" + file.getSize();
+return "fn:" + Objects.toString(file.getOriginalFilename(), "noname")
+ + "_ts:" + System.nanoTime(); // 충돌 최소화📝 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.
| /** | |
| * SHA-256 해시(Base64 URL-safe)로 캐시 키 생성 | |
| * @param file MultipartFile | |
| */ | |
| @SuppressWarnings("unused") | |
| public static String generateCacheKey(MultipartFile file) { | |
| try { | |
| byte[] data = file.getBytes(); | |
| MessageDigest md = MessageDigest.getInstance("SHA-256"); | |
| byte[] hash = md.digest(data); | |
| return Base64.getUrlEncoder().encodeToString(hash); | |
| } catch (IOException | NoSuchAlgorithmException e) { | |
| return file.getOriginalFilename() + "_" + file.getSize(); | |
| } | |
| } | |
| /** | |
| * SHA-256 해시(Base64 URL-safe)로 캐시 키 생성 | |
| * @param file MultipartFile | |
| */ | |
| @SuppressWarnings("unused") | |
| public static String generateCacheKey(MultipartFile file) { | |
| try { | |
| byte[] data = file.getBytes(); | |
| MessageDigest md = MessageDigest.getInstance("SHA-256"); | |
| byte[] hash = md.digest(data); | |
| return Base64.getUrlEncoder().encodeToString(hash); | |
| } catch (IOException | NoSuchAlgorithmException e) { | |
| return "fn:" + Objects.toString(file.getOriginalFilename(), "noname") | |
| + "_ts:" + System.nanoTime(); // 충돌 최소화 | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/service/VisionServiceImpl.java around
lines 74 to 88, remove NoSuchAlgorithmException from the catch clause since
SHA-256 is always available in standard JRE. Modify the fallback key generation
to avoid NullPointerException by safely handling possible null original
filenames and reduce collision risk by incorporating a more unique identifier
such as a timestamp or a hash of the file content size combined with the
filename. This will improve the safety and uniqueness of the cache key.
핵심적인 개선이 많이 이루어진 것을 확인했습니다. 작업하시느라 고생 많으셨습니다. |
📜 PR 내용 요약
⚒️ 작업 및 변경 내용(상세하게)
✅ 1. PromptFactory 리팩토링 및 기능 확장
interimMsg,toGeminiReq,toTogetherReq등의 메서드를 구조적으로 통합detailedPrompt()메서드를 추가하여 LLM 분석 요청의 일관된 포맷 제공✅ 2. 이미지 유효성 검사 기능 도입 (
ImageValidator)업로드된 이미지에 대해 다음 항목 검사:
유효하지 않은 파일은 Controller 단에서 사전에 차단하여 불필요한 API 호출 방지
✅ 3. Spring 캐시 기반 분석 결과 저장
CacheConfig에서ConcurrentMapCacheManager로 단순 메모리 캐시 설정VisionServiceImpl에서@Cacheable적용 (분석 결과 재활용)speciesResults: interim 결과 캐시visionResults: analyze 결과 캐시이미지 내용 기반 해시 키(SHA-256) 생성으로 캐시 키 충돌 최소화
📚 기타 참고 사항
PromptFactory는 프로젝트 내에서 그대로 주입받아 사용되고 있으며, 기존 사용자 코드와 충돌 없이 통합됩니다.