Skip to content

Conversation

@dahyun24
Copy link
Contributor

@dahyun24 dahyun24 commented Feb 2, 2026

🔗 연관된 이슈

🚀 변경 유형

  • ✨ 기능 추가 (feature)
  • 🐛 버그 수정 (fix)
  • 📝 문서 변경 (docs)
  • ♻️ 리팩토링 (refactor)
  • 🧪 테스트 추가 / 수정 (test)
  • ⚙️ 설정 변경 (chore)

📝 작업 내용

  • clove studio를 이용한 치즈네컷 AI 기능 추가
  1. 수동 치즈네컷 확정
  2. 앨범 만료 후 치즈네컷 자동 생성

-> 이 두 경우에 대하여 비동기적으로 AI 를 이용하여 이미지 요약 기능 구현

  1. 프론트에서 AI 생성 완료 여부를 알기 위한 Polling API 구현

📸 스크린샷

수동 확정

스크린샷 2026-02-03 오전 1 53 35

polling API

스크린샷 2026-02-03 오전 1 22 40

로그 확인

스크린샷 2026-02-03 오전 1 26 55

💬 리뷰 요구사항

📜 리뷰 규칙

Reviewer는 아래 P5 Rule을 참고하여 리뷰를 진행합니다.
P5 Rule을 통해 Reviewer는 Reviewee에게 리뷰의 의도를 보다 정확히 전달할 수 있습니다.

  • P1: 꼭 반영해주세요 (Comment)
  • P2: 적극적으로 고려해주세요 (Comment)
  • P3: 웬만하면 반영해 주세요 (Comment)
  • P4: 반영해도 좋고 넘어가도 좋습니다 (Approve)
  • P5: 그냥 사소한 의견입니다 (Approve)

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • AI 기반 사진 앨범 요약 생성 기능 추가
    • 앨범 AI 요약 조회 엔드포인트 추가
    • 비동기 처리를 통한 AI 기능 구현
    • 사진 썸네일 자동 리사이징 기능 추가
  • Chores

    • 이미지 처리 라이브러리 의존성 추가

@dahyun24 dahyun24 self-assigned this Feb 2, 2026
@dahyun24 dahyun24 added the ✨feature New feature or request label Feb 2, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

Walkthrough

이 PR은 Clova AI API를 통한 AI 기반 치즈네컷 이미지 분석 기능을 추가합니다. 비동기 이벤트 기반 아키텍처로 구현되어 치즈네컷 생성 시 자동으로 AI 처리를 트리거하며, 이미지 리사이징 및 요약 생성 기능을 포함합니다.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
build.gradle, src/main/java/com/cheeeese/CheeeeseApplication.java, src/main/java/com/cheeeese/global/config/AsyncConfig.java
Thumbnailator 이미지 리사이징 라이브러리 추가, @EnableAsync 애노테이션으로 비동기 처리 활성화, ThreadPoolTaskExecutor를 이용한 cheeseAsyncExecutor 빈 설정
Event-Driven Architecture
src/main/java/com/cheeeese/cheese4cut/application/Cheese4cutFinalizedEvent.java, src/main/java/com/cheeeese/cheese4cut/application/Cheese4cutAiEventListener.java
Cheese4cut 생성 완료 시 발행할 불변 이벤트 레코드 생성, @Async@TransactionalEventListener로 비동기 이벤트 처리 수행
AI Service & Infrastructure
src/main/java/com/cheeeese/cheese4cut/infrastructure/ClovaClient.java, src/main/java/com/cheeeese/cheese4cut/application/Cheese4cutAiService.java
Clova HCX-005(이미지 분석) 및 HCX-DASH-002(요약 생성) API 호출 클라이언트 구현, 이미지 인코딩 및 AI 결과 집계 로직 포함 서비스 계층
Domain Model & Persistence
src/main/java/com/cheeeese/cheese4cut/domain/Cheese4cutAiSummary.java, src/main/java/com/cheeeese/cheese4cut/infrastructure/persistence/Cheese4cutAiSummaryRepository.java
AI 생성 요약을 저장할 JPA 엔티티(cheese4cut_ai_summary 테이블) 추가, Cheese4cut과 1:1 관계 설정
Response DTOs
src/main/java/com/cheeeese/cheese4cut/dto/response/AiResult.java, src/main/java/com/cheeeese/cheese4cut/dto/response/Cheese4cutAiResponse.java
AI 결과 데이터 캐리어 레코드 및 PROCESSING/COMPLETED 상태를 포함한 응답 레코드 추가
Application Service Updates
src/main/java/com/cheeeese/album/application/AlbumExpirationService.java, src/main/java/com/cheeeese/cheese4cut/application/Cheese4cutService.java
Cheese4cut 생성 시 Cheese4cutFinalizedEvent 발행으로 AI 처리 트리거, finalizeCheese4cutWithAi 메서드 추가
Presentation Layer
src/main/java/com/cheeeese/cheese4cut/presentation/Cheese4cutController.java, src/main/java/com/cheeeese/cheese4cut/presentation/swagger/Cheese4cutSwagger.java
AI 기반 치즈네컷 완성(POST /fixed/ai) 및 AI 요약 조회(GET /ai-summary) 엔드포인트 추가
Error & Success Codes
src/main/java/com/cheeeese/global/common/code/ErrorCode.java, src/main/java/com/cheeeese/global/common/code/SuccessCode.java, src/main/java/com/cheeeese/photo/exception/code/PhotoErrorCode.java
CLOVA_API_ERROR, CLOVA_RESPONSE_EMPTY 에러 코드 및 CHEESE4CUT_AI_GET_SUCCESS 성공 코드, IMAGE_PROCESSING_FAILED 에러 코드 추가
Utilities
src/main/java/com/cheeeese/global/util/ImageUtil.java
URL로부터 이미지를 1024x1024로 리사이징하고 Base64 인코딩하는 유틸리티 메서드 추가

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Controller as Cheese4cutController
    participant Service as Cheese4cutService
    participant EventPublisher as EventPublisher
    participant EventListener as Cheese4cutAiEventListener
    participant AiService as Cheese4cutAiService
    participant ClovaClient as ClovaClient
    participant Repository as Repositories

    Client->>Controller: POST /fixed/ai (cheese4cut 최종화 with AI)
    Controller->>Service: finalizeCheese4cutWithAi()
    Service->>Repository: save(cheese4cut)
    Repository-->>Service: cheese4cut
    Service->>EventPublisher: publishEvent(Cheese4cutFinalizedEvent)
    EventPublisher-->>Service: 완료 (즉시 응답)
    Service-->>Controller: 성공
    Controller-->>Client: 200 OK (AI 처리 중 상태)
    
    rect rgba(100, 150, 255, 0.5)
    Note over EventListener,Repository: 비동기 AI 처리 (별도 스레드)
    EventPublisher->>EventListener: Cheese4cutFinalizedEvent (AFTER_COMMIT)
    EventListener->>AiService: generateAiSummary()
    AiService->>Repository: photo 목록 조회
    loop 각 사진마다
        AiService->>ClovaClient: callHcx005(base64Image)
        ClovaClient-->>AiService: 이미지 분석 결과
    end
    AiService->>ClovaClient: callHcxDash002(combinedAnalysis)
    ClovaClient-->>AiService: AiResult(title, content)
    AiService->>Repository: save(Cheese4cutAiSummary)
    Repository-->>AiService: 저장 완료
    end

    Client->>Controller: GET /ai-summary?code=xxx
    Controller->>AiService: getAiSummary(code)
    AiService->>Repository: findByCheese4cutId()
    alt AI 요약이 완성된 경우
        Repository-->>AiService: Cheese4cutAiSummary
        AiService-->>Controller: Cheese4cutAiResponse(COMPLETED, title, content)
    else AI 처리 중
        Repository-->>AiService: 없음
        AiService-->>Controller: Cheese4cutAiResponse(PROCESSING, null, null)
    end
    Controller-->>Client: 200 OK with status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

✨ feature

Suggested reviewers

  • zyovn

Poem

🐰 치즈네컷에 AI의 마법이 내려앉고,
비동기 이벤트가 춤을 추며,
이미지는 리사이징되고 요약은 피어난다.
Clova와 함께 꿈꾸는 자동화의 세계,
한 번의 클릭으로 완성되는 지능형 분석! ✨📸

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경사항인 AI 기능 개발을 명확하게 요약하며, Clova를 사용한 Cheese4Cut AI 기능 추가라는 핵심 목표를 잘 반영합니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch CEEZ-24-AI-API-개발

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@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: 8

🤖 Fix all issues with AI agents
In `@build.gradle`:
- Around line 74-75: Update the Thumbnailator dependency to the newer 0.4.21
release by replacing the current artifact version used in the build.gradle
dependency declaration for 'net.coobird:thumbnailator' so the project pulls the
0.4.21 artifact with its bug fixes and memory optimizations.

In `@src/main/java/com/cheeeese/cheese4cut/application/Cheese4cutAiService.java`:
- Around line 62-66: The AI response lengths are not validated before building
Cheese4cutAiSummary, risking DB errors; in Cheese4cutAiService before calling
Cheese4cutAiSummary.builder() validate and normalize result.title() and
result.content(): trim whitespace, enforce title length <= 10 (truncate and log
or return error) and enforce content length between 180 and 220 characters (if
shorter, pad/mark as invalid or reject; if longer, truncate to 220 and log);
update the code around result.title() and result.content() to perform these
checks and only build/save the Cheese4cutAiSummary when the values meet the
constraints (or handle/record validation failures) so DB insert won't fail.
- Around line 72-74: The catch block in Cheese4cutAiService (where you log "AI
Summary 생성 중 치명적 오류 발생") swallows exceptions leaving the AI job permanently in
PROCESSING; update the exception handling to persist a failure state so
getAiSummary can return FAILED instead of forever PROCESSING: add a FAILED value
to Cheese4cutAiResponse (or the status enum/entity used), and in the catch of
the AI summary generation method in Cheese4cutAiService set and save the
entity/status to FAILED (or record retry metadata) — alternatively implement a
retry mechanism there — ensuring getAiSummary reads the persisted status and
returns FAILED when appropriate.

In `@src/main/java/com/cheeeese/cheese4cut/infrastructure/ClovaClient.java`:
- Around line 167-170: 현재 ClovaClient.java의 catch(Exception e) 블록이
BusinessException을 포함한 모든 예외를 잡아 ErrorCode.CLOVA_API_ERROR로 덮어써 원본 오류 정보를 잃습니다;
수정 방법은 BusinessException은 그대로 재던지거나 그대로 처리하도록 별도 catch로 분리하고(즉
catch(BusinessException be) { throw be; }) 나머지 일반 예외는 기존 log.error(...)에 원본 예외를
포함시키고(new BusinessException(ErrorCode.CLOVA_API_ERROR, e) 또는 생성자에 원인 전달) 원본 예외를
cause로 유지해 던지도록 변경하세요; 참조 심볼: ClovaClient 클래스의 catch 블록, BusinessException,
ErrorCode.CLOVA_API_ERROR, log.error.
- Around line 30-31: ClovaClient currently instantiates RestTemplate and
ObjectMapper inline, preventing configured timeouts/connection pooling; change
to inject them as Spring beans instead: create a configuration class that
defines a RestTemplate bean (eg. clovaRestTemplate built with
RestTemplateBuilder and setConnectTimeout/setReadTimeout to desired values) and
an ObjectMapper bean (or use Jackson2ObjectMapperBuilder), then modify the
ClovaClient class to accept RestTemplate and ObjectMapper via constructor
injection (replace the private final new RestTemplate()/new ObjectMapper()
fields with the injected instances) so external API calls use the configured
timeouts and pooling.
- Around line 126-129: In the ClovaClient class catch block that currently
returns new AiResult(title, response) on Exception, avoid saving the raw
unparsed response into AiResult (to prevent leaking malformed/unsafe data);
instead log the exception and the original response using the existing logger,
and return a safe fallback AiResult (e.g., new AiResult(title, "") or a
sanitized placeholder) or set a dedicated error field if AiResult supports it.
Update the catch in ClovaClient.java around the JSON parsing to call
logger.error(...) with the exception and raw response, and return the
safe/sanitized AiResult rather than the full raw response.

In `@src/main/java/com/cheeeese/global/common/code/ErrorCode.java`:
- Around line 17-19: The ErrorCode enum contains a duplicate semicolon after the
enum constant list (CLOVA_API_ERROR and CLOVA_RESPONSE_EMPTY); remove the extra
semicolon so there is only a single terminating semicolon (or none if there is
no following member/constructor) after the enum constants in the ErrorCode enum
to eliminate the redundant punctuation.

In `@src/main/java/com/cheeeese/global/util/ImageUtil.java`:
- Around line 16-32: The resizeAndEncodeToBase64FromUrl method currently opens a
stream via URI.create(...).toURL().openStream() without timeouts and doesn't
validate the imageUrl; update it to first validate imageUrl is non-null and
parsable (handle IllegalArgumentException/MalformedURLException), then open an
HttpURLConnection (or URLConnection) to the URI, set explicit connect and read
timeouts (e.g., reasonable ms values), get the InputStream from that connection
in a try-with-resources block, pass that stream to Thumbnails as before, and
wrap any IOException/IllegalArgumentException/MalformedURLException in
PhotoException(PhotoErrorCode.IMAGE_PROCESSING_FAILED) so all failure modes are
consistently handled; ensure the connection and stream are properly closed.
🧹 Nitpick comments (12)
src/main/java/com/cheeeese/cheese4cut/application/Cheese4cutFinalizedEvent.java (1)

9-13: 이벤트 데이터 구조가 적절합니다.

AI 처리에 필요한 Cheese4cut, Album, List<Photo> 정보를 포함한 불변 이벤트 record입니다.

선택적 개선사항: List<Photo>는 외부에서 수정 가능하므로, 방어적 복사(List.copyOf(photos))를 고려해볼 수 있습니다.

♻️ 방어적 복사 적용 예시
 public record Cheese4cutFinalizedEvent(
         Cheese4cut cheese4cut,
         Album album,
         List<Photo> photos
-) {}
+) {
+    public Cheese4cutFinalizedEvent {
+        photos = photos != null ? List.copyOf(photos) : List.of();
+    }
+}
src/main/java/com/cheeeese/global/config/AsyncConfig.java (1)

14-24: 비동기 Executor 설정에 거부 정책 및 종료 설정 추가를 권장합니다.

현재 설정에서 큐(50)가 가득 차고 최대 스레드(10)가 모두 사용 중일 때 새 작업은 기본적으로 AbortPolicy로 거부됩니다. AI 처리가 ~28초 소요되는 점을 감안하면, 거부 정책과 graceful shutdown 설정이 필요합니다.

♻️ 권장 설정 추가
     `@Bean`(name = "cheeseAsyncExecutor")
     public Executor cheeseAsyncExecutor() {
         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
         executor.setCorePoolSize(4);
         executor.setMaxPoolSize(10);
         executor.setQueueCapacity(50);
-
         executor.setThreadNamePrefix("AI-ASYNC-");
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        executor.setAwaitTerminationSeconds(60);
         executor.initialize();
         return executor;
     }

CallerRunsPolicy는 큐가 가득 찼을 때 호출자 스레드에서 작업을 실행하여 작업 손실을 방지합니다.

src/main/java/com/cheeeese/CheeeeseApplication.java (1)

10-10: @EnableAsync 중복 선언

AsyncConfig.java에 이미 @EnableAsync가 선언되어 있습니다. 둘 중 하나만 유지하는 것을 권장합니다. 일반적으로 설정 클래스(AsyncConfig)에 두는 것이 관례입니다.

♻️ 중복 제거
-@EnableAsync
 `@SpringBootApplication`
 `@EnableJpaAuditing`
 `@EnableRedisRepositories`(basePackages = "com.cheeeese.auth.infrastructure.persistence")
 `@EnableScheduling`
 public class CheeeeseApplication {
src/main/java/com/cheeeese/cheese4cut/dto/response/Cheese4cutAiResponse.java (1)

3-14: 상태 값에 enum 사용 및 Swagger 문서화 추가를 권장합니다.

  1. status가 String으로 선언되어 있어 타입 안전성이 부족합니다. enum으로 변경하면 유효하지 않은 상태 값을 컴파일 타임에 방지할 수 있습니다.

  2. 동일 패키지의 다른 Response DTO(Cheese4cutFinalResponse, Cheese4cutPreviewResponse)는 @Schema 어노테이션이 있으나, 이 클래스에는 누락되어 API 문서 일관성이 떨어집니다.

♻️ enum 적용 및 Schema 추가 예시
 package com.cheeeese.cheese4cut.dto.response;

+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "치즈네컷 AI 요약 응답 DTO")
 public record Cheese4cutAiResponse(
-        String status,
+        `@Schema`(description = "AI 처리 상태", example = "COMPLETED")
+        AiStatus status,
+        `@Schema`(description = "AI 생성 제목", example = "졸업식의 기쁨")
         String title,
+        `@Schema`(description = "AI 생성 내용")
         String content
 ) {
+    public enum AiStatus {
+        PROCESSING, COMPLETED
+    }
+
     public static Cheese4cutAiResponse processing() {
-        return new Cheese4cutAiResponse("PROCESSING", null, null);
+        return new Cheese4cutAiResponse(AiStatus.PROCESSING, null, null);
     }

     public static Cheese4cutAiResponse completed(String title, String content) {
-        return new Cheese4cutAiResponse("COMPLETED", title, content);
+        return new Cheese4cutAiResponse(AiStatus.COMPLETED, title, content);
     }
 }
src/main/java/com/cheeeese/album/application/AlbumExpirationService.java (1)

43-43: 사용되지 않는 의존성을 제거해주세요.

cheese4cutAiService 필드가 주입되었지만 이 클래스 내에서 사용되지 않습니다. 이벤트 기반 아키텍처로 전환되어 Cheese4cutAiEventListener가 AI 처리를 담당하므로, 이 필드는 불필요합니다.

♻️ 수정 제안
 import com.cheeeese.cheese4cut.application.Cheese4cutAiService;

위 import와 아래 필드를 제거하세요:

-    private final Cheese4cutAiService cheese4cutAiService;
src/main/java/com/cheeeese/cheese4cut/presentation/swagger/Cheese4cutSwagger.java (2)

125-142: API 설명이 AI 기능에 대한 내용을 포함하지 않습니다.

finalizeCheese4cutWithAi의 설명이 기존 finalizeCheese4cut과 동일하게 복사되어 있습니다. AI 처리가 비동기로 트리거된다는 핵심 차이점이 문서화되어 있지 않아 API 사용자가 혼동할 수 있습니다.

📝 설명 개선 제안
     `@Operation`(
             summary = "치즈네컷 수동 확정 API with AI",
             description = """
                     ### PathVariable
                     ---
                     `code`: 앨범 코드
                     
                     ### RequestBody
                     ---
                     `photoIds`: 사용자가 최종 선택한 4장의 사진 ID \n
                     
                     ### 로직 상세
                     ---
                     1. 사용자 권한 확인 (MAKER만 가능).
                     2. 앨범 만료 및 이미 확정 여부 확인.
                     3. 요청된 4장의 사진 ID가 모두 **COMPLETED 상태**이고 해당 앨범에 속하는지 검증.
                     4. `Cheese4cut` 레코드를 생성하고 저장.
+                    5. AI 요약 생성이 비동기로 시작됩니다. `/ai-summary` 엔드포인트로 결과를 폴링하세요.
                     """
     )

182-182: code 파라미터에 @NotBlank 검증이 누락되었습니다.

다른 엔드포인트(getCheese4cut, finalizeCheese4cut, finalizeCheese4cutWithAi)에서는 @PathVariable @notblank String code를 사용하지만, getAiSummary에서는 @NotBlank가 빠져 있어 일관성이 없습니다.

✏️ 수정 제안
-    CommonResponse<Cheese4cutAiResponse> getAiSummary(`@PathVariable` String code);
+    CommonResponse<Cheese4cutAiResponse> getAiSummary(`@PathVariable` `@NotBlank` String code);
src/main/java/com/cheeeese/cheese4cut/application/Cheese4cutService.java (2)

186-223: finalizeCheese4cut과 중복 코드가 많습니다.

finalizeCheese4cutWithAi의 검증 로직 및 저장 로직이 finalizeCheese4cut (Lines 127-151)과 거의 동일합니다. 이벤트 발행 여부만 다르므로 공통 로직을 추출하면 유지보수성이 향상됩니다.

♻️ 리팩토링 제안
+    private Cheese4cut doFinalizeCheese4cut(User user, String code, Cheese4cutFixedRequest request) {
+        Album album = albumValidator.validateAlbumCode(code);
+
+        if (album.isExpired()) {
+            throw new AlbumException(AlbumErrorCode.ALBUM_EXPIRED);
+        }
+
+        cheese4cutValidator.validateUserIsMaker(album, user);
+
+        if (cheese4cutRepository.findByAlbumId(album.getId()).isPresent()) {
+            throw new Cheese4cutException(Cheese4cutErrorCode.CHEESE4CUT_ALREADY_FINALIZED);
+        }
+
+        cheese4cutValidator.validateFinalizePhotos(album, request.photoIds());
+
+        List<Photo> orderedPhotos =
+                photoRepository.findAllByIdInOrderByLikesDescCreatedDesc(request.photoIds());
+
+        if (orderedPhotos.size() != request.photoIds().size()) {
+            throw new Cheese4cutException(Cheese4cutErrorCode.INSUFFICIENT_COUNT_FOR_CHEESE4CUT);
+        }
+
+        Cheese4cut cheese4cut = cheese4cutRepository.save(Cheese4cutMapper.toEntity(album, orderedPhotos));
+        cheese4cutLogger.logCheese4CutFinalized(user.getId(), request.photoIds(), album.getCode());
+        
+        return cheese4cut;
+    }
+
     `@Transactional`
     public void finalizeCheese4cut(User user, String code, Cheese4cutFixedRequest request) {
-        // ... 기존 로직 ...
+        doFinalizeCheese4cut(user, code, request);
     }
 
     `@Transactional`
     public void finalizeCheese4cutWithAi(User user, String code, Cheese4cutFixedRequest request) {
-        // ... 중복 로직 ...
+        Cheese4cut cheese4cut = doFinalizeCheese4cut(user, code, request);
+        
+        Album album = cheese4cut.getAlbum();
+        List<Photo> orderedPhotos = photoRepository.findAllByIdInOrderByLikesDescCreatedDesc(request.photoIds());
+        
+        eventPublisher.publishEvent(
+                new Cheese4cutFinalizedEvent(cheese4cut, album, orderedPhotos)
+        );
     }

217-219: 현재 코드에서는 lazy loading 문제가 발생하지 않지만, 향후 변경 시 주의 필요

Cheese4cutFinalizedEventorderedPhotos에는 Photo 엔티티의 lazy-loaded 관계(user, album)가 존재합니다. 하지만 현재 Cheese4cutAiService.generateAiSummary에서는 photo.getThumbnailUrl()만 접근하므로 lazy loading이 발생하지 않습니다. 또한 해당 메서드는 @Transactional(propagation = Propagation.REQUIRES_NEW)로 새로운 트랜잭션을 생성하므로, 설령 lazy 관계를 접근하더라도 트랜잭션 컨텍스트가 유지됩니다.

다만 향후 Photo의 다른 lazy-loaded 필드(user, album 등)에 접근하는 로직이 추가될 경우 주의가 필요합니다. 필요시 event listener에서 Photo 엔티티를 미리 초기화(Hibernate.initialize)하거나, 쿼리 단계에서 eager loading을 적용하는 것을 고려하세요.

src/main/java/com/cheeeese/cheese4cut/application/Cheese4cutAiService.java (1)

44-52: 외부 API 호출이 순차적으로 4회 발생합니다.

각 사진에 대해 clovaClient.callHcx005를 순차적으로 호출하여 총 ~28초(PR 설명의 로그 기준) 소요됩니다. 병렬 처리로 응답 시간을 개선할 수 있습니다.

⚡ 병렬 처리 제안
// CompletableFuture를 사용한 병렬 처리 예시
List<CompletableFuture<String>> futures = photos.stream()
        .map(photo -> CompletableFuture.supplyAsync(() -> {
            String absoluteUrl = cdnUrlResolver.resolveThumbnail(photo.getThumbnailUrl());
            String base64 = imageUtil.resizeAndEncodeToBase64FromUrl(absoluteUrl);
            return String.format("[사진 분석]\n%s\n", clovaClient.callHcx005(base64));
        }, asyncExecutor))
        .toList();

String combinedAnalysis = futures.stream()
        .map(CompletableFuture::join)
        .collect(Collectors.joining("\n"));
src/main/java/com/cheeeese/cheese4cut/presentation/Cheese4cutController.java (1)

64-64: code 파라미터에 @NotBlank 검증이 누락되었습니다.

Swagger 인터페이스와 마찬가지로 다른 엔드포인트와의 일관성을 위해 @NotBlank 어노테이션을 추가해주세요.

✏️ 수정 제안
-    public CommonResponse<Cheese4cutAiResponse> getAiSummary(`@PathVariable` String code) {
+    public CommonResponse<Cheese4cutAiResponse> getAiSummary(`@PathVariable` `@NotBlank` String code) {
src/main/java/com/cheeeese/cheese4cut/infrastructure/ClovaClient.java (1)

148-149: Raw type Map 사용으로 타입 안전성이 떨어집니다.

✏️ 타입 파라미터 추가
-            ResponseEntity<Map> resp =
-                    restTemplate.postForEntity(url, new HttpEntity<>(body, headers), Map.class);
+            `@SuppressWarnings`("unchecked")
+            ResponseEntity<Map<String, Object>> resp =
+                    restTemplate.postForEntity(url, new HttpEntity<>(body, headers), 
+                            (Class<Map<String, Object>>)(Class<?>)Map.class);

또는 Clova API 응답에 맞는 DTO 클래스를 정의하는 것이 더 좋습니다.

Comment on lines +16 to +32
public String resizeAndEncodeToBase64FromUrl(String imageUrl) {
try (InputStream is = URI.create(imageUrl).toURL().openStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

Thumbnails.of(is)
.size(1024, 1024)
.outputFormat("jpg")
.outputQuality(0.8)
.toOutputStream(outputStream);

byte[] imageBytes = outputStream.toByteArray();
String b64 = Base64.getEncoder().encodeToString(imageBytes);

return b64;
} catch (IOException e) {
throw new PhotoException(PhotoErrorCode.IMAGE_PROCESSING_FAILED);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

네트워크 요청에 타임아웃 설정이 필요합니다.

URI.create(imageUrl).toURL().openStream()은 연결 타임아웃이 없어 응답하지 않는 서버에 대해 무한 대기할 수 있습니다. 또한 imageUrl이 null이거나 잘못된 형식일 경우 IllegalArgumentException이 발생하여 PhotoException으로 래핑되지 않습니다.

🛡️ 타임아웃 및 입력 검증 추가 제안
+import java.net.HttpURLConnection;
+import java.net.URL;

 `@Component`
 public class ImageUtil {
+    private static final int CONNECTION_TIMEOUT_MS = 10_000;
+    private static final int READ_TIMEOUT_MS = 30_000;
+
     // URL로부터 이미지를 가져와 리사이징 및 Base64 인코딩
     public String resizeAndEncodeToBase64FromUrl(String imageUrl) {
-        try (InputStream is = URI.create(imageUrl).toURL().openStream();
+        if (imageUrl == null || imageUrl.isBlank()) {
+            throw new PhotoException(PhotoErrorCode.IMAGE_PROCESSING_FAILED);
+        }
+        try {
+            URL url = URI.create(imageUrl).toURL();
+            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+            conn.setConnectTimeout(CONNECTION_TIMEOUT_MS);
+            conn.setReadTimeout(READ_TIMEOUT_MS);
+            try (InputStream is = conn.getInputStream();
              ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
 
             Thumbnails.of(is)
                     .size(1024, 1024)
                     .outputFormat("jpg")
                     .outputQuality(0.8)
                     .toOutputStream(outputStream);
 
             byte[] imageBytes = outputStream.toByteArray();
-            String b64 = Base64.getEncoder().encodeToString(imageBytes);
-
-            return b64;
-        } catch (IOException e) {
+            return Base64.getEncoder().encodeToString(imageBytes);
+            }
+        } catch (IOException | IllegalArgumentException e) {
             throw new PhotoException(PhotoErrorCode.IMAGE_PROCESSING_FAILED);
         }
     }
 }
🤖 Prompt for AI Agents
In `@src/main/java/com/cheeeese/global/util/ImageUtil.java` around lines 16 - 32,
The resizeAndEncodeToBase64FromUrl method currently opens a stream via
URI.create(...).toURL().openStream() without timeouts and doesn't validate the
imageUrl; update it to first validate imageUrl is non-null and parsable (handle
IllegalArgumentException/MalformedURLException), then open an HttpURLConnection
(or URLConnection) to the URI, set explicit connect and read timeouts (e.g.,
reasonable ms values), get the InputStream from that connection in a
try-with-resources block, pass that stream to Thumbnails as before, and wrap any
IOException/IllegalArgumentException/MalformedURLException in
PhotoException(PhotoErrorCode.IMAGE_PROCESSING_FAILED) so all failure modes are
consistently handled; ensure the connection and stream are properly closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants