Skip to content

[FIX] 비동기로 인해, ChatMessage가 두 번 저장하려하면서 락 장애 발생하는 오류 수정#263

Merged
yooooonshine merged 1 commit intodevelopfrom
feature/261-fix-ai-chat
Nov 16, 2025
Merged

[FIX] 비동기로 인해, ChatMessage가 두 번 저장하려하면서 락 장애 발생하는 오류 수정#263
yooooonshine merged 1 commit intodevelopfrom
feature/261-fix-ai-chat

Conversation

@yooooonshine
Copy link
Contributor

@yooooonshine yooooonshine commented Nov 16, 2025

개요

작업사항

image 비동기 메서드 외부에서 데이터를 저장하고, 비동기 내부에서 값을 변경하려고했다. 하지만 비동기 메서드여서 단순 update메서드로 변경감지가 이뤄지지 않았고, 이에 save를 추가했을 때 새로운데이터로 인식해 두 번 저장하려다 장애가 발생했다.

이를 비동기 메서드에 엔티티를 직접적으로 넘기지 않고, 다시 조회하게끔하여 해결했다.

Summary by CodeRabbit

릴리스 노트

  • 개선 사항

    • AI 채팅 이미지 처리 로직을 최적화하여 성능을 개선했습니다.
    • 이미지 소유권 검증 프로세스를 단순화하여 처리 속도를 향상시켰습니다.
  • 버그 수정

    • AI 이미지 처리 시 소유권 검증의 안정성을 강화했습니다.

@coderabbitai
Copy link

coderabbitai bot commented Nov 16, 2025

Walkthrough

AI 채팅 요청 처리 흐름을 ID 기반으로 단순화. AiChatMessageService에서 단일 이미지 ID 소유권 검증으로 변경하고, AiServerService의 processAiRequest 메서드 시그니처를 엔터티 목록 대신 messageId와 aiChatImageId를 받도록 수정. AiServerService 내부에서 필요한 엔터티를 직접 조회하는 방식으로 변경.

Changes

Cohort / File(s) 변경 요약
소유권 검증 단순화
src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiChatMessageService.java
메서드 validateAiChatImagesOwnershipvalidateAiChatImageOwnership으로 이름 변경. 여러 이미지 엔터티 목록 검증에서 단일 이미지 ID 검증으로 변경. processAiRequest 호출 시 이미지 엔터티 리스트 대신 aiChatImageId 전달.
요청 처리 메서드 리팩토링
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiServerService.java
processAiRequest 메서드 시그니처 변경: (Long userId, AiChatMessageEntity, List<AiChatImageEntity>)(Long userId, Long messageId, Long aiChatImageId). 메서드 내부에서 messageId로 메시지 엔터티 조회 및 aiChatImageId로 이미지 엔터티 선택적 조회 추가. 메시지 미존재 시 AI_CHAT_MESSAGE_NOT_FOUND 예외 발생.

Sequence Diagram

sequenceDiagram
    participant Client
    participant AiChatMessageService as AiChatMessageService
    participant AiServerService as AiServerService
    participant DB as Database

    Note over AiChatMessageService,AiServerService: 기존 방식 (Before)
    Client->>AiChatMessageService: AI 요청 처리
    AiChatMessageService->>DB: 여러 이미지 엔터티 조회
    AiChatMessageService->>AiChatMessageService: 여러 이미지 소유권 검증
    AiChatMessageService->>AiServerService: processAiRequest(userId, message, imageList)
    
    Note over AiChatMessageService,AiServerService: 새로운 방식 (After)
    Client->>AiChatMessageService: AI 요청 처리
    AiChatMessageService->>DB: 단일 이미지 ID로 소유권 검증
    AiChatMessageService->>AiServerService: processAiRequest(userId, messageId, aiChatImageId)
    AiServerService->>DB: messageId로 메시지 엔터티 조회
    AiServerService->>DB: aiChatImageId로 이미지 엔터티 조회 (선택적)
    AiServerService->>AiServerService: 후속 처리 (권한 검증, 분류 등)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • 주의 깊게 검토할 사항:
    • AiServerService.processAiRequest 메서드 시그니처 변경으로 인한 모든 호출부 영향도 확인 필요
    • 엔터티 조회 실패 시 예외 처리 로직 검증 (AI_CHAT_MESSAGE_NOT_FOUND)
    • null aiChatImageId 처리로 인한 빈 이미지 리스트 생성 로직의 부작용 검토
    • 기존 다운스트림 처리 로직(권한 검증, 요청 분류 등)이 새로운 엔터티 조회 방식과 호환되는지 확인

Possibly related PRs

  • PR #245: AI 채팅 요청 흐름 및 AiServerService.processAiRequest 메서드 수정으로 이미지 URL 처리 및 이미지 리스트 사용 방식을 변경했으며, 현재 PR과 상충할 수 있는 관련 변경사항 포함.
  • PR #259: 동일한 AI 채팅 메시지 처리 흐름 및 AI_CHAT_MESSAGE_NOT_FOUND 예외, 이미지 상태 취소 처리를 포함하여 현재 변경사항과 관련된 로직 수정.
  • PR #169: AiChatMessageService 및 AiServerService 리팩토링으로 동일한 메서드 시그니처 변경을 도입하여 현재 PR의 기반이 되는 관련 변경사항.

Poem

🐰 ID로 이미지를 쏙쏙 찾아내고,
엔터티 목록은 이제 안녕,
메서드 안에서 조회하며 우아하게,
단순해진 흐름이 마음에 쏘옥!
소유권 검증도 한 발 한 발 간단해져요 🌟

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경사항의 주요 내용을 정확히 반영하고 있습니다. 비동기 처리로 인한 중복 저장 및 락 장애 문제를 명확히 설명합니다.
Description check ✅ Passed PR 설명이 템플릿의 필수 섹션(개요, 작업사항)을 모두 포함하고 있으며, 문제 상황과 해결 방안을 구체적으로 설명합니다.
Linked Issues check ✅ Passed 코드 변경이 #262 이슈의 요구사항을 충족합니다. 비동기 메서드 내에서 엔티티를 재조회하도록 변경하여 요청 메시지 상태 업데이트의 즉시 저장을 보장합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 비동기 처리 중 중복 저장 문제 해결이라는 명확한 범위 내에 있습니다. 불필요한 외부 변경이 없습니다.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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 feature/261-fix-ai-chat

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiServerService.java (1)

279-289: 잠재적 NoSuchElementException 위험

Line 280의 .get() 호출은 이미지가 존재하지 않을 경우 NoSuchElementException을 발생시킬 수 있습니다.

시나리오:

  1. AiChatMessageEntityaiChatImageId는 null이 아님 (hasImage() 통과)
  2. 하지만 실제 AiChatImageEntity는 삭제되었거나 존재하지 않음
  3. Line 280에서 예외 발생

다음과 같이 안전하게 처리하세요:

 				// 이미지 컨텐츠 추가 (이미지가 있는 경우)
 				if (entity.hasImage()) {
-					AiChatImageEntity aiChatImage = aiChatImageRepository.findById(entity.getAiChatImageId()).get();
+					AiChatImageEntity aiChatImage = aiChatImageRepository.findById(entity.getAiChatImageId())
+						.orElseThrow(() -> new AppException(ErrorCode.IMAGE_NOT_FOUND_EXCEPTION));
 
 					contents.add(
 						AiImageRequestMessageDto.ChatContent.image(
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da00bd0 and f182ce3.

📒 Files selected for processing (2)
  • src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiChatMessageService.java (3 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiServerService.java (1 hunks)
⏰ 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
🔇 Additional comments (5)
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiServerService.java (2)

103-108: 에러 처리 로직 확인됨

비동기 메서드 내에서 엔티티를 재조회한 후 상태를 업데이트하고 명시적으로 저장하는 패턴이 올바르게 적용되었습니다. 이는 PR 목표인 중복 저장 문제를 해결합니다.

참고: @Transactional 컨텍스트에서 명시적인 save() 호출은 변경 감지(dirty checking)로 자동 처리될 수 있지만, 비동기 환경에서는 명시적 저장이 의도를 명확히 하고 더 안전합니다.


172-174: 상태 업데이트 및 저장 로직이 올바르게 적용됨

요청 메시지의 상태를 REQUEST로 업데이트하고 명시적으로 저장하는 로직이 에러 처리 패턴과 일관되게 적용되었습니다. 이는 Issue #262의 목표인 "요청 메시지 status를 REQUEST로 변경하여 즉시 반영"을 충족합니다.

src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiChatMessageService.java (3)

54-92: 비동기 처리 리팩토링이 올바르게 적용됨

엔티티 대신 ID를 비동기 메서드에 전달하도록 변경한 것은 PR의 핵심 목표인 "중복 저장으로 인한 락 장애" 문제를 해결합니다.

개선 사항:

  • Line 59: 단일 이미지 소유권 검증으로 단순화
  • Line 65: 비동기 호출 전 이미지 존재 여부 검증
  • Line 88: 엔티티 대신 ID 전달하여 비동기 메서드 내부에서 재조회 가능

이는 분리된 엔티티(detached entity) 문제와 중복 저장 시도를 방지하는 올바른 접근 방식입니다.


127-135: 소유권 검증 로직이 효율적으로 개선됨

단일 이미지 검증으로 단순화하고 existsByIdAndUserId를 사용하는 것은 좋은 개선입니다.

장점:

  • 엔티티 전체를 조회하지 않고 존재 여부만 확인하여 성능 최적화
  • null 처리가 명확함
  • 메서드명이 단수형으로 변경되어 의도가 명확함

137-143: 이미지 존재 검증 로직이 올바르게 작동함

비동기 처리 전에 이미지 존재 여부를 검증하는 것은 좋은 방어 코딩입니다.

참고: 검증 시점과 비동기 메서드에서 이미지를 실제로 사용하는 시점 사이에 이미지가 삭제될 수 있는 경쟁 조건(race condition)이 존재합니다. AiServerService의 Lines 80-82에서 이를 처리하는 방식이 주석과 일치하는지 확인이 필요합니다 (해당 파일의 별도 코멘트 참고).

Comment on lines +70 to 72
final Long messageId,
final Long aiChatImageId // 없으면 null, null이 아니면 해당 이미지는 반드시 존재해야 함
) {
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

메서드 계약과 구현 간의 불일치 확인 필요

Line 71의 주석은 aiChatImageId가 null이 아니면 해당 이미지가 반드시 존재해야 한다고 명시하고 있습니다. 하지만 Lines 80-82의 구현은 .orElse(List.of())를 사용하여 이미지가 없을 경우 조용히 빈 리스트를 반환합니다.

이는 다음과 같은 문제를 야기할 수 있습니다:

  • AiChatMessageService에서 이미지 존재 여부를 검증(Line 65)한 후, 비동기 메서드가 실행되기 전에 이미지가 삭제되면 조용히 무시됨
  • 디버깅이 어려워지고 예상치 못한 동작 발생 가능

다음 중 하나를 선택하여 일관성을 확보하세요:

옵션 1 (권장): 이미지가 없으면 예외 발생

 		// 이미지가 있으면 해당 이미지 조회
 		List<AiChatImageEntity> aiChatImages = List.of();
 		if (aiChatImageId != null) {
-			aiChatImages = aiChatImageRepository.findById(aiChatImageId)
-				.map(List::of)
-				.orElse(List.of());
+			AiChatImageEntity aiChatImage = aiChatImageRepository.findById(aiChatImageId)
+				.orElseThrow(() -> new AppException(ErrorCode.IMAGE_NOT_FOUND_EXCEPTION));
+			aiChatImages = List.of(aiChatImage);
 		}

옵션 2: 주석을 구현에 맞게 수정

-		final Long aiChatImageId // 없으면 null, null이 아니면 해당 이미지는 반드시 존재해야 함
+		final Long aiChatImageId // 없으면 null, null이 아니거나 이미지가 삭제된 경우 빈 리스트로 처리
📝 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
final Long messageId,
final Long aiChatImageId // 없으면 null, null이 아니면 해당 이미지는 반드시 존재해야 함
) {
final Long messageId,
final Long aiChatImageId // 없으면 null, null이 아니거나 이미지가 삭제된 경우 빈 리스트로 처리
) {

@yooooonshine yooooonshine merged commit daf65f5 into develop Nov 16, 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.

1 participant