-
Notifications
You must be signed in to change notification settings - Fork 6
[VISION] 이미지 유효성 검사·캐싱·프롬프트 최적화 통합 및 리팩토링 #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
b0551c0
feat: ImageValidator 도입 및 VisionController 검증 로직 통합
23MinL 7f53a1d
feat: 메모리 기반 캐시 도입 및 VisionServiceImpl 캐싱 로직 추가
23MinL a4f5f11
refactor: PromptFactory 리팩토링 및 프롬프트 최적화 로직 통합
23MinL 0b734f2
Merge branch 'main' into feat/vision-config
23MinL File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
41 changes: 34 additions & 7 deletions
41
src/main/java/io/github/petty/vision/adapter/in/VisionController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,58 @@ | ||
| package io.github.petty.vision.adapter.in; | ||
|
|
||
| import java.io.IOException; | ||
| import io.github.petty.vision.helper.ImageValidator; | ||
| import io.github.petty.vision.helper.ImageValidator.ValidationResult; | ||
| import io.github.petty.vision.port.in.VisionUseCase; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Controller; | ||
| import org.springframework.web.bind.annotation.*; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
| import org.springframework.web.server.ResponseStatusException; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| @Controller | ||
| @RequestMapping("/vision") | ||
| @RequiredArgsConstructor | ||
| public class VisionController { | ||
| private final VisionUseCase vision; | ||
| private final ImageValidator imageValidator; | ||
|
|
||
| @GetMapping("/upload") | ||
| public String page(){ return "visionUpload"; } | ||
| public String page() { | ||
| return "visionUpload"; | ||
| } | ||
|
|
||
| @PostMapping("/species") | ||
| @ResponseBody | ||
| public String getSpeciesInterim(@RequestParam("file") MultipartFile file, | ||
| @RequestParam("petName") String petName) throws IOException { | ||
| public String getSpeciesInterim( | ||
| @RequestParam("file") MultipartFile file, | ||
| @RequestParam("petName") String petName | ||
| ) throws IOException { | ||
| // 파일 유효성 검사 | ||
| ValidationResult vr = imageValidator.validate(file); | ||
| if (!vr.isValid()) { | ||
| throw new ResponseStatusException(HttpStatus.BAD_REQUEST, vr.getMessage()); | ||
| } | ||
|
|
||
| // 기존 서비스 호출 | ||
| return vision.interim(file.getBytes(), petName); | ||
| } | ||
|
|
||
| @PostMapping("/analyze") | ||
| @ResponseBody | ||
| public String analyze(@RequestParam("file") MultipartFile file, | ||
| @RequestParam("petName") String petName) { | ||
| public String analyze( | ||
| @RequestParam("file") MultipartFile file, | ||
| @RequestParam("petName") String petName | ||
| ) { | ||
| // 파일 유효성 검사 | ||
| ValidationResult vr = imageValidator.validate(file); | ||
| if (!vr.isValid()) { | ||
| throw new ResponseStatusException(HttpStatus.BAD_REQUEST, vr.getMessage()); | ||
| } | ||
|
|
||
| // 기존 서비스 호출 | ||
| return vision.analyze(file, petName); | ||
| } | ||
| } | ||
| } | ||
18 changes: 18 additions & 0 deletions
18
src/main/java/io/github/petty/vision/config/CacheConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package io.github.petty.vision.config; | ||
|
|
||
| import org.springframework.cache.CacheManager; | ||
| import org.springframework.cache.annotation.EnableCaching; | ||
| import org.springframework.cache.concurrent.ConcurrentMapCacheManager; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| @Configuration | ||
| @EnableCaching | ||
| public class CacheConfig { | ||
|
|
||
| @Bean | ||
| public CacheManager cacheManager() { | ||
| // 기본 메모리 캐시: speciesResults, visionResults 두 개의 캐시 영역만 생성 | ||
| return new ConcurrentMapCacheManager("speciesResults", "visionResults"); | ||
| } | ||
| } |
139 changes: 139 additions & 0 deletions
139
src/main/java/io/github/petty/vision/helper/ImageValidator.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| package io.github.petty.vision.helper; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
| import software.amazon.awssdk.core.SdkBytes; | ||
| import software.amazon.awssdk.services.rekognition.RekognitionClient; | ||
| import software.amazon.awssdk.services.rekognition.model.DetectLabelsRequest; | ||
| import software.amazon.awssdk.services.rekognition.model.DetectLabelsResponse; | ||
| import software.amazon.awssdk.services.rekognition.model.Image; | ||
| import software.amazon.awssdk.services.rekognition.model.Label; | ||
|
|
||
| import javax.imageio.ImageIO; | ||
| import java.awt.image.BufferedImage; | ||
| import java.io.ByteArrayInputStream; | ||
| import java.io.IOException; | ||
| import java.util.Arrays; | ||
| import java.util.HashSet; | ||
| import java.util.Set; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class ImageValidator { | ||
|
|
||
| private final RekognitionClient rekognitionClient; | ||
|
|
||
| private static final Set<String> VALID_EXTENSIONS = new HashSet<>( | ||
| Arrays.asList("jpg", "jpeg", "png", "bmp") | ||
| ); | ||
| private static final long MIN_FILE_SIZE = 10 * 1024; // 10KB | ||
| private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB | ||
| private static final int MIN_WIDTH = 200; | ||
| private static final int MIN_HEIGHT = 200; | ||
| private static final float MIN_ANIMAL_CONFIDENCE = 70.0f; | ||
| private static final Set<String> ANIMAL_LABELS = new HashSet<>( | ||
| Arrays.asList("Animal", "Pet", "Dog", "Cat", "Mammal", "Canine", "Feline") | ||
| ); | ||
|
|
||
| public ValidationResult validate(MultipartFile file) { | ||
| if (file == null || file.isEmpty()) { | ||
| return ValidationResult.invalid("이미지 파일이 없습니다."); | ||
| } | ||
| if (file.getSize() < MIN_FILE_SIZE) { | ||
| return ValidationResult.invalid("이미지 파일이 너무 작습니다. 최소 10KB 이상이어야 합니다."); | ||
| } | ||
| if (file.getSize() > MAX_FILE_SIZE) { | ||
| return ValidationResult.invalid("이미지 파일이 너무 큽니다. 최대 5MB 이하여야 합니다."); | ||
| } | ||
|
|
||
| String originalFilename = file.getOriginalFilename(); | ||
| if (originalFilename == null || !originalFilename.contains(".")) { | ||
| return ValidationResult.invalid("파일 형식을 확인할 수 없습니다."); | ||
| } | ||
| String extension = originalFilename.substring(originalFilename.lastIndexOf('.') + 1).toLowerCase(); | ||
| if (!VALID_EXTENSIONS.contains(extension)) { | ||
| return ValidationResult.invalid("지원하지 않는 이미지 형식입니다. JPG, PNG, BMP 형식만 지원합니다."); | ||
| } | ||
|
|
||
| try { | ||
| byte[] bytes = file.getBytes(); | ||
| if (!hasValidSignature(bytes, extension)) { | ||
| return ValidationResult.invalid("파일 시그니처가 유효하지 않습니다."); | ||
| } | ||
| BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes)); | ||
| if (image == null) { | ||
| return ValidationResult.invalid("유효한 이미지 파일이 아닙니다."); | ||
| } | ||
| if (image.getWidth() < MIN_WIDTH || image.getHeight() < MIN_HEIGHT) { | ||
| return ValidationResult.invalid("이미지 해상도가 너무 낮습니다. 최소 200×200 이상이어야 합니다."); | ||
| } | ||
| return validateAnimalContent(bytes); | ||
| } catch (IOException e) { | ||
| log.error("이미지 처리 중 오류 발생", e); | ||
| return ValidationResult.invalid("이미지 파일을 처리하는 중 오류가 발생했습니다."); | ||
| } | ||
| } | ||
|
|
||
| private ValidationResult validateAnimalContent(byte[] bytes) { | ||
| try { | ||
| DetectLabelsRequest request = DetectLabelsRequest.builder() | ||
| .image(Image.builder().bytes(SdkBytes.fromByteArray(bytes)).build()) | ||
| .maxLabels(10) | ||
| .minConfidence(50.0f) | ||
| .build(); | ||
| DetectLabelsResponse response = rekognitionClient.detectLabels(request); | ||
|
|
||
| for (Label label : response.labels()) { | ||
| if (ANIMAL_LABELS.contains(label.name()) && label.confidence() >= MIN_ANIMAL_CONFIDENCE) { | ||
| log.info("동물 감지됨: {}, 신뢰도: {}", label.name(), label.confidence()); | ||
| return ValidationResult.valid(); | ||
| } | ||
| } | ||
| return ValidationResult.invalid("반려동물이 감지되지 않았습니다."); | ||
| } catch (Exception e) { | ||
| log.error("Rekognition 오류", e); | ||
| return ValidationResult.invalid("이미지 분석 중 오류가 발생했습니다."); | ||
| } | ||
| } | ||
|
|
||
| // Magic Number Signatures | ||
| private static final byte[] JPG_SIG = new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF}; | ||
| private static final byte[] PNG_SIG = new byte[]{(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; | ||
| private static final byte[] BMP_SIG = new byte[]{0x42, 0x4D}; | ||
|
|
||
| private boolean hasValidSignature(byte[] data, String ext) { | ||
| if (data.length < 8) return false; | ||
| return switch (ext) { | ||
| case "jpg", "jpeg" -> startsWith(data, JPG_SIG); | ||
| case "png" -> startsWith(data, PNG_SIG); | ||
| case "bmp" -> startsWith(data, BMP_SIG); | ||
| default -> false; | ||
| }; | ||
| } | ||
|
|
||
| private boolean startsWith(byte[] data, byte[] sig) { | ||
| for (int i = 0; i < sig.length; i++) { | ||
| if (data[i] != sig[i]) return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| public static class ValidationResult { | ||
| private final boolean valid; | ||
| private final String message; | ||
|
|
||
| public static ValidationResult valid() { | ||
| return new ValidationResult(true, "유효한 이미지입니다."); | ||
| } | ||
|
|
||
| public static ValidationResult invalid(String message) { | ||
| return new ValidationResult(false, message); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
코드 중복이 있습니다.
이미지 유효성 검사 로직이 두 엔드포인트에서 동일하게 반복됩니다. 이를 추출하여 재사용 가능한 private 메소드로 만들면 코드 중복을 줄이고 유지보수성을 향상시킬 수 있습니다.
📝 Committable suggestion
🤖 Prompt for AI Agents