[VISION] Vision 모듈 전면 리팩터링 + .gitignore 보강#21
Conversation
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (20)
src/main/java/io/github/petty/vision/port/out/GeminiPort.java (1)
1-8: 인터페이스 설계가 명확하고 깔끔합니다Clean Architecture의 Output Port 패턴을 잘 따르고 있습니다. 단일 책임 원칙(SRP)에 맞게 하나의 메서드만 정의하여 Gemini API와의 통신에 집중하고 있습니다.
다만, 메서드에 대한 Javadoc을 추가하면 더 좋을 것 같습니다. 특히 예외 처리 방식, 반환값의 의미 등을 명확히 하면 인터페이스 사용자에게 도움이 될 것입니다.
package io.github.petty.vision.port.out; import io.github.petty.vision.dto.gemini.GeminiRequest; import io.github.petty.vision.dto.gemini.GeminiResponse; +/** + * Gemini AI 모델과의 통신을 위한 포트 인터페이스 + */ public interface GeminiPort { + /** + * Gemini AI 모델에 요청을 보내고 응답을 받아옵니다. + * + * @param req Gemini AI 모델에 전송할 요청 + * @return Gemini AI 모델로부터 받은 응답 + * @throws io.github.petty.vision.exception.ExternalApiException API 호출 중 오류 발생 시 + */ GeminiResponse generate(GeminiRequest req); }src/main/java/io/github/petty/vision/dto/gemini/GeminiRequest.java (1)
1-6: DTO를 Record로 구현한 좋은 접근입니다Java Record를 사용하여 불변 데이터 객체로 만든 점이 좋습니다. 이는 DTO 패턴에 적합하며 스레드 안전성을 보장합니다.
그러나
contents필드는 매우 일반적인 구조(List<Map<String, Object>>)를 가지고 있어, 컴파일 시점에 타입 안전성이 보장되지 않습니다. Javadoc을 추가하여 기대되는 데이터 구조와 형식을 명확히 하면 좋을 것 같습니다.package io.github.petty.vision.dto.gemini; import java.util.List; import java.util.Map; +/** + * Gemini API 요청을 위한 DTO + * + * contents 필드는 다음과 같은 구조를 가져야 합니다: + * [ + * { + * "role": "user" | "system", + * "parts": [ + * { + * "text": "텍스트 내용" | "mime_type": "image/jpeg", "data": "base64로 인코딩된 이미지" + * } + * ] + * } + * ] + */ public record GeminiRequest(List<Map<String,Object>> contents) {}src/main/java/io/github/petty/vision/port/out/RekognitionPort.java (1)
1-9: AWS Rekognition과의 통신을 위한 포트가 잘 정의되었습니다Clean Architecture의 Output Port 패턴을 따르고 있으며, 단일 책임 원칙(SRP)에 맞게 설계되어 있습니다.
다만 몇 가지 개선 사항을 제안드립니다:
AWS SDK 타입(Label)에 직접 의존하고 있어 AWS SDK와의 결합도가 높습니다. 자체 DTO를 정의하여 외부 라이브러리와의 결합도를 낮추는 것이 좋을 것 같습니다.
Javadoc을 추가하여 메서드의 동작, 예외 처리 방식 등을 문서화하면 인터페이스 사용자에게 도움이 될 것입니다.
package io.github.petty.vision.port.out; import software.amazon.awssdk.services.rekognition.model.Label; import java.util.List; +/** + * AWS Rekognition 서비스와의 통신을 위한 포트 인터페이스 + */ public interface RekognitionPort { + /** + * 이미지에서 레이블(객체, 장면, 컨셉 등)을 감지합니다. + * + * @param image 분석할 이미지 바이트 배열 + * @return 감지된 레이블 목록 + * @throws io.github.petty.vision.exception.ExternalApiException API 호출 중 오류 발생 시 + */ List<Label> detectLabels(byte[] image); }src/main/java/io/github/petty/vision/port/in/VisionUseCase.java (1)
1-9: 인터페이스가 명확하게 정의되었습니다Clean Architecture의 Input Port 패턴을 잘 따르고 있으며, 사용 사례(Use Case)를 명확히 정의하고 있습니다.
개선을 위한 몇 가지 제안사항이 있습니다:
반환 타입이
String으로 되어 있어 구조화된 데이터를 반환하기 어렵습니다. DTO를 사용하여 구조화된 응답을 제공하는 것이 더 좋을 것 같습니다.
analyze와interim메서드의 차이점과 목적이 명확하지 않습니다. Javadoc을 추가하여 각 메서드의 역할을 설명하면 좋을 것 같습니다.Spring의
MultipartFile에 직접 의존하고 있어 프레임워크와의 결합도가 높습니다. 자체 DTO를 사용하는 것이 결합도를 낮출 수 있습니다.package io.github.petty.vision.port.in; import org.springframework.web.multipart.MultipartFile; +/** + * 비전 서비스의 사용 사례(Use Case)를 정의하는 인터페이스 + */ public interface VisionUseCase { + /** + * 업로드된 반려동물 이미지를 분석하여 상세 보고서를 생성합니다. + * + * @param file 분석할 반려동물 이미지 파일 + * @param petName 반려동물 이름 + * @return 분석된 상세 보고서(JSON 형식) + * @throws io.github.petty.vision.exception.ImageProcessingException 이미지 처리 중 오류 발생 시 + */ String analyze(MultipartFile file, String petName); + + /** + * 이미지에서 반려동물의 종을 감지하여 중간 결과를 반환합니다. + * + * @param image 분석할 이미지 바이트 배열 + * @param petName 반려동물 이름 + * @return 종 감지 결과(JSON 형식) + * @throws io.github.petty.vision.exception.SpeciesDetectionException 종 감지 중 오류 발생 시 + */ String interim(byte[] image, String petName); }src/main/java/io/github/petty/vision/config/VisionProperties.java (1)
8-24: 프로퍼티 클래스의 구조가 잘 설계되었습니다.Spring의 @ConfigurationProperties를 사용하여 외부 설정을 효과적으로 바인딩하고 있습니다. 중첩 클래스를 사용하여 관련 속성을 그룹화한 것이 좋은 접근 방식입니다.
몇 가지 개선 사항을 제안합니다:
- 필수 속성에 대한 유효성 검증 어노테이션(@NotNull, @notblank 등)을 추가하여 런타임 오류를 방지하세요.
- 각 필드와 클래스에 JavaDoc 주석을 추가하여 속성의 목적과 사용법을 명확히 하세요.
- 민감한 정보(API 키)를 마스킹하는 toString() 메서드 구현을 고려하세요.
@Getter @Setter @Component @ConfigurationProperties(prefix = "vision") +@ToString public class VisionProperties { private Aws aws = new Aws(); private Gemini gemini = new Gemini(); private Together together = new Together(); + @NotBlank(message = "geminiModel must not be empty") private String geminiModel; + @NotBlank(message = "togetherModel must not be empty") private String togetherModel; + @NotBlank(message = "llamaModel must not be empty") private String llamaModel; - @Getter @Setter public static class Aws { private String region; } - @Getter @Setter public static class Gemini { private String url; private String key; } - @Getter @Setter public static class Together { private String url; private String key; } + @Getter @Setter public static class Aws { + @NotBlank(message = "aws.region must not be empty") + private String region; + } + + @Getter @Setter public static class Gemini { + @NotBlank(message = "gemini.url must not be empty") + private String url; + @NotBlank(message = "gemini.key must not be empty") + private String key; + + @Override + public String toString() { + return "Gemini(url=" + url + ", key=***)"; + } + } + + @Getter @Setter public static class Together { + @NotBlank(message = "together.url must not be empty") + private String url; + @NotBlank(message = "together.key must not be empty") + private String key; + + @Override + public String toString() { + return "Together(url=" + url + ", key=***)"; + } + } }.gitignore (1)
39-46: 보안 관련 파일 패턴을 추가하면 좋을 것 같습니다..gitignore 파일 구조가 잘 정리되어 있습니다. "dev"와 "Vision" 섹션을 분리한 것이 좋습니다.
보안 강화를 위해 다음과 같은 민감한 파일 패턴을 추가하는 것을 고려해보세요:
### dev ### application-dev.yml ### secret ### application-secret.yml +credentials.json +*.pem +*.key +*.p12 +*.jks +secrets/** ### Vision ### application-vision-only.yml이렇게 하면 민감한 인증 정보가 실수로 저장소에 커밋되는 것을 더 효과적으로 방지할 수 있습니다.
src/main/java/io/github/petty/vision/port/out/TogetherPort.java (1)
6-8: 인터페이스에 문서화를 추가하면 좋을 것 같습니다.Port/Adapter 패턴에 맞게 인터페이스가 간결하게 정의되어 있습니다. 그러나 문서화가 부족합니다.
JavaDoc 주석을 추가하여 인터페이스와 메서드의 목적을 명확히 하고, 가능한 예외 타입을 선언하면 좋을 것 같습니다:
+/** + * Together AI 서비스와의 통신을 위한 출력 포트 인터페이스입니다. + * 이 인터페이스는 Together AI 모델을 사용하여 텍스트와 이미지 기반 프롬프트로부터 + * 응답을 생성하는 기능을 제공합니다. + */ public interface TogetherPort { + /** + * Together AI 모델을 사용하여 요청에 대한 응답을 생성합니다. + * + * @param req Together AI 요청 객체 + * @return Together AI 모델이 생성한 응답 + * @throws RuntimeException API 호출 중 오류가 발생한 경우 + */ - TogetherResponse generate(TogetherRequest req); + TogetherResponse generate(TogetherRequest req) throws RuntimeException; }이렇게 하면 인터페이스 사용자에게 더 명확한 정보를 제공할 수 있습니다.
src/main/java/io/github/petty/vision/dto/together/TogetherRequest.java (1)
6-13: Javadoc 예제를 더 상세하게 보완하면 좋겠습니다.현재 Javadoc에는
messages필드에 대해[ ... ]형태로 예시가 생략되어 있습니다. API 사용자가 쉽게 이해할 수 있도록 실제 메시지 구조의 예시를 포함하는 것이 좋겠습니다./** * Together Chat Completion 요청용 DTO * * 예시 * { * "model": "meta-llama/Llama-3.2‑11B‑Vision‑Instruct‑Turbo‑Free", - * "messages": [ ... ] + * "messages": [ + * { + * "role": "user", + * "content": [ + * {"type": "text", "text": "이 사진 속 동물을 분석해주세요"}, + * {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}} + * ] + * } + * ] * } */src/main/java/io/github/petty/vision/dto/together/TogetherResponse.java (2)
18-30: 성능 최적화를 위한 ObjectMapper 정적 인스턴스화 제안
raw()메서드에서 매번 새로운 ObjectMapper 인스턴스를 생성하고 있습니다. 이는 메서드 호출마다 인스턴스 생성 비용이 발생하여 성능에 영향을 줄 수 있습니다. 정적 상수로 선언하여 재사용하는 것이 좋습니다.+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @JsonIgnore public JsonNode raw() { // ← TogetherClientImpl 에서 사용 - return new ObjectMapper().valueToTree(this); + return OBJECT_MAPPER.valueToTree(this); }또한 인라인 주석은 코드-문서 간 불일치가 발생할 가능성이 있으므로 제거하는 것이 좋습니다.
14-30: 클래스 수준의 Javadoc 추가 권장클래스 목적과 사용법을 설명하는 Javadoc이 누락되어 있습니다. TogetherRequest 클래스에는 Javadoc이 있지만, 이 클래스에는 없어 일관성이 부족합니다. 응답 구조와 사용 방법에 대한 설명을 추가하는 것이 좋겠습니다.
+/** + * Together Chat Completion API 응답을 위한 DTO + * + * 예시: + * { + * "choices": [ + * { + * "message": { + * "content": "분석 결과 텍스트" + * } + * } + * ] + * } + */ @Getter @NoArgsConstructor @AllArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class TogetherResponse {src/main/java/io/github/petty/vision/helper/SpeciesDetector.java (2)
16-22: 매핑 테이블 개선 필요현재 매핑 테이블에는
/* … */주석이 있어 완성되지 않은 것으로 보입니다. 두 가지 개선사항을 제안합니다:
- 모든 지원 가능한 동물 종류를 포함하거나
- 주석을 더 명확하게 작성하여 일부만 예시로 포함된 것임을 명시
또한 정적 맵의 크기가 큰 경우
Map.of()보다 정적 초기화 블록을 사용하는 것이 가독성 측면에서 더 좋을 수 있습니다.-private static final Map<String,String> MAP = Map.of( - "cat","고양이","kitten","고양이", - "dog","개","puppy","개", - "rabbit","토끼", "hamster","햄스터" - /* … */ -); +/** 영어 동물 이름을 한글로 변환하는 매핑 테이블 (주요 반려동물) */ +private static final Map<String,String> MAP; + +static { + Map<String, String> map = new HashMap<>(); + map.put("cat", "고양이"); + map.put("kitten", "고양이"); + map.put("dog", "개"); + map.put("puppy", "개"); + map.put("rabbit", "토끼"); + map.put("hamster", "햄스터"); + // 필요시 더 많은 동물 종류 추가 + MAP = Collections.unmodifiableMap(map); +}
37-37: 복잡한 반환 로직 개선 필요37번 라인의 반환 로직이 다소 복잡하고 이해하기 어렵습니다. 의도를 명확히 하고 가독성을 높이기 위해 다음과 같이 개선하는 것이 좋겠습니다:
-return labels.isEmpty() ? "알 수 없음" : MAP.getOrDefault(labels.get(0).name().toLowerCase(),"알 수 없음"); +// 레이블이 없는 경우 +if (labels.isEmpty()) { + return "알 수 없음"; +} + +// 매칭된 동물이 없지만 레이블은 있는 경우, 첫 번째 레이블을 시도 +String firstLabelName = labels.get(0).name().toLowerCase(); +return MAP.getOrDefault(firstLabelName, "알 수 없음");이렇게 하면 코드의 의도가 주석을 통해 명확해지고 각 조건별 처리가 분리되어 이해하기 쉬워집니다.
src/main/java/io/github/petty/vision/adapter/out/GeminiClientImpl.java (1)
55-70: 헬퍼 메소드 개선 가능성코드 중복을 줄이고 유지보수성을 높이기 위해 다음 개선사항을 제안합니다:
isBlank메소드는 Java 11+ 에서 제공하는String.isBlank()를 사용하거나, Apache Commons Lang의StringUtils.isBlank()를 사용validateGeminiConfig와 같은 검증 로직을 추상화하여 다른 클라이언트 구현체(Together 등)와 공유 가능-private boolean isBlank(String v) { return v == null || v.isBlank(); }또한 여러 클라이언트 구현체에서 공통으로 사용할 수 있는 추상 클래스를 만드는 것을 고려해보세요:
public abstract class AbstractApiClient<REQ, RESP> { protected final RestTemplate rt; protected AbstractApiClient(RestTemplate rt) { this.rt = rt; } protected HttpHeaders jsonHeaders() { HttpHeaders h = new HttpHeaders(); h.setContentType(MediaType.APPLICATION_JSON); return h; } protected void validateConfig(String url, String key, String serviceName) { if (url == null || url.isBlank() || key == null || key.isBlank()) { throw new IllegalStateException(serviceName + " URL/Key 설정이 없습니다"); } } // 기타 공통 메소드 }이를 통해
GeminiClientImpl,TogetherClientImpl등에서 중복 코드를 줄일 수 있습니다.src/main/java/io/github/petty/vision/adapter/in/VisionController.java (1)
10-10: 불필요한 빈 줄 제거코드 일관성 유지를 위해 불필요한 빈 줄을 제거하는 것이 좋습니다.
src/main/java/io/github/petty/vision/service/VisionServiceImpl.java (5)
22-22: 주석 처리된 코드 제거사용하지 않는 주석 처리된 코드는 제거하는 것이 좋습니다. 필요한 경우 버전 관리 시스템을 통해 과거 코드를 확인할 수 있습니다.
- // private final RestTemplate rest;
23-26: @OverRide 어노테이션 추가
analyze메서드에는@Override어노테이션이 있지만,interim메서드에는 없습니다.VisionUseCase인터페이스를 구현하는 메서드라면 일관성을 위해 모든 메서드에@Override어노테이션을 추가하는 것이 좋습니다.- public String interim(byte[] image, String petName) { + @Override + public String interim(byte[] image, String petName) {
33-35: 구체적인 예외 처리현재 코드는 모든 예외를
Exception으로 캐치하고 있습니다. 더 구체적인 예외 타입을 사용하면 디버깅과 문제 해결이 용이해집니다.try { img = file.getBytes(); -} catch (Exception e) { +} catch (IOException e) { throw new IllegalStateException("이미지를 읽을 수 없습니다", e); }
38-38: 불명확한 주석 수정 필요
// (원하면 프론트에 먼저 보내기)주석은 현재 구현과 일치하지 않습니다. 현재 구현에서는 두 AI 서비스가 모두 실패할 경우에만 interim 메시지를 리턴합니다. 주석이 향후 개선 방향을 나타내는 것이라면, TODO 형식으로 명확하게 표현하는 것이 좋습니다.- String interim = prompt.interimMsg(pet, species); // (원하면 프론트에 먼저 보내기) + String interim = prompt.interimMsg(pet, species); // TODO: 비동기 처리시 프론트에 먼저 전송하는 기능 구현 고려
41-47: 더 구체적인 예외 처리 필요
gemini.generate와together.generate호출 부분에서도 모든 예외를Exception으로 캐치하고 있습니다. 예외 유형에 따라 다르게 대응하려면 좀 더 구체적인 예외 타입을 사용하는 것이 좋습니다./* ---------------- Gemini ---------------- */ try { return gemini.generate( prompt.toGeminiReq(img, pet, species) ).plainText(); -} catch (Exception gex) { +} catch (IllegalStateException gex) { log.warn("Gemini 실패 → Together fallback", gex); } /* ---------------- Together -------------- */ try { return together.generate( prompt.toTogetherReq(img, pet) ).plainText(); -} catch (Exception tex) { +} catch (IllegalStateException tex) { log.error("Together 실패", tex); return interim + "\n\n최종 분석 보고서 생성에 실패했습니다."; }Also applies to: 49-57
src/main/java/io/github/petty/vision/helper/PromptFactory.java (1)
17-21: 사용자 친화적인 메시지 처리가 잘 구현되었습니다.조건부 메시지 생성이 잘 처리되었습니다. 다만, "알 수 없음"과 같은 문자열은 상수로 추출하는 것이 유지보수성을 높일 수 있습니다.
public class PromptFactory { private final VisionProperties prop; + private static final String SPECIES_UNKNOWN = "알 수 없음"; /* ---------- 공통 ---------- */ public String interimMsg(String pet, String sp){ - return "알 수 없음".equals(sp)? + return SPECIES_UNKNOWN.equals(sp)? String.format("'%s'에 대해서 알아볼게요! \n잠시만 기다려 주세요. 보고서를 작성 중입니다...", pet): String.format("오 '%s'는 '%s'이군요!\n잠시만 기다려 주세요. 보고서를 작성 중입니다...", pet, sp); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
.gitignore(1 hunks)src/main/java/io/github/petty/vision/adapter/in/VisionController.java(1 hunks)src/main/java/io/github/petty/vision/adapter/out/GeminiClientImpl.java(1 hunks)src/main/java/io/github/petty/vision/adapter/out/RekognitionClientImpl.java(1 hunks)src/main/java/io/github/petty/vision/adapter/out/TogetherClientImpl.java(1 hunks)src/main/java/io/github/petty/vision/config/VisionProperties.java(1 hunks)src/main/java/io/github/petty/vision/controller/VisionController.java(0 hunks)src/main/java/io/github/petty/vision/dto/gemini/GeminiRequest.java(1 hunks)src/main/java/io/github/petty/vision/dto/gemini/GeminiResponse.java(1 hunks)src/main/java/io/github/petty/vision/dto/together/TogetherRequest.java(1 hunks)src/main/java/io/github/petty/vision/dto/together/TogetherResponse.java(1 hunks)src/main/java/io/github/petty/vision/helper/PromptFactory.java(1 hunks)src/main/java/io/github/petty/vision/helper/SpeciesDetector.java(1 hunks)src/main/java/io/github/petty/vision/port/in/VisionUseCase.java(1 hunks)src/main/java/io/github/petty/vision/port/out/GeminiPort.java(1 hunks)src/main/java/io/github/petty/vision/port/out/RekognitionPort.java(1 hunks)src/main/java/io/github/petty/vision/port/out/TogetherPort.java(1 hunks)src/main/java/io/github/petty/vision/service/VisionService.java(0 hunks)src/main/java/io/github/petty/vision/service/VisionServiceImpl.java(1 hunks)
💤 Files with no reviewable changes (2)
- src/main/java/io/github/petty/vision/service/VisionService.java
- src/main/java/io/github/petty/vision/controller/VisionController.java
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/main/java/io/github/petty/vision/dto/together/TogetherResponse.java (1)
src/main/java/io/github/petty/vision/config/VisionProperties.java (4)
Getter(8-24)Getter(21-21)Getter(22-22)Getter(23-23)
src/main/java/io/github/petty/vision/adapter/out/GeminiClientImpl.java (2)
src/main/java/io/github/petty/vision/adapter/out/RekognitionClientImpl.java (1)
Repository(13-29)src/main/java/io/github/petty/vision/adapter/out/TogetherClientImpl.java (1)
Repository(15-70)
src/main/java/io/github/petty/vision/adapter/out/TogetherClientImpl.java (2)
src/main/java/io/github/petty/vision/adapter/out/RekognitionClientImpl.java (1)
Repository(13-29)src/main/java/io/github/petty/vision/adapter/out/GeminiClientImpl.java (1)
Repository(15-70)
🔇 Additional comments (5)
src/main/java/io/github/petty/vision/dto/together/TogetherRequest.java (1)
14-17: 구조적으로 잘 설계된 DTO입니다.클래스가 Record로 정의되어 있어 불변성을 보장하고, 필요한 메서드(constructor, getter, equals, hashCode, toString)가 자동으로 생성됩니다. Clean Architecture의 DTO 계층에 적합한 구현입니다.
src/main/java/io/github/petty/vision/dto/together/TogetherResponse.java (1)
32-40: 내부 클래스 구조가 적절합니다.응답 구조에 맞게
Choice와Message내부 클래스가 잘 구성되어 있습니다. Lombok을 활용하여 간결하게 작성된 점이 좋습니다.src/main/java/io/github/petty/vision/adapter/out/GeminiClientImpl.java (1)
15-22: Clean Architecture에 맞게 잘 구현된 클래스입니다.이 클래스는 포트/어댑터 패턴에 따라
GeminiPort인터페이스를 구현하고 있으며, 외부 시스템 통신을 담당하는 어댑터로서 적절한 위치에 있습니다. 의존성 주입도 생성자 주입 방식으로 올바르게 구현되어 있습니다.src/main/java/io/github/petty/vision/helper/PromptFactory.java (2)
1-15: 클래스 구조가 명확하고 Clean Architecture 원칙을 잘 따르고 있습니다.클래스가 단일 책임 원칙(SRP)을 잘 따르고 있으며, 적절한 의존성 주입을 사용하고 있습니다.
@RequiredArgsConstructor를 사용하여 필드 주입을 피하고 생성자 주입을 구현한 점이 좋습니다.
47-50: 프롬프트 내용이 구체적이고 잘 구성되어 있습니다.
detailedPrompt메서드는 텍스트 블록을 사용하여 가독성 높은 프롬프트를 생성하고 있습니다. 프롬프트가 구조화되어 있고 반려동물에 대한 중요한 정보를 포함하고 있어 좋습니다.
| @Override | ||
| public List<Label> detectLabels(byte[] image) { | ||
| DetectLabelsRequest req = DetectLabelsRequest.builder() | ||
| .image(Image.builder().bytes(SdkBytes.fromByteArray(image)).build()) | ||
| .maxLabels(20) | ||
| .build(); | ||
| DetectLabelsResponse resp = client.detectLabels(req); | ||
| return resp.labels(); // 👈 직렬화 NO, 그대로 반환 | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
예외 처리와 로깅이 개선될 수 있습니다.
detectLabels 메서드 구현은 깨끗하지만, 예외 처리와 로깅이 부족합니다. AWS 서비스 호출 중 발생할 수 있는 예외에 대한 처리가 없으며, 입력 이미지에 대한 유효성 검사도 부족합니다.
다음과 같은 개선을 제안합니다:
@Override
public List<Label> detectLabels(byte[] image) {
+ if (image == null || image.length == 0) {
+ log.error("이미지가 없거나 비어 있습니다.");
+ throw new IllegalArgumentException("이미지 데이터가 유효하지 않습니다.");
+ }
+
+ log.debug("AWS Rekognition 레이블 감지 시작 - 이미지 크기: {} bytes", image.length);
+
+ try {
DetectLabelsRequest req = DetectLabelsRequest.builder()
.image(Image.builder().bytes(SdkBytes.fromByteArray(image)).build())
.maxLabels(20)
.build();
DetectLabelsResponse resp = client.detectLabels(req);
+ log.debug("AWS Rekognition 레이블 감지 완료 - 감지된 레이블 수: {}", resp.labels().size());
return resp.labels(); // 👈 직렬화 NO, 그대로 반환
+ } catch (RekognitionException e) {
+ log.error("AWS Rekognition 레이블 감지 중 오류 발생: {}", e.getMessage(), e);
+ throw new RuntimeException("이미지 분석 중 오류가 발생했습니다.", e);
+ }
}추가로 maxLabels 값을 하드코딩하는 대신 설정 파일에서 구성 가능하도록 만드는 것을 고려해보세요.
📝 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.
| @Override | |
| public List<Label> detectLabels(byte[] image) { | |
| DetectLabelsRequest req = DetectLabelsRequest.builder() | |
| .image(Image.builder().bytes(SdkBytes.fromByteArray(image)).build()) | |
| .maxLabels(20) | |
| .build(); | |
| DetectLabelsResponse resp = client.detectLabels(req); | |
| return resp.labels(); // 👈 직렬화 NO, 그대로 반환 | |
| } | |
| @Override | |
| public List<Label> detectLabels(byte[] image) { | |
| if (image == null || image.length == 0) { | |
| log.error("이미지가 없거나 비어 있습니다."); | |
| throw new IllegalArgumentException("이미지 데이터가 유효하지 않습니다."); | |
| } | |
| log.debug("AWS Rekognition 레이블 감지 시작 - 이미지 크기: {} bytes", image.length); | |
| try { | |
| DetectLabelsRequest req = DetectLabelsRequest.builder() | |
| .image(Image.builder().bytes(SdkBytes.fromByteArray(image)).build()) | |
| .maxLabels(20) | |
| .build(); | |
| DetectLabelsResponse resp = client.detectLabels(req); | |
| log.debug("AWS Rekognition 레이블 감지 완료 - 감지된 레이블 수: {}", resp.labels().size()); | |
| return resp.labels(); // 👈 직렬화 NO, 그대로 반환 | |
| } catch (RekognitionException e) { | |
| log.error("AWS Rekognition 레이블 감지 중 오류 발생: {}", e.getMessage(), e); | |
| throw new RuntimeException("이미지 분석 중 오류가 발생했습니다.", e); | |
| } | |
| } |
| public String detect(byte[] img){ | ||
| List<Label> labels = rekognition.detectLabels(img); | ||
|
|
||
| List<String> matched = new ArrayList<>(); | ||
| for (Label l : labels){ | ||
| if(l.name()==null) continue; | ||
| String name = l.name().toLowerCase(); | ||
| MAP.keySet().stream() | ||
| .filter(name::contains) | ||
| .map(MAP::get) | ||
| .forEach(matched::add); | ||
| } | ||
| if(!matched.isEmpty()) return String.join(" + ", new HashSet<>(matched)); | ||
| return labels.isEmpty() ? "알 수 없음" : MAP.getOrDefault(labels.get(0).name().toLowerCase(),"알 수 없음"); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
detect 메서드 개선 가능성
detect 메서드에 다음과 같은 개선사항을 제안합니다:
- 입력 파라미터 유효성 검사 추가
- 스트림 API를 일관되게 사용하여 가독성 향상
- 로깅 추가로 디버깅 및 모니터링 용이성 확보
public String detect(byte[] img){
+ if (img == null || img.length == 0) {
+ throw new IllegalArgumentException("이미지 데이터가 없습니다");
+ }
+
+ log.debug("동물 종 감지 시작: 이미지 크기 {}bytes", img.length);
List<Label> labels = rekognition.detectLabels(img);
+ log.debug("감지된 레이블 수: {}", labels.size());
- List<String> matched = new ArrayList<>();
- for (Label l : labels){
- if(l.name()==null) continue;
- String name = l.name().toLowerCase();
- MAP.keySet().stream()
- .filter(name::contains)
- .map(MAP::get)
- .forEach(matched::add);
- }
+ Set<String> matched = labels.stream()
+ .filter(l -> l.name() != null)
+ .flatMap(l -> {
+ String name = l.name().toLowerCase();
+ return MAP.keySet().stream()
+ .filter(name::contains)
+ .map(MAP::get);
+ })
+ .collect(Collectors.toSet());
+
+ log.debug("매칭된 동물 종: {}", matched);
if(!matched.isEmpty()) return String.join(" + ", new HashSet<>(matched));
return labels.isEmpty() ? "알 수 없음" : MAP.getOrDefault(labels.get(0).name().toLowerCase(),"알 수 없음");
}위 개선사항은 코드의 가독성과 안정성을 높여줍니다. 특히 스트림 API를 일관되게 사용하면 코드의 의도가 더 명확해지고, 로깅을 추가하면 문제 해결이 용이해집니다.
📝 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.
| public String detect(byte[] img){ | |
| List<Label> labels = rekognition.detectLabels(img); | |
| List<String> matched = new ArrayList<>(); | |
| for (Label l : labels){ | |
| if(l.name()==null) continue; | |
| String name = l.name().toLowerCase(); | |
| MAP.keySet().stream() | |
| .filter(name::contains) | |
| .map(MAP::get) | |
| .forEach(matched::add); | |
| } | |
| if(!matched.isEmpty()) return String.join(" + ", new HashSet<>(matched)); | |
| return labels.isEmpty() ? "알 수 없음" : MAP.getOrDefault(labels.get(0).name().toLowerCase(),"알 수 없음"); | |
| } | |
| public String detect(byte[] img) { | |
| if (img == null || img.length == 0) { | |
| throw new IllegalArgumentException("이미지 데이터가 없습니다"); | |
| } | |
| log.debug("동물 종 감지 시작: 이미지 크기 {}bytes", img.length); | |
| List<Label> labels = rekognition.detectLabels(img); | |
| log.debug("감지된 레이블 수: {}", labels.size()); | |
| Set<String> matched = labels.stream() | |
| .filter(l -> l.name() != null) | |
| .flatMap(l -> { | |
| String name = l.name().toLowerCase(); | |
| return MAP.keySet().stream() | |
| .filter(name::contains) | |
| .map(MAP::get); | |
| }) | |
| .collect(Collectors.toSet()); | |
| log.debug("매칭된 동물 종: {}", matched); | |
| if (!matched.isEmpty()) { | |
| return String.join(" + ", new HashSet<>(matched)); | |
| } | |
| return labels.isEmpty() | |
| ? "알 수 없음" | |
| : MAP.getOrDefault(labels.get(0).name().toLowerCase(), "알 수 없음"); | |
| } |
| log.info("Gemini status={} bodyPresent={}", res.getStatusCode(), res.hasBody()); | ||
|
|
||
| if (res.getStatusCode() != HttpStatus.OK || res.getBody() == null) { | ||
| throw new IllegalStateException("Gemini 응답 오류 status=" + res.getStatusCode()); | ||
| } | ||
| return res.getBody(); | ||
|
|
||
| } catch (RestClientException ex) { | ||
| log.error("Gemini 호출 실패", ex); | ||
| throw new IllegalStateException("Gemini 호출 실패", ex); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
예외 처리 및 로깅 개선 권장
현재 코드는 모든 오류 상황에서 IllegalStateException을 사용하고 있습니다. 예외 처리를 더 명확하게 하고, 국제화 측면에서도 개선할 수 있습니다:
- 구체적인 예외 클래스 사용
- 로그 메시지의 언어 일관성 유지 (영어 또는 한글)
if (res.getStatusCode() != HttpStatus.OK || res.getBody() == null) {
- throw new IllegalStateException("Gemini 응답 오류 status=" + res.getStatusCode());
+ throw new ExternalApiException("Gemini API response error: status=" + res.getStatusCode());
}
return res.getBody();
} catch (RestClientException ex) {
- log.error("Gemini 호출 실패", ex);
- throw new IllegalStateException("Gemini 호출 실패", ex);
+ log.error("Gemini API call failed", ex);
+ throw new ExternalApiException("Gemini API call failed", ex);
}이를 위해 다음과 같은 커스텀 예외 클래스 생성을 권장합니다:
public class ExternalApiException extends RuntimeException {
public ExternalApiException(String message) {
super(message);
}
public ExternalApiException(String message, Throwable cause) {
super(message, cause);
}
}메시지 국제화가 필요한 경우 MessageSource를 활용하는 것도 고려해볼 수 있습니다.
| if (res.getStatusCode() == HttpStatus.OK && res.hasBody()) { | ||
| JsonNode choices = res.getBody().raw().path("choices"); | ||
| if (choices.isArray() && choices.size() > 0) { | ||
| return res.getBody(); | ||
| } | ||
| log.error("Together 응답에 choices 가 비어 있습니다: {}", res.getBody().raw()); | ||
| } | ||
| throw new IllegalStateException("Together API 오류 status=" + res.getStatusCode()); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
조건문 구조 개선 필요
현재 조건문 구조가 약간 복잡합니다. 상태가 OK이고 본문이 있는 경우에만 choices를 확인하지만, choices가 비어있을 때 로그만 남기고 예외는 조건문 밖에서 던지고 있습니다. 이는 상태가 OK가 아닌 경우에도 같은 예외가 발생하여 분기점이 명확하지 않습니다.
다음과 같이 조건문을 개선하는 것을 권장합니다:
- if (res.getStatusCode() == HttpStatus.OK && res.hasBody()) {
- JsonNode choices = res.getBody().raw().path("choices");
- if (choices.isArray() && choices.size() > 0) {
- return res.getBody();
- }
- log.error("Together 응답에 choices 가 비어 있습니다: {}", res.getBody().raw());
- }
- throw new IllegalStateException("Together API 오류 status=" + res.getStatusCode());
+ if (res.getStatusCode() != HttpStatus.OK || !res.hasBody()) {
+ throw new IllegalStateException("Together API 오류 status=" + res.getStatusCode());
+ }
+
+ JsonNode choices = res.getBody().raw().path("choices");
+ if (!choices.isArray() || choices.size() == 0) {
+ log.error("Together 응답에 choices 가 비어 있습니다: {}", res.getBody().raw());
+ throw new IllegalStateException("Together API 응답에 choices가 비어 있습니다");
+ }
+
+ return res.getBody();📝 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.
| if (res.getStatusCode() == HttpStatus.OK && res.hasBody()) { | |
| JsonNode choices = res.getBody().raw().path("choices"); | |
| if (choices.isArray() && choices.size() > 0) { | |
| return res.getBody(); | |
| } | |
| log.error("Together 응답에 choices 가 비어 있습니다: {}", res.getBody().raw()); | |
| } | |
| throw new IllegalStateException("Together API 오류 status=" + res.getStatusCode()); | |
| if (res.getStatusCode() != HttpStatus.OK || !res.hasBody()) { | |
| throw new IllegalStateException("Together API 오류 status=" + res.getStatusCode()); | |
| } | |
| JsonNode choices = res.getBody().raw().path("choices"); | |
| if (!choices.isArray() || choices.size() == 0) { | |
| log.error("Together 응답에 choices 가 비어 있습니다: {}", res.getBody().raw()); | |
| throw new IllegalStateException("Together API 응답에 choices가 비어 있습니다"); | |
| } | |
| return res.getBody(); |
| public String plainText(){ | ||
| if(candidates==null||candidates.isEmpty()) return ""; | ||
| return candidates.get(0).content.parts.stream() | ||
| .map(Part::text).reduce("", String::concat); | ||
| } |
There was a problem hiding this comment.
NullPointerException 방지를 위한 추가 검증 필요
현재 plainText() 메서드는 candidates가 null이거나 비어있는지만 확인하고 있습니다. 그러나 content나 parts가 null인 경우에 대한 검증이 없어 NPE가 발생할 수 있습니다.
다음과 같이 코드를 개선하는 것을 권장합니다:
public String plainText(){
if(candidates==null||candidates.isEmpty()) return "";
+ Candidate firstCandidate = candidates.get(0);
+ if(firstCandidate.content == null || firstCandidate.content.parts == null) return "";
return candidates.get(0).content.parts.stream()
.map(Part::text).reduce("", String::concat);
}또한 코드 스타일 측면에서 연산자 주변에 일관된 공백을 적용하는 것이 가독성을 높입니다:
-if(candidates==null||candidates.isEmpty()) return "";
+if (candidates == null || candidates.isEmpty()) return "";📝 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.
| public String plainText(){ | |
| if(candidates==null||candidates.isEmpty()) return ""; | |
| return candidates.get(0).content.parts.stream() | |
| .map(Part::text).reduce("", String::concat); | |
| } | |
| public String plainText() { | |
| if (candidates == null || candidates.isEmpty()) return ""; | |
| Candidate firstCandidate = candidates.get(0); | |
| if (firstCandidate.content == null || firstCandidate.content.parts == null) return ""; | |
| return candidates.get(0).content.parts.stream() | |
| .map(Part::text) | |
| .reduce("", String::concat); | |
| } |
| @ResponseBody | ||
| public String getSpeciesInterim(@RequestParam("file") MultipartFile file, | ||
| @RequestParam("petName") String petName) throws IOException { | ||
| return vision.interim(file.getBytes(), petName); | ||
| } | ||
|
|
||
| @PostMapping("/analyze") | ||
| @ResponseBody | ||
| public String analyze(@RequestParam("file") MultipartFile file, | ||
| @RequestParam("petName") String petName) { | ||
| return vision.analyze(file, petName); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
예외 처리 일관성 및 입력 검증 개선 필요
getSpeciesInterim 메서드는 throws IOException을 선언하고 있지만, analyze 메서드에서는 이 선언이 없습니다. 두 메서드 모두 file.getBytes()를 사용하고 있으므로 예외 처리 방식이 일관되어야 합니다.
또한, 파일이 실제로 이미지인지, pet 이름이 비어있지 않은지 등의 기본적인 입력 검증이 추가되면 좋을 것 같습니다.
@PostMapping("/species")
@ResponseBody
public String getSpeciesInterim(@RequestParam("file") MultipartFile file,
@RequestParam("petName") String petName) throws IOException {
+ if (file.isEmpty()) {
+ throw new IllegalArgumentException("이미지 파일이 비어 있습니다");
+ }
+ if (petName == null || petName.isBlank()) {
+ throw new IllegalArgumentException("반려동물 이름이 필요합니다");
+ }
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) throws IOException {
+ if (file.isEmpty()) {
+ throw new IllegalArgumentException("이미지 파일이 비어 있습니다");
+ }
+ if (petName == null || petName.isBlank()) {
+ throw new IllegalArgumentException("반려동물 이름이 필요합니다");
+ }
return vision.analyze(file, petName);
}Committable suggestion skipped: line range outside the PR's diff.
| package io.github.petty.vision.helper; | ||
|
|
||
| import io.github.petty.vision.config.VisionProperties; | ||
| import io.github.petty.vision.dto.gemini.GeminiRequest; | ||
| import io.github.petty.vision.dto.together.TogetherRequest; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
| import java.util.*; | ||
| import java.util.Base64; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class PromptFactory { | ||
| private final VisionProperties prop; | ||
|
|
||
| /* ---------- 공통 ---------- */ | ||
| public String interimMsg(String pet, String sp){ | ||
| return "알 수 없음".equals(sp)? | ||
| String.format("'%s'에 대해서 알아볼게요! \n잠시만 기다려 주세요. 보고서를 작성 중입니다...", pet): | ||
| String.format("오 '%s'는 '%s'이군요!\n잠시만 기다려 주세요. 보고서를 작성 중입니다...", pet, sp); | ||
| } | ||
|
|
||
| /* ---------- Gemini ---------- */ | ||
| public GeminiRequest toGeminiReq(byte[] img, String pet, String sp){ | ||
| String base64 = Base64.getEncoder().encodeToString(img); | ||
| String prompt = detailedPrompt(pet, sp); | ||
| Map<String,Object> part1 = Map.of("text", prompt); | ||
| Map<String,Object> part2 = Map.of("inline_data", Map.of("mime_type","image/jpeg","data", base64)); | ||
| Map<String,Object> content = Map.of("parts", List.of(part1, part2)); | ||
| return new GeminiRequest(List.of(content)); | ||
| } | ||
|
|
||
| /* ---------- Together ---------- */ | ||
| public TogetherRequest toTogetherReq(byte[] img, String pet){ | ||
| String base64 = Base64.getEncoder().encodeToString(img); | ||
| Map<String,Object> imageData = Map.of("format","jpeg","data", base64); | ||
| Map<String,Object> message = Map.of( | ||
| "role","user", | ||
| "content", List.of( | ||
| Map.of("type","text","text", detailedPrompt(pet, "")), | ||
| Map.of("type","image_data","image_data", imageData)) | ||
| ); | ||
| return new TogetherRequest(prop.getLlamaModel(), List.of(message)); | ||
| } | ||
|
|
||
| /* ---------- 내부 ---------- */ | ||
| private String detailedPrompt(String pet, String species){ | ||
| return String.format(""" | ||
| 반려동물 '%s'(종류: %s)에 대한 분석 보고서를 작성해줘.\n\n- 종류\n- 품종(믹스견은 추정 근거)\n- 외형(크기·털 색·특징)\n- 무게(1~40kg)\n- 맹수 여부\n- 감정·행동\n- 기타 특이사항(목줄·배경 등)\n\n보호자가 이해하기 쉬운 문장으로 요약해줘.""", pet, species); | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
🛠️ Refactor suggestion
전반적인 코드 품질과 구조에 대한 평가
이 클래스는 Clean Architecture 원칙을 잘 따르고 있으며, 프롬프트 생성과 요청 객체 구성이라는 단일 책임에 집중하고 있습니다. 코드가 명확하게 구성되어 있고 주석을 통해 각 섹션의 목적이 분명히 드러납니다.
다만, 몇 가지 개선할 점이 있습니다:
- 입력 매개변수 유효성 검증 추가
- Together 요청 시 종(species) 정보 활용 여부 확인
- 중복 코드(Base64 인코딩) 제거
- 하드코딩된 문자열 상수화
이러한 개선 사항을 반영하면 더욱 견고하고 유지보수하기 쉬운 코드가 될 것입니다.
| public GeminiRequest toGeminiReq(byte[] img, String pet, String sp){ | ||
| String base64 = Base64.getEncoder().encodeToString(img); | ||
| String prompt = detailedPrompt(pet, sp); | ||
| Map<String,Object> part1 = Map.of("text", prompt); | ||
| Map<String,Object> part2 = Map.of("inline_data", Map.of("mime_type","image/jpeg","data", base64)); | ||
| Map<String,Object> content = Map.of("parts", List.of(part1, part2)); | ||
| return new GeminiRequest(List.of(content)); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Gemini 요청 생성 로직이 잘 구현되었습니다.
이미지 데이터와 프롬프트를 적절히 조합하여 요청 객체를 생성하고 있습니다. 다만, 입력 매개변수(특히 byte[] img)에 대한 유효성 검증이 없습니다.
public GeminiRequest toGeminiReq(byte[] img, String pet, String sp){
+ if (img == null || img.length == 0) {
+ throw new IllegalArgumentException("이미지 데이터가 비어 있습니다");
+ }
+ if (pet == null || pet.isBlank()) {
+ throw new IllegalArgumentException("반려동물 이름이 비어 있습니다");
+ }
String base64 = Base64.getEncoder().encodeToString(img);
String prompt = detailedPrompt(pet, sp);
Map<String,Object> part1 = Map.of("text", prompt);
Map<String,Object> part2 = Map.of("inline_data", Map.of("mime_type","image/jpeg","data", base64));
Map<String,Object> content = Map.of("parts", List.of(part1, part2));
return new GeminiRequest(List.of(content));
}📝 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.
| public GeminiRequest toGeminiReq(byte[] img, String pet, String sp){ | |
| String base64 = Base64.getEncoder().encodeToString(img); | |
| String prompt = detailedPrompt(pet, sp); | |
| Map<String,Object> part1 = Map.of("text", prompt); | |
| Map<String,Object> part2 = Map.of("inline_data", Map.of("mime_type","image/jpeg","data", base64)); | |
| Map<String,Object> content = Map.of("parts", List.of(part1, part2)); | |
| return new GeminiRequest(List.of(content)); | |
| } | |
| public GeminiRequest toGeminiReq(byte[] img, String pet, String sp){ | |
| if (img == null || img.length == 0) { | |
| throw new IllegalArgumentException("이미지 데이터가 비어 있습니다"); | |
| } | |
| if (pet == null || pet.isBlank()) { | |
| throw new IllegalArgumentException("반려동물 이름이 비어 있습니다"); | |
| } | |
| String base64 = Base64.getEncoder().encodeToString(img); | |
| String prompt = detailedPrompt(pet, sp); | |
| Map<String,Object> part1 = Map.of("text", prompt); | |
| Map<String,Object> part2 = Map.of("inline_data", Map.of("mime_type","image/jpeg","data", base64)); | |
| Map<String,Object> content = Map.of("parts", List.of(part1, part2)); | |
| return new GeminiRequest(List.of(content)); | |
| } |
| /* ---------- Gemini ---------- */ | ||
| public GeminiRequest toGeminiReq(byte[] img, String pet, String sp){ | ||
| String base64 = Base64.getEncoder().encodeToString(img); | ||
| String prompt = detailedPrompt(pet, sp); | ||
| Map<String,Object> part1 = Map.of("text", prompt); | ||
| Map<String,Object> part2 = Map.of("inline_data", Map.of("mime_type","image/jpeg","data", base64)); | ||
| Map<String,Object> content = Map.of("parts", List.of(part1, part2)); | ||
| return new GeminiRequest(List.of(content)); | ||
| } | ||
|
|
||
| /* ---------- Together ---------- */ | ||
| public TogetherRequest toTogetherReq(byte[] img, String pet){ | ||
| String base64 = Base64.getEncoder().encodeToString(img); | ||
| Map<String,Object> imageData = Map.of("format","jpeg","data", base64); | ||
| Map<String,Object> message = Map.of( | ||
| "role","user", | ||
| "content", List.of( | ||
| Map.of("type","text","text", detailedPrompt(pet, "")), | ||
| Map.of("type","image_data","image_data", imageData)) | ||
| ); | ||
| return new TogetherRequest(prop.getLlamaModel(), List.of(message)); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
공통 로직 중복에 대한 리팩토링이 필요합니다.
toGeminiReq와 toTogetherReq 메서드 모두 Base64 인코딩 로직을 중복해서 사용하고 있습니다. 이를 별도의 private 메서드로 추출하여 코드 중복을 줄이는 것이 좋습니다.
/* ---------- 공통 ---------- */
+private String encodeImageToBase64(byte[] img) {
+ return Base64.getEncoder().encodeToString(img);
+}
/* ---------- Gemini ---------- */
public GeminiRequest toGeminiReq(byte[] img, String pet, String sp){
- String base64 = Base64.getEncoder().encodeToString(img);
+ String base64 = encodeImageToBase64(img);
String prompt = detailedPrompt(pet, sp);
Map<String,Object> part1 = Map.of("text", prompt);
Map<String,Object> part2 = Map.of("inline_data", Map.of("mime_type","image/jpeg","data", base64));
Map<String,Object> content = Map.of("parts", List.of(part1, part2));
return new GeminiRequest(List.of(content));
}
/* ---------- Together ---------- */
public TogetherRequest toTogetherReq(byte[] img, String pet){
- String base64 = Base64.getEncoder().encodeToString(img);
+ String base64 = encodeImageToBase64(img);
Map<String,Object> imageData = Map.of("format","jpeg","data", base64);
// ... 나머지 코드 ...
}📝 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.
| /* ---------- Gemini ---------- */ | |
| public GeminiRequest toGeminiReq(byte[] img, String pet, String sp){ | |
| String base64 = Base64.getEncoder().encodeToString(img); | |
| String prompt = detailedPrompt(pet, sp); | |
| Map<String,Object> part1 = Map.of("text", prompt); | |
| Map<String,Object> part2 = Map.of("inline_data", Map.of("mime_type","image/jpeg","data", base64)); | |
| Map<String,Object> content = Map.of("parts", List.of(part1, part2)); | |
| return new GeminiRequest(List.of(content)); | |
| } | |
| /* ---------- Together ---------- */ | |
| public TogetherRequest toTogetherReq(byte[] img, String pet){ | |
| String base64 = Base64.getEncoder().encodeToString(img); | |
| Map<String,Object> imageData = Map.of("format","jpeg","data", base64); | |
| Map<String,Object> message = Map.of( | |
| "role","user", | |
| "content", List.of( | |
| Map.of("type","text","text", detailedPrompt(pet, "")), | |
| Map.of("type","image_data","image_data", imageData)) | |
| ); | |
| return new TogetherRequest(prop.getLlamaModel(), List.of(message)); | |
| } | |
| /* ---------- 공통 ---------- */ | |
| private String encodeImageToBase64(byte[] img) { | |
| return Base64.getEncoder().encodeToString(img); | |
| } | |
| /* ---------- Gemini ---------- */ | |
| public GeminiRequest toGeminiReq(byte[] img, String pet, String sp){ | |
| String base64 = encodeImageToBase64(img); | |
| String prompt = detailedPrompt(pet, sp); | |
| Map<String,Object> part1 = Map.of("text", prompt); | |
| Map<String,Object> part2 = Map.of( | |
| "inline_data", | |
| Map.of("mime_type", "image/jpeg", "data", base64) | |
| ); | |
| Map<String,Object> content = Map.of("parts", List.of(part1, part2)); | |
| return new GeminiRequest(List.of(content)); | |
| } | |
| /* ---------- Together ---------- */ | |
| public TogetherRequest toTogetherReq(byte[] img, String pet){ | |
| String base64 = encodeImageToBase64(img); | |
| Map<String,Object> imageData = Map.of("format", "jpeg", "data", base64); | |
| Map<String,Object> message = Map.of( | |
| "role", "user", | |
| "content", List.of( | |
| Map.of("type", "text", "text", detailedPrompt(pet, "")), | |
| Map.of("type", "image_data", "image_data", imageData) | |
| ) | |
| ); | |
| return new TogetherRequest(prop.getLlamaModel(), List.of(message)); | |
| } |
| public TogetherRequest toTogetherReq(byte[] img, String pet){ | ||
| String base64 = Base64.getEncoder().encodeToString(img); | ||
| Map<String,Object> imageData = Map.of("format","jpeg","data", base64); | ||
| Map<String,Object> message = Map.of( | ||
| "role","user", | ||
| "content", List.of( | ||
| Map.of("type","text","text", detailedPrompt(pet, "")), | ||
| Map.of("type","image_data","image_data", imageData)) | ||
| ); | ||
| return new TogetherRequest(prop.getLlamaModel(), List.of(message)); | ||
| } |
There was a problem hiding this comment.
Together 요청 생성에서 종(species) 정보를 활용하지 않는 문제가 있습니다.
toTogetherReq 메서드는 detailedPrompt를 호출할 때 빈 문자열을 종(species) 매개변수로 전달하고 있어, 종 정보가 프롬프트에 반영되지 않습니다. 이는 의도적인 설계인지 확인이 필요합니다.
public TogetherRequest toTogetherReq(byte[] img, String pet){
+ if (img == null || img.length == 0) {
+ throw new IllegalArgumentException("이미지 데이터가 비어 있습니다");
+ }
+ if (pet == null || pet.isBlank()) {
+ throw new IllegalArgumentException("반려동물 이름이 비어 있습니다");
+ }
String base64 = Base64.getEncoder().encodeToString(img);
Map<String,Object> imageData = Map.of("format","jpeg","data", base64);
Map<String,Object> message = Map.of(
"role","user",
"content", List.of(
- Map.of("type","text","text", detailedPrompt(pet, "")),
+ Map.of("type","text","text", detailedPrompt(pet, "알 수 없음")),
Map.of("type","image_data","image_data", imageData))
);
return new TogetherRequest(prop.getLlamaModel(), List.of(message));
}|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Summary by CodeRabbit
Walkthrough이번 변경 사항은 반려동물 이미지 분석 및 리포트 생성을 위한 Vision 모듈의 구조를 대대적으로 리팩토링하고, 여러 외부 AI 서비스(Gemini, Together, AWS Rekognition)와의 연동을 위한 포트 및 어댑터, DTO, 헬퍼, 서비스 계층을 도입하는 내용입니다. 기존 VisionService 및 컨트롤러 구현을 삭제하고, 이를 대체하는 새로운 서비스 구현체, 컨트롤러, 포트/어댑터, DTO, 설정 클래스, 헬퍼 클래스 등이 추가되었습니다. 각 계층은 명확하게 역할이 분리되어 있으며, 외부 API 실패 시 대체 경로로 폴백하는 로직이 포함되어 있습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant VisionController
participant VisionServiceImpl
participant SpeciesDetector
participant PromptFactory
participant GeminiClientImpl
participant TogetherClientImpl
User->>VisionController: POST /vision/analyze (file, petName)
VisionController->>VisionServiceImpl: analyze(file, petName)
VisionServiceImpl->>VisionServiceImpl: 파일 바이트 추출
VisionServiceImpl->>SpeciesDetector: detect(이미지 바이트)
SpeciesDetector-->>VisionServiceImpl: 종명 반환
VisionServiceImpl->>PromptFactory: interimMsg(pet, species)
PromptFactory-->>VisionServiceImpl: 임시 메시지 반환
VisionServiceImpl->>GeminiClientImpl: generate(GeminiRequest)
alt Gemini 성공
GeminiClientImpl-->>VisionServiceImpl: GeminiResponse(plainText)
VisionServiceImpl-->>VisionController: 분석 결과 반환
else Gemini 실패
VisionServiceImpl->>TogetherClientImpl: generate(TogetherRequest)
alt Together 성공
TogetherClientImpl-->>VisionServiceImpl: TogetherResponse(plainText)
VisionServiceImpl-->>VisionController: 분석 결과 반환
else Together 실패
VisionServiceImpl-->>VisionController: 임시 메시지 + 실패 안내 반환
end
end
VisionController-->>User: 결과 응답
Poem
🪧 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: 9
🔭 Outside diff range comments (1)
src/main/java/io/github/petty/vision/port/out/RekognitionPort.java (1)
1-9: 🛠️ Refactor suggestionAWS SDK에 강한 결합이 있습니다.
아웃바운드 포트 인터페이스로서 기능적으로는 잘 설계되었으나, AWS SDK의
Label클래스를 직접 사용함으로써 외부 라이브러리에 강한 결합이 발생했습니다. Clean Architecture의 원칙에 따라 도메인이 외부 의존성에 좌우되지 않도록, 내부 DTO를 정의하고 어댑터에서 변환하는 방식을 고려해보세요.package io.github.petty.vision.port.out; -import software.amazon.awssdk.services.rekognition.model.Label; import java.util.List; +// 내부 도메인 모델 사용 +import io.github.petty.vision.dto.LabelDto; + public interface RekognitionPort { - List<Label> detectLabels(byte[] image); + List<LabelDto> detectLabels(byte[] image); }
♻️ Duplicate comments (10)
src/main/java/io/github/petty/vision/helper/SpeciesDetector.java (1)
24-38: detect 메서드 개선 가능성
detect메서드에 다음과 같은 개선사항을 제안합니다:
- 입력 파라미터 유효성 검사 추가
- 스트림 API를 일관되게 사용하여 가독성 향상
- 로깅 추가로 디버깅 및 모니터링 용이성 확보
src/main/java/io/github/petty/vision/adapter/out/RekognitionClientImpl.java (1)
20-28: 예외 처리와 로깅이 개선될 수 있습니다.detectLabels 메서드 구현은 깨끗하지만, 예외 처리와 로깅이 부족합니다. AWS 서비스 호출 중 발생할 수 있는 예외에 대한 처리가 없으며, 입력 이미지에 대한 유효성 검사도 부족합니다.
다음과 같은 개선을 제안합니다:
@Override public List<Label> detectLabels(byte[] image) { + if (image == null || image.length == 0) { + log.error("이미지가 없거나 비어 있습니다."); + throw new IllegalArgumentException("이미지 데이터가 유효하지 않습니다."); + } + + log.debug("AWS Rekognition 레이블 감지 시작 - 이미지 크기: {} bytes", image.length); + + try { DetectLabelsRequest req = DetectLabelsRequest.builder() .image(Image.builder().bytes(SdkBytes.fromByteArray(image)).build()) .maxLabels(20) .build(); DetectLabelsResponse resp = client.detectLabels(req); + log.debug("AWS Rekognition 레이블 감지 완료 - 감지된 레이블 수: {}", resp.labels().size()); return resp.labels(); // 👈 직렬화 NO, 그대로 반환 + } catch (RekognitionException e) { + log.error("AWS Rekognition 레이블 감지 중 오류 발생: {}", e.getMessage(), e); + throw new RuntimeException("이미지 분석 중 오류가 발생했습니다.", e); + } }추가로 maxLabels 값을 하드코딩하는 대신 설정 파일에서 구성 가능하도록 만드는 것을 고려해보세요.
src/main/java/io/github/petty/vision/adapter/out/GeminiClientImpl.java (1)
42-52: 예외 처리 및 로깅 개선 권장현재 코드는 모든 오류 상황에서
IllegalStateException을 사용하고 있습니다. 예외 처리를 더 명확하게 하고, 국제화 측면에서도 개선할 수 있습니다:
- 구체적인 예외 클래스 사용
- 로그 메시지의 언어 일관성 유지 (영어 또는 한글)
if (res.getStatusCode() != HttpStatus.OK || res.getBody() == null) { - throw new IllegalStateException("Gemini 응답 오류 status=" + res.getStatusCode()); + throw new ExternalApiException("Gemini API response error: status=" + res.getStatusCode()); } return res.getBody(); } catch (RestClientException ex) { - log.error("Gemini 호출 실패", ex); - throw new IllegalStateException("Gemini 호출 실패", ex); + log.error("Gemini API call failed", ex); + throw new ExternalApiException("Gemini API call failed", ex); }이를 위해 다음과 같은 커스텀 예외 클래스 생성을 권장합니다:
public class ExternalApiException extends RuntimeException { public ExternalApiException(String message) { super(message); } public ExternalApiException(String message, Throwable cause) { super(message, cause); } }메시지 국제화가 필요한 경우 MessageSource를 활용하는 것도 고려해볼 수 있습니다.
src/main/java/io/github/petty/vision/dto/gemini/GeminiResponse.java (1)
17-21: NullPointerException 방지를 위한 추가 검증 필요현재
plainText()메서드는candidates가 null이거나 비어있는지만 확인하고 있습니다. 그러나content나parts가 null인 경우에 대한 검증이 없어 NPE가 발생할 수 있습니다.다음과 같이 코드를 개선하는 것을 권장합니다:
public String plainText(){ - if(candidates==null||candidates.isEmpty()) return ""; + if (candidates == null || candidates.isEmpty()) return ""; + Candidate firstCandidate = candidates.get(0); + if (firstCandidate.content == null || firstCandidate.content.parts == null) return ""; return candidates.get(0).content.parts.stream() .map(Part::text).reduce("", String::concat); }또한 코드 스타일 측면에서 연산자 주변에 일관된 공백을 적용하는 것이 가독성을 높입니다:
-if(candidates==null||candidates.isEmpty()) return ""; +if (candidates == null || candidates.isEmpty()) return "";src/main/java/io/github/petty/vision/adapter/out/TogetherClientImpl.java (1)
40-47: 🛠️ Refactor suggestion조건문 구조 개선이 필요합니다.
현재 조건문 구조가 복잡하게 구성되어 있습니다. 상태가 OK이고 본문이 있는 경우에만 choices를 확인하지만, choices가 비어있을 때 로그만 남기고 예외는 조건문 밖에서 던지고 있습니다. 이로 인해 오류 처리 흐름이 명확하지 않습니다.
- if (res.getStatusCode() == HttpStatus.OK && res.hasBody()) { - JsonNode choices = res.getBody().raw().path("choices"); - if (choices.isArray() && choices.size() > 0) { - return res.getBody(); - } - log.error("Together 응답에 choices 가 비어 있습니다: {}", res.getBody().raw()); - } - throw new IllegalStateException("Together API 오류 status=" + res.getStatusCode()); + if (res.getStatusCode() != HttpStatus.OK || !res.hasBody()) { + throw new IllegalStateException("Together API 오류 status=" + res.getStatusCode()); + } + + JsonNode choices = res.getBody().raw().path("choices"); + if (!choices.isArray() || choices.size() == 0) { + log.error("Together 응답에 choices 가 비어 있습니다: {}", res.getBody().raw()); + throw new IllegalStateException("Together API 응답에 choices가 비어 있습니다"); + } + + return res.getBody();src/main/java/io/github/petty/vision/adapter/in/VisionController.java (2)
19-23: 🛠️ Refactor suggestion입력 검증 및 예외 처리 개선이 필요합니다.
현재 코드는 파일이 비어있거나 petName이 유효하지 않은 경우에 대한 검증이 없습니다. 또한
file.getBytes()에서 발생할 수 있는 IOException에 대한 처리도 개선이 필요합니다.@PostMapping("/species") @ResponseBody public String getSpeciesInterim(@RequestParam("file") MultipartFile file, @RequestParam("petName") String petName) throws IOException { + if (file.isEmpty()) { + throw new IllegalArgumentException("이미지 파일이 비어 있습니다"); + } + if (petName == null || petName.isBlank()) { + throw new IllegalArgumentException("반려동물 이름이 필요합니다"); + } return vision.interim(file.getBytes(), petName); }
25-30: 🛠️ Refactor suggestionanalyze 메서드의 예외 처리 일관성이 필요합니다.
analyze메서드는getSpeciesInterim과 달리throws IOException을 선언하지 않지만, 내부적으로vision.analyze()에서 파일 처리 시 IOException이 발생할 수 있습니다. 두 메서드의 예외 처리 방식을 일관되게 해야 합니다.@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) throws IOException { + if (file.isEmpty()) { + throw new IllegalArgumentException("이미지 파일이 비어 있습니다"); + } + if (petName == null || petName.isBlank()) { + throw new IllegalArgumentException("반려동물 이름이 필요합니다"); + } return vision.analyze(file, petName); }src/main/java/io/github/petty/vision/helper/PromptFactory.java (3)
34-44: Together 요청에서 종(species) 정보 활용 문제가 있습니다.
toTogetherReq메서드는 빈 문자열("")을 종(species) 매개변수로detailedPrompt에 전달하고 있어, 종 정보가 프롬프트에 반영되지 않습니다. 이는 의도적인 설계인지 확인이 필요합니다.public TogetherRequest toTogetherReq(byte[] img, String pet){ + if (img == null || img.length == 0) { + throw new IllegalArgumentException("이미지 데이터가 비어 있습니다"); + } + if (pet == null || pet.isBlank()) { + throw new IllegalArgumentException("반려동물 이름이 필요합니다"); + } String base64 = Base64.getEncoder().encodeToString(img); Map<String,Object> imageData = Map.of("format","jpeg","data", base64); Map<String,Object> message = Map.of( "role","user", "content", List.of( - Map.of("type","text","text", detailedPrompt(pet, "")), + Map.of("type","text","text", detailedPrompt(pet, "알 수 없음")), Map.of("type","image_data","image_data", imageData)) ); return new TogetherRequest(prop.getLlamaModel(), List.of(message)); }#!/bin/bash # 종(species) 정보가 어디서 가져오는지 확인 # VisionServiceImpl에서 Together API 호출 방식 확인 rg -A 3 -B 3 "toTogetherReq" src/
23-44: Base64 인코딩 중복 코드 리팩토링이 필요합니다.
toGeminiReq와toTogetherReq메서드 모두 동일한 Base64 인코딩 로직을 중복해서 사용하고 있습니다. 이를 별도의 private 메서드로 추출하는 것이 좋습니다./* ---------- 공통 ---------- */ +private String encodeImageToBase64(byte[] img) { + return Base64.getEncoder().encodeToString(img); +} /* ---------- Gemini ---------- */ public GeminiRequest toGeminiReq(byte[] img, String pet, String sp){ - String base64 = Base64.getEncoder().encodeToString(img); + String base64 = encodeImageToBase64(img); String prompt = detailedPrompt(pet, sp); Map<String,Object> part1 = Map.of("text", prompt); Map<String,Object> part2 = Map.of("inline_data", Map.of("mime_type","image/jpeg","data", base64)); Map<String,Object> content = Map.of("parts", List.of(part1, part2)); return new GeminiRequest(List.of(content)); } /* ---------- Together ---------- */ public TogetherRequest toTogetherReq(byte[] img, String pet){ - String base64 = Base64.getEncoder().encodeToString(img); + String base64 = encodeImageToBase64(img); Map<String,Object> imageData = Map.of("format","jpeg","data", base64); // ... 나머지 코드 ... }
24-31: 🛠️ Refactor suggestionGemini 요청 생성 시 입력 유효성 검증이 필요합니다.
toGeminiReq메서드는 입력 매개변수(특히 byte[] img)에 대한 유효성 검증이 없습니다. 이는 잠재적인 NullPointerException이나 오동작의 원인이 될 수 있습니다.public GeminiRequest toGeminiReq(byte[] img, String pet, String sp){ + if (img == null || img.length == 0) { + throw new IllegalArgumentException("이미지 데이터가 비어 있습니다"); + } + if (pet == null || pet.isBlank()) { + throw new IllegalArgumentException("반려동물 이름이 비어 있습니다"); + } String base64 = Base64.getEncoder().encodeToString(img); String prompt = detailedPrompt(pet, sp); Map<String,Object> part1 = Map.of("text", prompt); Map<String,Object> part2 = Map.of("inline_data", Map.of("mime_type","image/jpeg","data", base64)); Map<String,Object> content = Map.of("parts", List.of(part1, part2)); return new GeminiRequest(List.of(content)); }
🧹 Nitpick comments (7)
src/main/java/io/github/petty/vision/port/in/VisionUseCase.java (1)
1-9: 인바운드 포트 설계가 명확합니다.Clean Architecture의 인바운드 포트로서 사용자 사례(use case)를 잘 정의하고 있습니다. 두 메소드 모두 명확한 목적을 가지고 있으며, 파라미터와 반환 타입이 적절합니다. 다만, 복잡한 응답을 위해서는 String 대신 전용 DTO를 반환하는 것을 고려해 볼 수 있습니다.
package io.github.petty.vision.port.in; import org.springframework.web.multipart.MultipartFile; +import io.github.petty.vision.dto.AnalysisResponse; +import io.github.petty.vision.dto.InterimResponse; public interface VisionUseCase { - String analyze(MultipartFile file, String petName); - String interim(byte[] image, String petName); + AnalysisResponse analyze(MultipartFile file, String petName); + InterimResponse interim(byte[] image, String petName); }src/main/java/io/github/petty/vision/config/VisionProperties.java (1)
8-19: JavaDoc 문서화 추가 권장설정 클래스의 각 속성에 대한 목적과 예상되는 값 형식을 설명하는 JavaDoc 주석이 없습니다. 이는 코드의 가독성과 유지보수성을 저하시킬 수 있습니다.
다음과 같이 각 필드에 JavaDoc을 추가하여 문서화하는 것을 권장합니다:
@Getter @Setter @Component @ConfigurationProperties(prefix = "vision") public class VisionProperties { + /** + * AWS 관련 설정 (리전 등) + */ private Aws aws = new Aws(); + + /** + * Google Gemini API 관련 설정 (URL, API 키) + */ private Gemini gemini = new Gemini(); + + /** + * Together AI API 관련 설정 (URL, API 키) + */ private Together together = new Together(); + + /** + * Gemini 모델명 (예: gemini-pro-vision) + */ private String geminiModel; + + /** + * Together AI 모델명 (예: llama-3-70b-vision) + */ private String togetherModel; + + /** + * Llama 모델명 + */ private String llamaModel;이를 통해 다른 개발자가 애플리케이션을 설정할 때 각 속성의 의미와 필요한 값을 이해하는 데 도움이 됩니다.
src/main/java/io/github/petty/vision/dto/gemini/GeminiResponse.java (1)
6-16: 응답 구조에 대한 문서화 추가 권장현재 DTO 클래스에는 Gemini API의 응답 구조를 설명하는 문서화가 없습니다. 이는 다른 개발자가 코드를 이해하는 데 어려움을 줄 수 있습니다.
다음과 같이 각 클래스와 필드에 대한 JavaDoc을 추가하는 것을 권장합니다:
+ /** + * Gemini API 응답을 나타내는 DTO 클래스입니다. + * API 응답에서 생성된 텍스트를 추출하는 기능을 제공합니다. + */ @JsonIgnoreProperties(ignoreUnknown = true) public record GeminiResponse( + /** 응답 후보 목록 */ List<Candidate> candidates) { @JsonIgnoreProperties(ignoreUnknown = true) + /** 응답 후보를 나타내는 레코드 */ public record Candidate(Content content){} @JsonIgnoreProperties(ignoreUnknown = true) + /** 응답 내용을 나타내는 레코드 */ public record Content(List<Part> parts){} @JsonIgnoreProperties(ignoreUnknown = true) + /** 응답 텍스트 부분을 나타내는 레코드 */ public record Part(String text){}이렇게 하면 코드의 가독성과 유지보수성이 향상됩니다.
src/main/java/io/github/petty/vision/adapter/out/TogetherClientImpl.java (1)
23-33: 입력 매개변수 및 응답 로깅 개선이 필요합니다.현재 생성 요청과 응답에 대한 로깅이 최소한으로만 구현되어 있습니다. 문제 해결 시 유용한 정보를 로그에 포함하도록 개선하는 것이 좋습니다.
@Override public TogetherResponse generate(TogetherRequest req) { validateTogetherConfig(); HttpHeaders headers = jsonHeaders(); headers.setBearerAuth(prop.getTogether().getKey()); + log.info("Together API 요청: model={}, messages={}", + req.getModel(), + req.getMessages().size()); try { ResponseEntity<TogetherResponse> res = rt.exchange( prop.getTogether().getUrl(), HttpMethod.POST, new HttpEntity<>(req, headers), TogetherResponse.class ); - log.info("Together status={} bodyPresent={}", res.getStatusCode(), res.hasBody()); + log.info("Together 응답: status={}, bodyPresent={}, contentLength={}", + res.getStatusCode(), + res.hasBody(), + res.getHeaders().getContentLength());src/main/java/io/github/petty/vision/service/VisionServiceImpl.java (3)
16-20: 사용되지 않는VisionProperties필드 제거 또는 활용 검토
prop필드가 주입만 되고 실제 로직에서 전혀 사용되지 않고 있습니다.
의도적으로 남겨둔 것이라면 TODO 주석을 추가해 향후 용도를 분명히 하거나, 그렇지 않다면 필드를 제거해 가독성과 유지보수성을 높여 주세요.
22-22: 주석 처리된 미사용 코드 삭제 권장
// private final RestTemplate rest;주석은 현재 구현과 무관하며 혼란을 줄 수 있습니다.
필요 없다면 과감히 삭제해 주세요.
41-57: 폴백(fallback) 로직 모니터링·알림 강화 제안현재 Gemini → Together 순서로 폴백하며 WARN/ERROR 로그만 남깁니다.
운영 환경에서 반복적으로 발생해도 인지하지 못할 위험이 있으므로, 다음을 고려해 주세요.
MeterRegistry등을 활용한 실패 카운터 지표 노출@Retryable+Recovery로 재시도/폴백 구성을 명시적으로 분리- Slack·PagerDuty 등 알림 연동
이렇게 하면 외부 API 장애를 빠르게 탐지하고 대응할 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
.gitignore(1 hunks)src/main/java/io/github/petty/vision/adapter/in/VisionController.java(1 hunks)src/main/java/io/github/petty/vision/adapter/out/GeminiClientImpl.java(1 hunks)src/main/java/io/github/petty/vision/adapter/out/RekognitionClientImpl.java(1 hunks)src/main/java/io/github/petty/vision/adapter/out/TogetherClientImpl.java(1 hunks)src/main/java/io/github/petty/vision/config/VisionProperties.java(1 hunks)src/main/java/io/github/petty/vision/controller/VisionController.java(0 hunks)src/main/java/io/github/petty/vision/dto/gemini/GeminiRequest.java(1 hunks)src/main/java/io/github/petty/vision/dto/gemini/GeminiResponse.java(1 hunks)src/main/java/io/github/petty/vision/dto/together/TogetherRequest.java(1 hunks)src/main/java/io/github/petty/vision/dto/together/TogetherResponse.java(1 hunks)src/main/java/io/github/petty/vision/helper/PromptFactory.java(1 hunks)src/main/java/io/github/petty/vision/helper/SpeciesDetector.java(1 hunks)src/main/java/io/github/petty/vision/port/in/VisionUseCase.java(1 hunks)src/main/java/io/github/petty/vision/port/out/GeminiPort.java(1 hunks)src/main/java/io/github/petty/vision/port/out/RekognitionPort.java(1 hunks)src/main/java/io/github/petty/vision/port/out/TogetherPort.java(1 hunks)src/main/java/io/github/petty/vision/service/VisionService.java(0 hunks)src/main/java/io/github/petty/vision/service/VisionServiceImpl.java(1 hunks)
💤 Files with no reviewable changes (2)
- src/main/java/io/github/petty/vision/controller/VisionController.java
- src/main/java/io/github/petty/vision/service/VisionService.java
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/main/java/io/github/petty/vision/adapter/out/TogetherClientImpl.java (1)
src/main/java/io/github/petty/vision/adapter/out/GeminiClientImpl.java (1)
Repository(15-70)
src/main/java/io/github/petty/vision/helper/PromptFactory.java (1)
src/main/java/io/github/petty/vision/helper/SpeciesDetector.java (1)
Component(10-39)
src/main/java/io/github/petty/vision/dto/together/TogetherResponse.java (1)
src/main/java/io/github/petty/vision/config/VisionProperties.java (4)
Getter(8-24)Getter(21-21)Getter(22-22)Getter(23-23)
🔇 Additional comments (7)
src/main/java/io/github/petty/vision/port/out/GeminiPort.java (1)
1-8: 인터페이스 설계가 깔끔합니다.Clean Architecture 원칙에 잘 맞춰 설계된 아웃바운드 포트 인터페이스입니다. 단일 책임 원칙을 따르고 있으며, 메소드 시그니처가 명확합니다. GeminiRequest와 GeminiResponse DTO를 사용하여 의존성 역전 원칙(DIP)을 잘 구현했습니다.
src/main/java/io/github/petty/vision/dto/together/TogetherResponse.java (3)
18-30: 응답 구조와 추출 로직이 잘 정의되어 있습니다.클래스 구조가 Together API의 응답 형식에 맞게 잘 설계되어 있으며,
plainText()메서드가 첫 번째 응답 메시지의 내용을 효과적으로 추출합니다. 객체 그래프 탐색 시 choices가 null이거나 비어있는 경우를 적절히 처리하고 있습니다.
22-25:raw()메서드의 주석이 유익합니다.주석에 메서드가 어디서 사용되는지(
TogetherClientImpl에서) 명시하여 코드의 흐름을 이해하는 데 도움이 됩니다. 이러한 문서화 스타일은 매우 좋은 관행입니다.
32-35: 내부 클래스 구조가 간결하게 정의되어 있습니다.Lombok의
@Getter와@Setter애노테이션을 활용하여 내부 클래스를 간결하게 정의했습니다. 중첩 구조가 API 응답 형식을 명확하게 표현합니다.Also applies to: 37-40
src/main/java/io/github/petty/vision/adapter/out/TogetherClientImpl.java (1)
57-61: 설정 검증 메시지가 명확합니다.
validateTogetherConfig()메서드는 필수 설정이 누락된 경우 명확한 예외 메시지를 제공하여 문제 해결을 용이하게 합니다.src/main/java/io/github/petty/vision/adapter/in/VisionController.java (1)
11-18: 컨트롤러 구조가 간결하게 설계되어 있습니다.Clean Architecture 원칙에 따라 컨트롤러가 가볍게 유지되며, 비즈니스 로직은 적절하게 VisionUseCase에 위임하고 있습니다. 또한 엔드포인트 매핑이 명확하게 정의되어 있습니다.
src/main/java/io/github/petty/vision/helper/PromptFactory.java (1)
46-50: 자세한 프롬프트 템플릿이 잘 작성되어 있습니다.
detailedPrompt메서드는 텍스트 블록을 활용하여 반려동물 분석 보고서에 필요한 항목들을 명확하게 정의하고 있습니다. 프롬프트 구조가 체계적으로 잘 설계되어 있습니다.
| ### dev ### | ||
| application-dev.yml | ||
| credentials.json | ||
|
|
||
| ### secret ### | ||
| application-secret.yml | ||
|
|
||
| ### Vision ### | ||
| application-vision-only.yml No newline at end of file |
There was a problem hiding this comment.
민감한 파일 패턴이 누락되었습니다.
PR 설명에서는 credentials.json, secrets/**, *.pem 등의 민감한 파일 패턴을 .gitignore에 추가한다고 했으나, 실제 변경 사항에는 이러한 패턴이 포함되어 있지 않습니다. 보안을 강화하기 위해 이러한 패턴을 추가하는 것을 권장합니다.
### secret ###
application-secret.yml
+credentials.json
+secrets/**
+*.pem
### Vision ###
application-vision-only.yml📝 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.
| ### dev ### | |
| application-dev.yml | |
| credentials.json | |
| ### secret ### | |
| application-secret.yml | |
| ### Vision ### | |
| application-vision-only.yml | |
| ### secret ### | |
| application-secret.yml | |
| credentials.json | |
| secrets/** | |
| *.pem | |
| ### Vision ### | |
| application-vision-only.yml |
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| public record GeminiRequest(List<Map<String,Object>> contents) {} No newline at end of file |
There was a problem hiding this comment.
🛠️ Refactor suggestion
레코드 필드에 대한 설명이 부족합니다.
contents 필드는 복잡한 구조(List<Map<String,Object>>)를 가지고 있는데, 이 구조가 어떻게 구성되어야 하는지에 대한 문서화가 없습니다. Gemini API와 통신할 때 이 필드의 정확한 형식과 필수 키 값들에 대한 Javadoc을 추가하는 것이 좋겠습니다.
-public record GeminiRequest(List<Map<String,Object>> contents) {}
+/**
+ * Gemini API 요청을 위한 DTO
+ *
+ * @param contents API 요청 본문
+ * 예시 형식:
+ * [
+ * {
+ * "role": "user",
+ * "parts": [
+ * {"text": "프롬프트 텍스트"},
+ * {"inlineData": {"mimeType": "image/jpeg", "data": "base64 인코딩된 이미지"}}
+ * ]
+ * }
+ * ]
+ */
+public record GeminiRequest(List<Map<String,Object>> contents) {}| public interface TogetherPort { | ||
| TogetherResponse generate(TogetherRequest req); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
인터페이스 및 메소드 문서화 필요
이 포트 인터페이스와 generate 메소드에 대한 Javadoc 설명이 없습니다. 클린 아키텍처의 포트로서 어떤 역할을 하는지, 외부 시스템과의 통신 시 발생할 수 있는 예외 상황은
어떻게 처리하는지 등에 대한 문서화가 필요합니다.
+/**
+ * Together API와의 통신을 위한 아웃바운드 포트 인터페이스
+ * Together LLM 모델을 사용하여 텍스트 및 이미지 분석 결과를 생성합니다.
+ */
public interface TogetherPort {
+ /**
+ * Together API를 호출하여 주어진 요청에 대한 LLM 응답을 생성합니다.
+ *
+ * @param req 프롬프트 및 이미지를 포함한 Together API 요청 객체
+ * @return 생성된 Together API 응답
+ * @throws RuntimeException API 호출 실패, 응답 파싱 오류 등 예외 발생 시
+ */
TogetherResponse generate(TogetherRequest req);
}| /** | ||
| * Together Chat Completion 요청용 DTO | ||
| * * 예시 | ||
| * { | ||
| * "model": "meta-llama/Llama-3.2‑11B‑Vision‑Instruct‑Turbo‑Free", | ||
| * "messages": [ ... ] | ||
| * } | ||
| */ | ||
| public record TogetherRequest( | ||
| String model, | ||
| List<Map<String, Object>> messages | ||
| ) {} |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Javadoc 예시 개선 필요
현재 Javadoc의 예시에는 "messages": [ ... ] 부분이 구체적으로 어떤 형식으로 구성되어야 하는지 명확하지 않습니다. Together API에서 요구하는 메시지 구조에 대한 자세한 예시를 추가하면 이 DTO를 사용하는 개발자가 더 쉽게 이해할 수 있을 것입니다.
/**
* Together Chat Completion 요청용 DTO
* * 예시
* {
* "model": "meta-llama/Llama-3.2‑11B‑Vision‑Instruct‑Turbo‑Free",
- * "messages": [ ... ]
+ * "messages": [
+ * {
+ * "role": "system",
+ * "content": "당신은 반려동물 분석 전문가입니다."
+ * },
+ * {
+ * "role": "user",
+ * "content": [
+ * {
+ * "type": "text",
+ * "text": "이 고양이의 특징을 분석해줘."
+ * },
+ * {
+ * "type": "image_url",
+ * "image_url": {
+ * "url": "data:image/jpeg;base64,..."
+ * }
+ * }
+ * ]
+ * }
+ * ]
* }
*/📝 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.
| /** | |
| * Together Chat Completion 요청용 DTO | |
| * * 예시 | |
| * { | |
| * "model": "meta-llama/Llama-3.2‑11B‑Vision‑Instruct‑Turbo‑Free", | |
| * "messages": [ ... ] | |
| * } | |
| */ | |
| public record TogetherRequest( | |
| String model, | |
| List<Map<String, Object>> messages | |
| ) {} | |
| /** | |
| * Together Chat Completion 요청용 DTO | |
| * * 예시 | |
| * { | |
| * "model": "meta-llama/Llama-3.2‑11B‑Vision‑Instruct‑Turbo‑Free", | |
| * "messages": [ | |
| * { | |
| * "role": "system", | |
| * "content": "당신은 반려동물 분석 전문가입니다." | |
| * }, | |
| * { | |
| * "role": "user", | |
| * "content": [ | |
| * { | |
| * "type": "text", | |
| * "text": "이 고양이의 특징을 분석해줘." | |
| * }, | |
| * { | |
| * "type": "image_url", | |
| * "image_url": { | |
| * "url": "data:image/jpeg;base64,..." | |
| * } | |
| * } | |
| * ] | |
| * } | |
| * ] | |
| * } | |
| */ | |
| public record TogetherRequest( | |
| String model, | |
| List<Map<String, Object>> messages | |
| ) {} |
| /** 영어 → 한글 매핑표 (생략) */ | ||
| private static final Map<String,String> MAP = Map.of( | ||
| "cat","고양이","kitten","고양이", | ||
| "dog","개","puppy","개", | ||
| "rabbit","토끼", "hamster","햄스터" | ||
| /* … */ | ||
| ); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Map 상수 정의 개선 필요
현재 MAP 상수 정의에는 /* … */ 주석이 있어 일부 동물 종만 예시로 표시되어 있습니다. 실제 사용 시에는 필요한 모든 동물 종을 포함하도록 완성이 필요합니다. 또한, Map.of()는 최대 10개의 키-값 쌍만 지원하므로, 더 많은 종을 매핑해야 한다면 다른 방식의 Map 초기화가 필요합니다.
- /** 영어 → 한글 매핑표 (생략) */
- private static final Map<String,String> MAP = Map.of(
- "cat","고양이","kitten","고양이",
- "dog","개","puppy","개",
- "rabbit","토끼", "hamster","햄스터"
- /* … */
- );
+ /** 영어 → 한글 동물 종 매핑표 */
+ private static final Map<String,String> MAP = new HashMap<>() {{
+ put("cat", "고양이");
+ put("kitten", "고양이");
+ put("dog", "개");
+ put("puppy", "개");
+ put("rabbit", "토끼");
+ put("hamster", "햄스터");
+ put("turtle", "거북이");
+ put("bird", "새");
+ put("fish", "물고기");
+ put("guinea pig", "기니피그");
+ // 필요한 다른 동물 종 추가
+ }};📝 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 static final Map<String,String> MAP = Map.of( | |
| "cat","고양이","kitten","고양이", | |
| "dog","개","puppy","개", | |
| "rabbit","토끼", "hamster","햄스터" | |
| /* … */ | |
| ); | |
| /** 영어 → 한글 동물 종 매핑표 */ | |
| private static final Map<String,String> MAP = new HashMap<>() {{ | |
| put("cat", "고양이"); | |
| put("kitten", "고양이"); | |
| put("dog", "개"); | |
| put("puppy", "개"); | |
| put("rabbit", "토끼"); | |
| put("hamster", "햄스터"); | |
| put("turtle", "거북이"); | |
| put("bird", "새"); | |
| put("fish", "물고기"); | |
| put("guinea pig", "기니피그"); | |
| // 필요한 다른 동물 종 추가 | |
| }}; |
| String url = UriComponentsBuilder | ||
| .fromHttpUrl(prop.getGemini().getUrl()) | ||
| .queryParam("key", prop.getGemini().getKey()) | ||
| .toUriString(); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
API 키를 URL 쿼리 파라미터로 전송하는 보안 위험이 있습니다.
현재 구현에서는 Gemini API 키를 URL 쿼리 파라미터로 전송하고 있습니다. 이는 다음과 같은 보안 위험을 초래할 수 있습니다:
- 서버 로그에 API 키가 평문으로 기록될 수 있습니다.
- 프록시 서버나 중간자가 URL을 볼 수 있습니다.
- 브라우저 히스토리나 캐시에 저장될 수 있습니다.
API 키는 일반적으로 HTTP 헤더를 통해 전송하는 것이 더 안전합니다:
- String url = UriComponentsBuilder
- .fromHttpUrl(prop.getGemini().getUrl())
- .queryParam("key", prop.getGemini().getKey())
- .toUriString();
+ String url = prop.getGemini().getUrl();그리고 jsonHeaders() 메서드를 수정하여 헤더에 API 키를 추가합니다:
private HttpHeaders jsonHeaders() {
HttpHeaders h = new HttpHeaders();
h.setContentType(MediaType.APPLICATION_JSON);
+ h.set("x-goog-api-key", prop.getGemini().getKey());
return h;
}| public class VisionProperties { | ||
|
|
||
| private Aws aws = new Aws(); | ||
| private Gemini gemini = new Gemini(); | ||
| private Together together = new Together(); | ||
| private String geminiModel; | ||
| private String togetherModel; | ||
| private String llamaModel; | ||
|
|
||
| @Getter @Setter public static class Aws { private String region; } | ||
| @Getter @Setter public static class Gemini { private String url; private String key; } | ||
| @Getter @Setter public static class Together { private String url; private String key; } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
필수 속성에 대한 유효성 검증 추가 권장
현재 설정 클래스는 필수 속성에 대한 명시적인 유효성 검증이 없습니다. 런타임 시 속성이 누락되면 예기치 않은 NullPointerException이 발생할 수 있습니다.
Jakarta Validation 또는 Spring Validation을 사용하여 필수 속성에 대한 검증을 추가하세요:
+ import jakarta.validation.Valid;
+ import jakarta.validation.constraints.NotBlank;
+ import org.springframework.validation.annotation.Validated;
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "vision")
+ @Validated
public class VisionProperties {
private Aws aws = new Aws();
+ @Valid
private Gemini gemini = new Gemini();
+ @Valid
private Together together = new Together();
+ @NotBlank(message = "vision.geminiModel must not be empty")
private String geminiModel;
+ @NotBlank(message = "vision.togetherModel must not be empty")
private String togetherModel;
+ @NotBlank(message = "vision.llamaModel must not be empty")
private String llamaModel;
@Getter @Setter public static class Aws { private String region; }
- @Getter @Setter public static class Gemini { private String url; private String key; }
- @Getter @Setter public static class Together { private String url; private String key; }
+ @Getter @Setter public static class Gemini {
+ @NotBlank(message = "vision.gemini.url must not be empty")
+ private String url;
+ @NotBlank(message = "vision.gemini.key must not be empty")
+ private String key;
+ }
+ @Getter @Setter public static class Together {
+ @NotBlank(message = "vision.together.url must not be empty")
+ private String url;
+ @NotBlank(message = "vision.together.key must not be empty")
+ private String key;
+ }
}이렇게 하면 애플리케이션 시작 시 필수 속성이 누락되었을 때 명확한 오류 메시지와 함께 즉시 실패하므로 디버깅이 쉬워집니다.
📝 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.
| public class VisionProperties { | |
| private Aws aws = new Aws(); | |
| private Gemini gemini = new Gemini(); | |
| private Together together = new Together(); | |
| private String geminiModel; | |
| private String togetherModel; | |
| private String llamaModel; | |
| @Getter @Setter public static class Aws { private String region; } | |
| @Getter @Setter public static class Gemini { private String url; private String key; } | |
| @Getter @Setter public static class Together { private String url; private String key; } | |
| // + these imports at the top of the file | |
| import jakarta.validation.Valid; | |
| import jakarta.validation.constraints.NotBlank; | |
| import org.springframework.validation.annotation.Validated; | |
| @Getter | |
| @Setter | |
| @Component | |
| @ConfigurationProperties(prefix = "vision") | |
| @Validated | |
| public class VisionProperties { | |
| private Aws aws = new Aws(); | |
| @Valid | |
| private Gemini gemini = new Gemini(); | |
| @Valid | |
| private Together together = new Together(); | |
| @NotBlank(message = "vision.geminiModel must not be empty") | |
| private String geminiModel; | |
| @NotBlank(message = "vision.togetherModel must not be empty") | |
| private String togetherModel; | |
| @NotBlank(message = "vision.llamaModel must not be empty") | |
| private String llamaModel; | |
| @Getter @Setter | |
| public static class Aws { | |
| private String region; | |
| } | |
| @Getter @Setter | |
| public static class Gemini { | |
| @NotBlank(message = "vision.gemini.url must not be empty") | |
| private String url; | |
| @NotBlank(message = "vision.gemini.key must not be empty") | |
| private String key; | |
| } | |
| @Getter @Setter | |
| public static class Together { | |
| @NotBlank(message = "vision.together.url must not be empty") | |
| private String url; | |
| @NotBlank(message = "vision.together.key must not be empty") | |
| private String key; | |
| } | |
| } |
| 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
입력값 검증 추가 필요
interim(byte[] image, String petName)는 image 길이나 petName의 빈 문자열 여부를 점검하지 않습니다.
외부(Controller)로부터 직접 호출될 가능성이 있으므로 NPE·불필요한 API 호출을 막기 위해 아래와 같이 사전 검증을 추가하는 편이 안전합니다.
public String interim(byte[] image, String petName) {
- String species = detector.detect(image);
+ if (image == null || image.length == 0) {
+ throw new IllegalArgumentException("image가 비어 있습니다.");
+ }
+ if (petName == null || petName.isBlank()) {
+ throw new IllegalArgumentException("petName이 비어 있습니다.");
+ }
+ String species = detector.detect(image);
return prompt.interimMsg(petName, species);
}📝 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.
| public String interim(byte[] image, String petName) { | |
| String species = detector.detect(image); | |
| return prompt.interimMsg(petName, species); | |
| } | |
| public String interim(byte[] image, String petName) { | |
| if (image == null || image.length == 0) { | |
| throw new IllegalArgumentException("image가 비어 있습니다."); | |
| } | |
| if (petName == null || petName.isBlank()) { | |
| throw new IllegalArgumentException("petName이 비어 있습니다."); | |
| } | |
| String species = detector.detect(image); | |
| return prompt.interimMsg(petName, species); | |
| } |
| byte[] img; | ||
| try { | ||
| img = file.getBytes(); | ||
| } catch (Exception e) { | ||
| throw new IllegalStateException("이미지를 읽을 수 없습니다", e); | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
대용량 파일·I/O 예외 처리 보강
-
file.getBytes()는 파일 크기만큼 메모리를 한 번에 할당합니다.
예상보다 큰 이미지가 업로드되면 OOM(Out Of Memory) 위험이 있습니다.- 파일 크기를
VisionProperties등으로 제한하거나 - 스트림 처리로 변경하는 방안을 검토해 주세요.
- 파일 크기를
-
catch (Exception e)로 모든 예외를 묶어IllegalStateException으로 감싸면 원인 파악이 어려워집니다.
구체적으로IOException만 캐치하거나, 추가적인 예외 메시지를 포함해 주세요.
byte[] img;
try {
img = file.getBytes();
} catch (IOException ioEx) {
throw new IllegalStateException("이미지 바이너리 읽기 실패", ioEx);
}📝 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.
| byte[] img; | |
| try { | |
| img = file.getBytes(); | |
| } catch (Exception e) { | |
| throw new IllegalStateException("이미지를 읽을 수 없습니다", e); | |
| } | |
| byte[] img; | |
| try { | |
| img = file.getBytes(); | |
| } catch (IOException ioEx) { | |
| throw new IllegalStateException("이미지 바이너리 읽기 실패", ioEx); | |
| } |
리뷰
제안
좋은 코드 작성해주셔서 감사합니다. 고생 많으셨습니다. |
📜 PR 내용 요약
.gitignore에 민감 파일 패턴을 추가했습니다.⚒️ 작업 및 변경 내용
.gitignoreVisionProperties.javavision.*YAML 바인딩VisionControllerVisionServiceImplGeminiClientImpl,TogetherClientImpl,RekognitionClientImplPromptFactory,SpeciesDetectorGeminiRequest/Response,TogetherRequest/Responseapplication-vision-only.yml📚 기타 참고 사항