Skip to content

[VISION] Vision 모듈 전면 리팩터링 + .gitignore 보강#21

Merged
s0ooo0k merged 4 commits into
PETTY-HUB:mainfrom
23MinL:main
Apr 24, 2025
Merged

[VISION] Vision 모듈 전면 리팩터링 + .gitignore 보강#21
s0ooo0k merged 4 commits into
PETTY-HUB:mainfrom
23MinL:main

Conversation

@23MinL
Copy link
Copy Markdown
Contributor

@23MinL 23MinL commented Apr 23, 2025

📜 PR 내용 요약

이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능)

  • .gitignore에 민감 파일 패턴을 추가했습니다.
  • Vision 모듈을 클린 아키텍처(Port/Adapter) 구조로 전면 개편했습니다.

⚒️ 작업 및 변경 내용

변경 내용, 업데이트 및 수정 사항을 자세하게 적어주세요

구분 파일/폴더 설명
chore .gitignore credentials.json, secrets/**, *.pem 등 무시
config VisionProperties.java vision.* YAML 바인딩
controller VisionController thin controller, IO 예외 처리
service VisionServiceImpl 비즈니스 로직 + Gemini→Together fail-over
adapter-out GeminiClientImpl, TogetherClientImpl, RekognitionClientImpl RestTemplate 호출·검증·로깅
helper PromptFactory, SpeciesDetector 책임 분리
dto GeminiRequest/Response, TogetherRequest/Response 직렬/역직렬화 DTO
port in/out 5종 DIP 적용
resources application-vision-only.yml 모델·URL·Key 외부화

📚 기타 참고 사항

리뷰 포인트, 참고 사항, 빌드 관련 내용 기타 사항을 자세히 적어주세요

  • 폴더 및 파일 구조가 클린 아키텍처 원칙에 따라 구성되었는지 확인해 주세요.
  • 클래스와 메서드의 네이밍이 역할을 직관적으로 드러내는지 확인해 주세요.
  • VisionProperties 설정 바인딩이 정상적으로 이루어지는지 확인해 주세요.
  • 외부 API 응답을 매핑하는 DTO 필드 구성이 실제 응답 스키마와 일치하는지 확인해 주세요.
  • 예외 처리 및 로그 메시지가 충분히 보완되었는지 확인해 주세요.

@23MinL 23MinL changed the title [refactor] Vision 모듈 전면 리팩터링 + .gitignore 보강 [VISION] Vision 모듈 전면 리팩터링 + .gitignore 보강 Apr 23, 2025
@PETTY-HUB PETTY-HUB deleted a comment from coderabbitai Bot Apr 23, 2025
@PETTY-HUB PETTY-HUB deleted a comment from coderabbitai Bot Apr 23, 2025
@s0ooo0k
Copy link
Copy Markdown
Member

s0ooo0k commented Apr 23, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)에 맞게 설계되어 있습니다.

다만 몇 가지 개선 사항을 제안드립니다:

  1. AWS SDK 타입(Label)에 직접 의존하고 있어 AWS SDK와의 결합도가 높습니다. 자체 DTO를 정의하여 외부 라이브러리와의 결합도를 낮추는 것이 좋을 것 같습니다.

  2. 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)를 명확히 정의하고 있습니다.

개선을 위한 몇 가지 제안사항이 있습니다:

  1. 반환 타입이 String으로 되어 있어 구조화된 데이터를 반환하기 어렵습니다. DTO를 사용하여 구조화된 응답을 제공하는 것이 더 좋을 것 같습니다.

  2. analyzeinterim 메서드의 차이점과 목적이 명확하지 않습니다. Javadoc을 추가하여 각 메서드의 역할을 설명하면 좋을 것 같습니다.

  3. 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를 사용하여 외부 설정을 효과적으로 바인딩하고 있습니다. 중첩 클래스를 사용하여 관련 속성을 그룹화한 것이 좋은 접근 방식입니다.

몇 가지 개선 사항을 제안합니다:

  1. 필수 속성에 대한 유효성 검증 어노테이션(@NotNull, @notblank 등)을 추가하여 런타임 오류를 방지하세요.
  2. 각 필드와 클래스에 JavaDoc 주석을 추가하여 속성의 목적과 사용법을 명확히 하세요.
  3. 민감한 정보(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: 매핑 테이블 개선 필요

현재 매핑 테이블에는 /* … */ 주석이 있어 완성되지 않은 것으로 보입니다. 두 가지 개선사항을 제안합니다:

  1. 모든 지원 가능한 동물 종류를 포함하거나
  2. 주석을 더 명확하게 작성하여 일부만 예시로 포함된 것임을 명시

또한 정적 맵의 크기가 큰 경우 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: 헬퍼 메소드 개선 가능성

코드 중복을 줄이고 유지보수성을 높이기 위해 다음 개선사항을 제안합니다:

  1. isBlank 메소드는 Java 11+ 에서 제공하는 String.isBlank()를 사용하거나, Apache Commons Lang의 StringUtils.isBlank()를 사용
  2. 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.generatetogether.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

📥 Commits

Reviewing files that changed from the base of the PR and between adcf207 and 7d49ee1.

📒 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: 내부 클래스 구조가 적절합니다.

응답 구조에 맞게 ChoiceMessage 내부 클래스가 잘 구성되어 있습니다. 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 메서드는 텍스트 블록을 사용하여 가독성 높은 프롬프트를 생성하고 있습니다. 프롬프트가 구조화되어 있고 반려동물에 대한 중요한 정보를 포함하고 있어 좋습니다.

Comment on lines +20 to +28
@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, 그대로 반환
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
@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);
}
}

Comment on lines +24 to +38
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(),"알 수 없음");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

detect 메서드 개선 가능성

detect 메서드에 다음과 같은 개선사항을 제안합니다:

  1. 입력 파라미터 유효성 검사 추가
  2. 스트림 API를 일관되게 사용하여 가독성 향상
  3. 로깅 추가로 디버깅 및 모니터링 용이성 확보
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.

Suggested change
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(), "알 수 없음");
}

Comment on lines +42 to +52
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

예외 처리 및 로깅 개선 권장

현재 코드는 모든 오류 상황에서 IllegalStateException을 사용하고 있습니다. 예외 처리를 더 명확하게 하고, 국제화 측면에서도 개선할 수 있습니다:

  1. 구체적인 예외 클래스 사용
  2. 로그 메시지의 언어 일관성 유지 (영어 또는 한글)
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를 활용하는 것도 고려해볼 수 있습니다.

Comment on lines +40 to +47
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());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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();

Comment on lines +17 to +21
public String plainText(){
if(candidates==null||candidates.isEmpty()) return "";
return candidates.get(0).content.parts.stream()
.map(Part::text).reduce("", String::concat);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

NullPointerException 방지를 위한 추가 검증 필요

현재 plainText() 메서드는 candidates가 null이거나 비어있는지만 확인하고 있습니다. 그러나 contentparts가 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.

Suggested change
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);
}

Comment on lines +19 to +30
@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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +1 to +51
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

전반적인 코드 품질과 구조에 대한 평가

이 클래스는 Clean Architecture 원칙을 잘 따르고 있으며, 프롬프트 생성과 요청 객체 구성이라는 단일 책임에 집중하고 있습니다. 코드가 명확하게 구성되어 있고 주석을 통해 각 섹션의 목적이 분명히 드러납니다.

다만, 몇 가지 개선할 점이 있습니다:

  1. 입력 매개변수 유효성 검증 추가
  2. Together 요청 시 종(species) 정보 활용 여부 확인
  3. 중복 코드(Base64 인코딩) 제거
  4. 하드코딩된 문자열 상수화

이러한 개선 사항을 반영하면 더욱 견고하고 유지보수하기 쉬운 코드가 될 것입니다.

Comment on lines +24 to +31
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));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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));
}

Comment on lines +23 to +44
/* ---------- 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));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

공통 로직 중복에 대한 리팩토링이 필요합니다.

toGeminiReqtoTogetherReq 메서드 모두 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.

Suggested change
/* ---------- 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));
}

Comment on lines +34 to +44
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));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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));
}

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Summary by CodeRabbit

  • 신규 기능

    • 반려동물 이미지를 업로드하여 종(종류) 감지 및 분석 리포트를 제공하는 비전(이미지 분석) 기능이 추가되었습니다.
    • 이미지 업로드 페이지 및 결과 조회 엔드포인트가 제공됩니다.
    • AI 기반 분석(종 감지, 상세 리포트 생성) 기능이 포함되어 있습니다.
  • 버그 수정

    • 해당 없음.
  • 기타

    • 설정 파일 및 내부 구조가 개선되어 외부 AI 서비스 연동이 강화되었습니다.

Walkthrough

이번 변경 사항은 반려동물 이미지 분석 및 리포트 생성을 위한 Vision 모듈의 구조를 대대적으로 리팩토링하고, 여러 외부 AI 서비스(Gemini, Together, AWS Rekognition)와의 연동을 위한 포트 및 어댑터, DTO, 헬퍼, 서비스 계층을 도입하는 내용입니다. 기존 VisionService 및 컨트롤러 구현을 삭제하고, 이를 대체하는 새로운 서비스 구현체, 컨트롤러, 포트/어댑터, DTO, 설정 클래스, 헬퍼 클래스 등이 추가되었습니다. 각 계층은 명확하게 역할이 분리되어 있으며, 외부 API 실패 시 대체 경로로 폴백하는 로직이 포함되어 있습니다.

Changes

파일/경로 요약 변경 요약
.gitignore "Vision test API" 섹션을 "dev"로 변경, credentials.json 제외, "Vision" 섹션 및 application-vision-only.yml 추가
src/main/java/io/github/petty/vision/adapter/in/VisionController.java
src/main/java/io/github/petty/vision/controller/VisionController.java
새로운 VisionController 추가(패키지 이동 및 리팩토링), 기존 컨트롤러 삭제. GET/POST 엔드포인트 제공, VisionUseCase에 위임
src/main/java/io/github/petty/vision/adapter/out/GeminiClientImpl.java
.../out/RekognitionClientImpl.java
.../out/TogetherClientImpl.java
Gemini, Together, Rekognition 외부 연동 어댑터 신규 추가. 각각 포트 인터페이스 구현, 외부 API 호출 및 예외 처리
src/main/java/io/github/petty/vision/config/VisionProperties.java Vision 관련 외부 서비스 설정을 위한 프로퍼티 클래스 및 내부 static 클래스(Aws, Gemini, Together) 추가
src/main/java/io/github/petty/vision/dto/gemini/GeminiRequest.java
.../dto/gemini/GeminiResponse.java
Gemini API 연동용 요청/응답 DTO(Record) 신규 추가, 응답 파싱 및 plainText 추출 메소드 포함
src/main/java/io/github/petty/vision/dto/together/TogetherRequest.java
.../dto/together/TogetherResponse.java
Together API 연동용 요청/응답 DTO 추가, plainText 추출 및 원본 JsonNode 반환 메소드 포함
src/main/java/io/github/petty/vision/helper/PromptFactory.java
.../helper/SpeciesDetector.java
프롬프트 생성 및 이미지 기반 종 판별 헬퍼 클래스 추가. 이미지 Base64 인코딩, 프롬프트 포맷팅, 종 매핑 등 담당
src/main/java/io/github/petty/vision/port/in/VisionUseCase.java Vision 도메인 유즈케이스 인터페이스 신설(분석 및 임시 메시지 메소드)
src/main/java/io/github/petty/vision/port/out/GeminiPort.java
.../port/out/RekognitionPort.java
.../port/out/TogetherPort.java
Gemini, Rekognition, Together 외부 연동 포트 인터페이스 신설
src/main/java/io/github/petty/vision/service/VisionService.java 기존 VisionService 클래스 전체 삭제(종 판별, 리포트 생성, 외부 API 연동 및 폴백 로직 포함)
src/main/java/io/github/petty/vision/service/VisionServiceImpl.java VisionUseCase 구현체 신규 추가. 종 판별, 프롬프트 생성, Gemini/Together API 호출 및 폴백, 예외 처리 로직 구현

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: 결과 응답
Loading

Poem

(∩❛ڡ❛∩) 🐰

새 옷 입은 비전 토끼,
서비스 분리, 포트도 찰칵!
제미니, 투게더, AWS 친구와
프롬프트 공장, 종도 척척
실패해도 걱정 마,
폴백 타고 다시 달려!
오늘도 똑똑해진 토끼,
반려동물 리포트는 내게 맡겨!

🥕


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🔭 Outside diff range comments (1)
src/main/java/io/github/petty/vision/port/out/RekognitionPort.java (1)

1-9: 🛠️ Refactor suggestion

AWS 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 메서드에 다음과 같은 개선사항을 제안합니다:

  1. 입력 파라미터 유효성 검사 추가
  2. 스트림 API를 일관되게 사용하여 가독성 향상
  3. 로깅 추가로 디버깅 및 모니터링 용이성 확보
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을 사용하고 있습니다. 예외 처리를 더 명확하게 하고, 국제화 측면에서도 개선할 수 있습니다:

  1. 구체적인 예외 클래스 사용
  2. 로그 메시지의 언어 일관성 유지 (영어 또는 한글)
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이거나 비어있는지만 확인하고 있습니다. 그러나 contentparts가 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 suggestion

analyze 메서드의 예외 처리 일관성이 필요합니다.

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 인코딩 중복 코드 리팩토링이 필요합니다.

toGeminiReqtoTogetherReq 메서드 모두 동일한 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 suggestion

Gemini 요청 생성 시 입력 유효성 검증이 필요합니다.

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

📥 Commits

Reviewing files that changed from the base of the PR and between adcf207 and 7d49ee1.

📒 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 메서드는 텍스트 블록을 활용하여 반려동물 분석 보고서에 필요한 항목들을 명확하게 정의하고 있습니다. 프롬프트 구조가 체계적으로 잘 설계되어 있습니다.

Comment thread .gitignore
Comment on lines +39 to +46
### dev ###
application-dev.yml
credentials.json

### secret ###
application-secret.yml

### Vision ###
application-vision-only.yml No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

민감한 파일 패턴이 누락되었습니다.

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.

Suggested change
### 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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) {}

Comment on lines +6 to +8
public interface TogetherPort {
TogetherResponse generate(TogetherRequest req);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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);
 }

Comment on lines +6 to +17
/**
* 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
) {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
/**
* 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
) {}

Comment on lines +16 to +22
/** 영어 → 한글 매핑표 (생략) */
private static final Map<String,String> MAP = Map.of(
"cat","고양이","kitten","고양이",
"dog","개","puppy","개",
"rabbit","토끼", "hamster","햄스터"
/* … */
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
/** 영어 → 한글 매핑표 (생략) */
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", "기니피그");
// 필요한 다른 동물 종 추가
}};

Comment on lines +29 to +32
String url = UriComponentsBuilder
.fromHttpUrl(prop.getGemini().getUrl())
.queryParam("key", prop.getGemini().getKey())
.toUriString();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

API 키를 URL 쿼리 파라미터로 전송하는 보안 위험이 있습니다.

현재 구현에서는 Gemini API 키를 URL 쿼리 파라미터로 전송하고 있습니다. 이는 다음과 같은 보안 위험을 초래할 수 있습니다:

  1. 서버 로그에 API 키가 평문으로 기록될 수 있습니다.
  2. 프록시 서버나 중간자가 URL을 볼 수 있습니다.
  3. 브라우저 히스토리나 캐시에 저장될 수 있습니다.

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;
}

Comment on lines +12 to +23
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; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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;
}
}

Comment on lines +23 to +27
public String interim(byte[] image, String petName) {
String species = detector.detect(image);
return prompt.interimMsg(petName, species);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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);
}

Comment on lines +30 to +36
byte[] img;
try {
img = file.getBytes();
} catch (Exception e) {
throw new IllegalStateException("이미지를 읽을 수 없습니다", e);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

대용량 파일·I/O 예외 처리 보강

  1. file.getBytes()는 파일 크기만큼 메모리를 한 번에 할당합니다.
    예상보다 큰 이미지가 업로드되면 OOM(Out Of Memory) 위험이 있습니다.

    • 파일 크기를 VisionProperties 등으로 제한하거나
    • 스트림 처리로 변경하는 방안을 검토해 주세요.
  2. 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.

Suggested change
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);
}

@Juyoung8563
Copy link
Copy Markdown
Contributor

리뷰

  • 아키텍쳐 적용으로 책임 분할을 하여 호환성을 보완한 점 인상 깊었습니다.
  • 클래스, 메서드 네이밍을 직관적으로 이해하기 쉽도록 세심한 리팩터링 해주셔서 감사합니다.

제안

  • 다소 복잡한 if문을 조금만 수정하면 좋을 것 같습니다!

좋은 코드 작성해주셔서 감사합니다. 고생 많으셨습니다.

@s0ooo0k s0ooo0k merged commit a6c41c9 into PETTY-HUB:main Apr 24, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants