Skip to content

AI 유사도 검사 비동기 처리 구현#205

Merged
goalSetter09 merged 7 commits intodevelopfrom
modic_backend_202
Oct 12, 2025
Merged

AI 유사도 검사 비동기 처리 구현#205
goalSetter09 merged 7 commits intodevelopfrom
modic_backend_202

Conversation

@goalSetter09
Copy link
Collaborator

@goalSetter09 goalSetter09 commented Oct 12, 2025

개요

AI 파생 포스트 생성 시 원본과 파생 이미지의 유사도를 AI 서버에 요청하여 자동으로 판단하는 기능을 RabbitMQ 기반 비동기로 구현했습니다.

주요 변경사항

1. DTO 추가

  • SimilarityCheckRequestDto: AI 서버로 전송할 유사도 검사 요청
  • SimilarityCheckResponseDto: AI 서버로부터 받을 유사도 검사 응답

2. RabbitMQ 설정

  • 유사도 검사 요청/응답 큐, 익스체인지, 라우팅 키 추가
  • 기존 AI 이미지 생성 패턴과 동일한 구조 적용

3. 비동기 처리 서비스

  • AiSimilarityRequestService: RabbitMQ로 유사도 검사 요청 전송
  • 투표 상태를 IN_PROGRESS로 업데이트
  • 예외 발생 시 로깅 후 안전하게 종료

4. 응답 리스너

  • SimilarityCheckListener: AI 서버 응답 수신 및 처리
  • AI 판단 결과에 따라 가중치 추가 (APPROVE/DENY)
  • 실패 시 투표를 CANCELLED 상태로 변경
  • 투표 및 포스트 상태는 PENDING 유지 (사람 투표 대기)

5. 트랜잭션 동기화

  • TransactionSynchronization을 사용하여 트랜잭션 커밋 후 비동기 실행
  • DB 커밋 전 조회 시도로 인한 타이밍 이슈 해결

테스트

  • AiSimilarityRequestServiceTest: 요청 전송 및 실패 처리 테스트
  • SimilarityCheckListenerTest: 응답 처리 및 다양한 실패 시나리오 테스트
  • AiDerivedPostServiceTest: 통합 테스트 업데이트
  • 모든 단위 테스트 통과 (11/11)

Closes #202

Summary by CodeRabbit

  • New Features

    • AI 유사도 검사 통합: 파생 게시물 생성 직후 유사도 검사 요청을 자동 전송하고, 결과에 따라 투표 요약에 AI 판단과 가중치를 반영합니다.
    • 투표 상태 가시성 향상: 검사 요청 시 투표 상태를 진행(IN_PROGRESS)으로 업데이트합니다.
    • 비정상 응답 처리: 응답 누락 또는 유효하지 않은 응답 발생 시 해당 투표를 취소하여 일관성 유지.
  • Tests

    • 유사도 요청/응답 흐름에 대한 단위 및 통합 테스트 추가/확장(승인/거절/누락/예외 시나리오 포함).

- SimilarityCheckRequestDto: voteId, 원본/파생 이미지 경로 포함
- SimilarityCheckResponseDto: voteId, AI 판단 결과 포함
- 요청 큐, 익스체인지, 라우팅 키 추가
- 응답 큐, 익스체인지, 라우팅 키 추가
- 기존 AI 이미지 생성 패턴과 동일한 구조 적용
- 비동기로 RabbitMQ에 유사도 검사 요청 전송
- 투표 상태를 IN_PROGRESS로 업데이트
- 예외 발생 시 로깅 후 안전하게 종료 (비동기 메서드 특성)
- AI 서버 응답 수신 및 처리
- 판단 결과에 따라 승인/거부 가중치 추가
- 실패 시 투표를 CANCELLED 상태로 변경
- 투표 및 포스트 상태는 PENDING 유지 (사람 투표 대기)
- AiSimilarityRequestService 의존성 추가
- 원본 이미지 경로 조회 로직 추가
- TransactionSynchronization을 사용한 트랜잭션 커밋 후 비동기 실행
- 트랜잭션 타이밍 이슈 해결: DB 커밋 완료 후 비동기 메서드 호출
- AiSimilarityRequestServiceTest: 요청 전송, 실패 처리 테스트
- SimilarityCheckListenerTest: 응답 처리, 실패 시나리오 테스트
- AiDerivedPostServiceTest: 통합 테스트 업데이트
- 모든 테스트 케이스 통과 확인
@coderabbitai
Copy link

coderabbitai bot commented Oct 12, 2025

Walkthrough

RabbitMQ 토폴로지에 투표 유사도 요청/응답 큐·익스체인지·라우팅키를 추가하고, 유사도 요청/응답 DTO·AiSimilarityRequestService·SimilarityCheckListener를 신설했다. AiDerivedPostService는 커밋 후 트랜잭션 콜백으로 유사도 요청을 비동기로 발송하도록 연계되며 관련 단위·통합 테스트가 추가됐다.

Changes

Cohort / File(s) Change Summary
RabbitMQ 설정 (유사도 요청/응답)
src/main/java/.../common/amqp/config/RabbitMqConfig.java
유사도 요청/응답용 상수 추가 및 요청/응답 Queue, TopicExchange, Binding 빈 추가.
AI 유사도 요청 서비스
src/main/java/.../domain/vote/service/AiSimilarityRequestService.java
비동기 @async 메서드로 vote 조회, SimilarityCheckRequestDto 생성·MQ 전송, vote 상태를 IN_PROGRESS로 업데이트하는 서비스 추가.
AI 응답 리스너
src/main/java/.../domain/vote/listener/SimilarityCheckListener.java
RabbitListener로 SimilarityCheckResponseDto 처리: vote 및 summary 조회, VoteProperties.aiVoteWeight 적용해 summary 가중치/aiDecision 반영; 요약 누락/잘못된 응답 시 vote 취소 처리.
Vote DTOs
src/main/java/.../domain/vote/dto/SimilarityCheckRequestDto.java, src/main/java/.../domain/vote/dto/SimilarityCheckResponseDto.java
요청(record: voteId, originalImagePath, derivedImagePath) 및 응답(record: voteId, VoteDecision) DTO 추가.
파생 게시글 서비스 연계
src/main/java/.../domain/post/service/AiDerivedPostService.java
aiSimilarityRequestService 주입 및 트랜잭션 afterCommit 콜백에서 sendSimilarityCheckRequest 호출 추가; 원본/파생 이미지 경로 조회 흐름 조정.
테스트 - 파생 게시글 서비스
src/test/java/.../domain/post/service/AiDerivedPostServiceTest.java
aiSimilarityRequestService 목 주입 및 afterCommit 시점 모킹, 원본/파생 이미지 경로와 유사도 요청 호출 검증 추가.
테스트 - 응답 리스너
src/test/java/.../domain/vote/listener/SimilarityCheckListenerTest.java
APPROVE/DENY 처리, vote 미존재/summary 미존재, 잘못된 결정 시 취소 등 시나리오 검증 테스트 추가.
테스트 - 요청 서비스
src/test/java/.../domain/vote/service/AiSimilarityRequestServiceTest.java
RabbitTemplate 호출 검증, vote 미존재/예외 시 전송·저장 미발생 검증 테스트 추가.
테스트 - 통합(파생 컨트롤러)
src/test/java/.../web/post/controller/AiDerivedPostControllerIntegrationTest.java
원본 PostImage 엔티티 생성 후 PostEntity.thumbnailImageId를 실제 이미지 ID로 갱신해 저장하도록 통합 테스트 수정.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client as 클라이언트
    participant PostSvc as AiDerivedPostService
    participant DB as Repositories
    participant TX as Tx(afterCommit)
    participant ReqSvc as AiSimilarityRequestService
    participant MQ as RabbitMQ
    participant AISrv as AI 서버
    participant Listener as SimilarityCheckListener

    Client->>PostSvc: createAiDerivedPost(...)
    PostSvc->>DB: 원본/파생 엔티티 생성 및 저장
    PostSvc->>TX: afterCommit 등록 (유사도 요청)
    Note right of TX: 트랜잭션 커밋 후 실행
    TX->>ReqSvc: sendSimilarityCheckRequest(voteId, origPath, derPath)
    ReqSvc->>DB: SimilarityVote 조회 및 상태 IN_PROGRESS 저장
    ReqSvc->>MQ: publish SimilarityCheckRequestDto (exchange/routingKey)
    MQ-->>AISrv: 요청 전달
    AISrv-->>MQ: SimilarityCheckResponseDto(결정)
    MQ-->>Listener: 메시지 수신
    Listener->>DB: vote / summary 조회
    alt decision == APPROVE or DENY
        Listener->>DB: summary 가중치 추가, aiDecision 설정, summary 저장
    else invalid/missing
        Listener->>DB: vote 상태 CANCELLED 저장
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • yooooonshine

Poem

깡충, 커밋 뒤에 약속 보냈지 🐇
원본과 파생, 큐 위에 손편지 한 장
토픽 건너 답이 오면 가중치 톡톡
표들은 조용히 기록되고
토끼는 다시 메시지 밭을 달린다 🎋

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning PR 설명은 '## 개요'와 변경사항, 테스트 항목을 상세히 기술하고 있으나 저장소 템플릿에 명시된 '## 작업사항' 섹션이 누락되어 있어 템플릿 구조와 일치하지 않습니다. 템플릿에 맞게 '## 작업사항' 섹션을 추가하거나 '## 주요 변경사항' 헤더를 '## 작업사항'으로 변경해 주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed 제목이 RabbitMQ 기반 비동기 AI 유사도 검사 기능 구현이라는 주요 변경사항을 명확하고 간결하게 표현하여 PR의 핵심 내용을 잘 요약하고 있습니다.
Linked Issues Check ✅ Passed 본 PR은 RabbitMQ 설정, DTO 정의, AiSimilarityRequestService, SimilarityCheckListener, AiDerivedPostService 연동 및 관련 단위·통합 테스트 구현 등 이슈 #202의 주요 코딩 요구사항을 모두 충족하고 있어 연결된 이슈의 목표와 일치합니다.
Out of Scope Changes Check ✅ Passed PR에 포함된 모든 변경사항이 AI 유사도 검사 기능 구현과 관련된 항목으로, 범위를 벗어난 불필요한 수정은 없습니다.
✨ 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 modic_backend_202

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: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d71e2d and 33f27db.

📒 Files selected for processing (9)
  • src/main/java/hanium/modic/backend/common/amqp/config/RabbitMqConfig.java (2 hunks)
  • src/main/java/hanium/modic/backend/domain/post/service/AiDerivedPostService.java (4 hunks)
  • src/main/java/hanium/modic/backend/domain/vote/dto/SimilarityCheckRequestDto.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/vote/dto/SimilarityCheckResponseDto.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/vote/listener/SimilarityCheckListener.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestService.java (1 hunks)
  • src/test/java/hanium/modic/backend/domain/post/service/AiDerivedPostServiceTest.java (3 hunks)
  • src/test/java/hanium/modic/backend/domain/vote/listener/SimilarityCheckListenerTest.java (1 hunks)
  • src/test/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestServiceTest.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/test/java/hanium/modic/backend/domain/vote/listener/SimilarityCheckListenerTest.java (2)
src/test/java/hanium/modic/backend/domain/post/service/AiDerivedPostServiceTest.java (1)
  • ExtendWith (39-215)
src/test/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestServiceTest.java (1)
  • ExtendWith (24-113)
src/test/java/hanium/modic/backend/domain/post/service/AiDerivedPostServiceTest.java (1)
src/test/java/hanium/modic/backend/domain/user/factory/UserFactory.java (1)
  • UserFactory (9-29)
src/test/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestServiceTest.java (1)
src/test/java/hanium/modic/backend/domain/post/service/AiDerivedPostServiceTest.java (1)
  • ExtendWith (39-215)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Comment on lines +77 to 84
PostEntity originalPost = postEntityRepository.findById(createdAiImage.getPostId())
.orElseThrow(() -> new AppException(ErrorCode.POST_NOT_FOUND_EXCEPTION));
Long originalImageId = originalPost.getThumbnailImageId();

// 원본 이미지 경로 조회
PostImageEntity originalImage = postImageEntityRepository.findById(originalImageId)
.orElseThrow(() -> new AppException(ErrorCode.IMAGE_NOT_FOUND_EXCEPTION));

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

원본 썸네일 ID null 처리 누락

originalPost.getThumbnailImageId()가 null일 수 있는 경우(예: 썸네일 미설정 포스트) findById(null) 호출로 InvalidDataAccessApiUsageException이 바로 발생합니다. 명시적으로 null 여부를 검증해 도메인 예외를 던지거나 기본 이미지를 처리해야 합니다.

다음과 같이 null 검사를 추가해 주세요:

-		Long originalImageId = originalPost.getThumbnailImageId();
-
-		// 원본 이미지 경로 조회
-		PostImageEntity originalImage = postImageEntityRepository.findById(originalImageId)
+		Long originalImageId = originalPost.getThumbnailImageId();
+		if (originalImageId == null) {
+			throw new AppException(ErrorCode.IMAGE_NOT_FOUND_EXCEPTION);
+		}
+
+		// 원본 이미지 경로 조회
+		PostImageEntity originalImage = postImageEntityRepository.findById(originalImageId)
 			.orElseThrow(() -> new AppException(ErrorCode.IMAGE_NOT_FOUND_EXCEPTION));
📝 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
PostEntity originalPost = postEntityRepository.findById(createdAiImage.getPostId())
.orElseThrow(() -> new AppException(ErrorCode.POST_NOT_FOUND_EXCEPTION));
Long originalImageId = originalPost.getThumbnailImageId();
// 원본 이미지 경로 조회
PostImageEntity originalImage = postImageEntityRepository.findById(originalImageId)
.orElseThrow(() -> new AppException(ErrorCode.IMAGE_NOT_FOUND_EXCEPTION));
PostEntity originalPost = postEntityRepository.findById(createdAiImage.getPostId())
.orElseThrow(() -> new AppException(ErrorCode.POST_NOT_FOUND_EXCEPTION));
Long originalImageId = originalPost.getThumbnailImageId();
if (originalImageId == null) {
throw new AppException(ErrorCode.IMAGE_NOT_FOUND_EXCEPTION);
}
// 원본 이미지 경로 조회
PostImageEntity originalImage = postImageEntityRepository.findById(originalImageId)
.orElseThrow(() -> new AppException(ErrorCode.IMAGE_NOT_FOUND_EXCEPTION));
🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/domain/post/service/AiDerivedPostService.java
around lines 77 to 84, originalPost.getThumbnailImageId() may return null which
causes findById(null) to throw InvalidDataAccessApiUsageException; add an
explicit null check after retrieving originalPost and before calling
postImageEntityRepository.findById: if thumbnailImageId is null either throw a
domain AppException (e.g., ErrorCode.IMAGE_NOT_FOUND_EXCEPTION) or resolve a
default image id/path according to product rules, then proceed to load the image
entity only when the id is non-null.

Comment on lines +41 to +45
Optional<SimilarityVoteEntity> voteOpt = similarityVoteRepository.findById(voteId);
if (voteOpt.isEmpty()) {
log.error("[유사도 검사 요청 실패] 투표를 찾을 수 없습니다. voteId={}", voteId);
return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

트랜잭션 커밋 전에 조회되어 요청이 바로 무시됩니다

@Async 덕분에 이 메서드는 호출 직후 별도 스레드에서 실행되는데, 호출자는 (예: AiDerivedPostService#createAiDerivedPost) 동일 트랜잭션 안에서 SimilarityVoteEntity와 요약을 Persist 한 직후 여기로 위임합니다. 호출자의 트랜잭션이 아직 커밋되지 않은 상태에서 Line 41의 findById가 실행되면 대부분 Optional.empty()가 돌아오고, 현재 구현은 단순 로그 후 return 하기 때문에 큐로 아무 메시지도 나가지 않습니다. 결과적으로 AI 유사도 검사가 전혀 수행되지 않는 치명적인 경로입니다.
커밋 이후에만 이 로직이 돌도록 보장해 주세요. 예컨대 호출부에서 TransactionSynchronizationManager.registerSynchronization(... afterCommit ...)로 이 메서드를 스케줄하거나, 여기서 직접 TransactionSynchronization/@TransactionalEventListener(phase = AFTER_COMMIT)을 활용해 커밋 완료 이후에 메시지를 발행하도록 구조를 바꿔야 합니다. 그 과정에서 상태 업데이트도 커밋 전에 끝내고, 발행은 afterCommit 블록 안에서 수행하도록 옮기면 됩니다.

🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestService.java
around lines 41-45, the async method queries the vote before the caller's
transaction may have committed and returns when findById yields empty; change
the flow so publication happens only after the creating transaction commits.
Keep any state updates and persistence in the original transaction, and move the
message/publication logic into an after-commit hook — either by having the
caller register TransactionSynchronizationManager.registerSynchronization(...
afterCommit ...) to invoke this class, or by refactoring this service to emit an
event handled with @TransactionalEventListener(phase =
TransactionPhase.AFTER_COMMIT); ensure the after-commit handler re-fetches the
entity (or uses an ID) and publishes the AI similarity request, while preserving
existing error/logging behavior.

- AiDerivedPostServiceTest에 TransactionSynchronizationManager 모킹 추가
- MockedStatic을 사용하여 트랜잭션 커밋 후 콜백 즉시 실행 처리
- AiDerivedPostControllerIntegrationTest 통합 테스트 수정
- PostEntity 저장 전 PostImageEntity 생성하여 thumbnailImageId 제약조건 해결
- 원본 이미지 경로 조회를 위한 테스트 데이터 구조 개선

Co-Authored-By: Claude <noreply@anthropic.com>
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: 0

🧹 Nitpick comments (1)
src/test/java/hanium/modic/backend/domain/post/service/AiDerivedPostServiceTest.java (1)

164-168: AI 유사도 요청 검증 강화 필요

anyLong()으로 voteId를 허용하면 서비스가 잘못된 ID를 넘겨도 테스트가 통과하므로 회귀를 잡아내지 못합니다. eq(mockSavedVote.getId()) 또는 ArgumentCaptor를 사용해 저장된 투표 ID가 그대로 전달되는지 검증하도록 좁혀 주세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33f27db and a1700a3.

📒 Files selected for processing (2)
  • src/test/java/hanium/modic/backend/domain/post/service/AiDerivedPostServiceTest.java (2 hunks)
  • src/test/java/hanium/modic/backend/web/post/controller/AiDerivedPostControllerIntegrationTest.java (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/test/java/hanium/modic/backend/domain/post/service/AiDerivedPostServiceTest.java (2)
src/test/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestServiceTest.java (1)
  • ExtendWith (24-113)
src/test/java/hanium/modic/backend/domain/user/factory/UserFactory.java (1)
  • UserFactory (9-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

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.

AI 판단 요청 및 처리

1 participant