Skip to content

refactor: 프로필/썸네일 이미지 업로드를 동기 방식으로 전환#230

Merged
dstle merged 4 commits into
devfrom
feature/152-image-upload-sync
Jun 30, 2026
Merged

refactor: 프로필/썸네일 이미지 업로드를 동기 방식으로 전환#230
dstle merged 4 commits into
devfrom
feature/152-image-upload-sync

Conversation

@dstle

@dstle dstle commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

관련 이슈

##작업내용

동기 전환

  • user/content 이미지 업로드를 동기로 변경 — 업로드 성공 후에만 COMPLETED 저장
  • 업로드 실패 시 @Transactional 롤백 → 기존 이미지 유지 + 에러 응답(사용자 재시도)
  • 비동기 인프라 제거: BinaryContentUploadListener, BinaryContentUploadEvent 삭제

구조 정리

  • BinaryContentService.upload(...) 신설 — user/content에 중복되던 업로드 로직 일원화
  • 이미지 교체 시 기존 이미지 DELETED 처리를 엔티티(User.updateProfileImage / Content.attachThumbnail)로 위임 → 서비스 중첩 if 제거
  • BinaryContentUploadStatusCOMPLETED/DELETED 2개로 축소 (flyway V12)
  • 명세에 없던 ContentResponse.thumbnailUploadStatus 제거 → Swagger 명세와 일치
  • 프로필 이미지 조회 @EntityGraphleft join fetch

테스트

  • BinaryContentServiceTest: 업로드 성공 / 실패 시 예외 전파·저장 안 함
  • UserServiceTest.update_imageUploadFails: 업로드 실패 → 예외 + 프로필 미변경

테스트 내용

  • 테스트 코드 작성
  • 로컬 테스트 완료
  • API 테스트 완료
  • 기타 테스트 완료

체크리스트

  • 코드 컨벤션을 지켰습니다.
  • 불필요한 코드를 제거했습니다.
  • 주석이 필요한 부분에 추가했습니다.
  • 관련 문서를 수정했습니다.
  • 리뷰어가 이해하기 쉽도록 작성했습니다.

리뷰 포인트

  • 중점적으로 봐줬으면 하는 부분을 작성해주세요.

스크린샷 / 참고 자료

  • 필요한 경우 첨부해주세요.

🔗 관련 이슈

  • close #152

📝 작업 내용

  • 이미지 업로드를 비동기 이벤트 처리에서 동기 처리로 전환해, 업로드 결과와 상태 저장의 순서를 명확히 했습니다.
  • 기존에는 업로드 요청 후 PENDING/FAILED/COMPLETED 상태 전이를 별도 리스너가 처리하면서 흐름이 분산되어 있었는데, 이제는 업로드 성공 시에만 COMPLETED가 저장되도록 바꿔 실패 시 트랜잭션 롤백으로 기존 이미지를 유지합니다.
  • 프로필/썸네일 업로드 로직이 서비스별로 중복되어 있던 부분을 공통 업로드 서비스로 정리해, 책임을 “업로드 수행”과 “엔티티 연결”로 분리했습니다.

주요 변경사항

  1. 이미지 업로드 처리 구조를 비동기 이벤트 기반에서 동기 서비스 호출 기반으로 단순화했습니다.
  2. 업로드 실패 시 상태만 갱신하는 대신 전체 트랜잭션을 롤백하도록 바꿔, 데이터 불일치 가능성을 줄였습니다.
  3. 업로드 상태는 COMPLETEDDELETED만 남기고 정리했으며, 응답/API에서도 더 이상 불필요한 업로드 상태 필드를 노출하지 않도록 맞췄습니다.

📊 변경 효과 요약 (가능한 경우에만)

  • 업로드 상태값 축소: PENDING, FAILED 제거
  • 비동기 업로드 관련 구성요소 제거: 리스너/이벤트 삭제
  • 추가 테스트: 업로드 성공/실패 및 롤백 검증 케이스 보강
  • 정량 지표는 이번 PR에서 측정하지 않음

@dstle dstle self-assigned this Jun 30, 2026
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d2019b42-90ff-491e-87d1-4823d39b4da4

📥 Commits

Reviewing files that changed from the base of the PR and between 40d93a8 and 8c52bcc.

📒 Files selected for processing (20)
  • src/main/java/com/codeit/team5/mopl/binarycontent/entity/BinaryContent.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/entity/BinaryContentUploadStatus.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/event/BinaryContentUploadEvent.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListener.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentService.java
  • src/main/java/com/codeit/team5/mopl/content/dto/response/ContentResponse.java
  • src/main/java/com/codeit/team5/mopl/content/entity/Content.java
  • src/main/java/com/codeit/team5/mopl/content/mapper/ContentMapper.java
  • src/main/java/com/codeit/team5/mopl/content/service/ContentService.java
  • src/main/java/com/codeit/team5/mopl/user/entity/User.java
  • src/main/java/com/codeit/team5/mopl/user/repository/UserRepository.java
  • src/main/java/com/codeit/team5/mopl/user/service/UserService.java
  • src/main/resources/db/migration/V13__reduce_binary_content_upload_status.sql
  • src/test/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListenerTest.java
  • src/test/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentServiceTest.java
  • src/test/java/com/codeit/team5/mopl/content/controller/ContentControllerTest.java
  • src/test/java/com/codeit/team5/mopl/content/repository/ContentRepositoryTest.java
  • src/test/java/com/codeit/team5/mopl/content/service/ContentServiceTest.java
  • src/test/java/com/codeit/team5/mopl/user/service/UserServiceImageRollbackIntegrationTest.java
  • src/test/java/com/codeit/team5/mopl/user/service/UserServiceTest.java
💤 Files with no reviewable changes (6)
  • src/main/java/com/codeit/team5/mopl/binarycontent/entity/BinaryContentUploadStatus.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/event/BinaryContentUploadEvent.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListener.java
  • src/main/java/com/codeit/team5/mopl/content/mapper/ContentMapper.java
  • src/test/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListenerTest.java
  • src/main/java/com/codeit/team5/mopl/content/dto/response/ContentResponse.java

📝 Walkthrough

Walkthrough

이미지 업로드 처리를 비동기 이벤트 방식(BinaryContentUploadEvent + BinaryContentUploadListener)에서 동기 방식으로 전환합니다. BinaryContentUploadStatus에서 PENDINGFAILED를 제거하고 COMPLETEDDELETED만 유지하며, BinaryContent.pending()completed()로 교체합니다. BinaryContentService는 상태 갱신 메서드 대신 upload()를 제공하고, ContentServiceUserService는 저장소·이벤트 의존성 대신 BinaryContentService.upload()를 직접 호출합니다. 기존 썸네일과 프로필 이미지 교체 시 DELETED 상태 갱신이 엔티티 메서드로 이동됩니다. DB 마이그레이션과 관련 테스트도 함께 갱신됩니다.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • sb10-part4-team5/sb10-mopl-team5#140: BinaryContentUploadListener, BinaryContentUploadEvent, 썸네일·프로필 이미지 업로드 흐름(ContentServiceBinaryContentService)을 동일한 바이너리 업로드 도메인에서 수정·교체하고 있어 직접적으로 연관됩니다.
  • sb10-part4-team5/sb10-mopl-team5#146: BinaryContentStorage 키 생성 및 이미지 확장자 검증(ImageExtension) 도입과 같은 바이너리 이미지 업로드 경로 변경이 이번 PR의 BinaryContentService.upload() 전환과 맞물립니다.
  • sb10-part4-team5/sb10-mopl-team5#157: 콘텐츠 수정/삭제 시 BinaryContentUploadStatus.DELETED 적용과 썸네일 생명주기 처리가 이번 PR의 변경과 동일한 로직 영역에 직접 접합니다.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 프로필/썸네일 이미지 업로드를 동기 방식으로 전환하는 핵심 변경을 정확하고 간결하게 요약합니다.
Description check ✅ Passed 이슈, 작업 내용, 테스트, 체크리스트가 대부분 채워져 있어 템플릿 요구를 충분히 만족합니다.
Linked Issues check ✅ Passed 이미지 업로드 동기화와 상태값을 COMPLETED/DELETED로 축소하는 요구사항을 코드와 테스트가 모두 충족합니다.
Out of Scope Changes check ✅ Passed 변경된 항목들은 모두 이미지 업로드 동기화, 상태 정리, 관련 테스트/매핑 정리에 직접 연결되어 있습니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/152-image-upload-sync

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
src/test/java/com/codeit/team5/mopl/content/controller/ContentControllerTest.java (1)

114-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

제거된 응답 필드는 부재까지 검증하세요.

현재는 thumbnailUploadStatus 검증을 제거해서 생성자/응답 형태는 맞지만, 필드가 실수로 다시 직렬화되어도 테스트가 통과합니다. PR 목표가 응답 계약에서 제거하는 것이라면 주요 응답 경로에 doesNotExist()를 추가해 회귀를 잡는 편이 안전합니다.

🧪 예시 보강
-                .andExpect(jsonPath("$.watcherCount").value(0));
+                .andExpect(jsonPath("$.watcherCount").value(0))
+                .andExpect(jsonPath("$.thumbnailUploadStatus").doesNotExist());

목록 응답은 $.data[0].thumbnailUploadStatus로 같은 검증을 추가하면 됩니다.

Also applies to: 320-332, 539-550, 612-618

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/codeit/team5/mopl/content/controller/ContentControllerTest.java`
around lines 114 - 124, In the ContentControllerTest response assertions, add
negative coverage for the removed thumbnailUploadStatus field so the contract
change is enforced even if serialization regresses. Update the main
create/content response checks around the existing jsonPath assertions to verify
that thumbnailUploadStatus does not exist, and apply the same doesNotExist()
check in the other affected response tests that already reference the
ContentControllerTest response payloads, especially the list and detail paths
tied to the same DTO shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/codeit/team5/mopl/content/service/ContentService.java`:
- Around line 58-59: 썸네일 업로드가 attachTags()의 태그 검증보다 먼저 실행되어, 이후
normalizeTagNames()에서 예외가 나면 DB는 롤백돼도 스토리지에는 orphan 파일이 남습니다.
ContentService.create()에서 태그 정규화/검증을 먼저 끝내고, 그 다음에
binaryContentService.upload()와 content.attachThumbnail()를 수행하도록 순서를 바꾸세요. 핵심 변경
지점은 create(), attachTags(), normalizeTagNames() 흐름입니다.

In `@src/main/java/com/codeit/team5/mopl/user/entity/User.java`:
- Around line 71-75: 프로필 이미지 교체 시 기존 BinaryContent가 DELETED로 전환되는 분기가 테스트로 고정되지
않았습니다. User.updateProfileImage와 이를 호출하는 UserServiceTest 경로를 기준으로, 기존
profileImage가 있는 사용자에게 새 이미지를 적용할 때 이전 이미지의 upload status가
BinaryContentUploadStatus.DELETED로 변경되는 회귀 테스트를 추가하세요. 신규 업로드 성공/실패 테스트와 별개로,
ContentServiceTest.update_withNewThumbnail_oldThumbnailMarkedDeleted()처럼 기존 이미지
상태 변경까지 검증하는 케이스를 넣어 주세요.

In `@src/main/java/com/codeit/team5/mopl/user/service/UserService.java`:
- Line 78: The profile image upload in UserService.updateProfileImage currently
persists to external storage before the database transaction is safely
committed, which can leave orphaned files if later flush/commit fails. Fix this
by either registering a rollback compensation to delete the uploaded object when
the transaction fails, or by changing the flow to use a temporary upload key and
only finalize the object after commit. Keep the change localized around
UserService.updateProfileImage and the
binaryContentService.upload(StorageDirectory.PROFILE, user.getId(), image) call.

In
`@src/main/resources/db/migration/V12__reduce_binary_content_upload_status.sql`:
- Around line 1-10: The backfill in V12__reduce_binary_content_upload_status.sql
incorrectly upgrades all binary_contents rows with upload_status in PENDING or
FAILED to COMPLETED, which can preserve broken uploads as valid data. Update
this migration so the data cleanup in the binary_contents table either verifies
the stored object exists before marking it COMPLETED, or more conservatively
clears those references and marks them DELETED instead; then keep the
ck_binary_contents_upload_status constraint aligned with the final allowed
statuses. Use the existing binary_contents.upload_status update and
constraint-alter statements as the place to adjust this logic.

In
`@src/test/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentServiceTest.java`:
- Around line 54-63: 성공 케이스 테스트에서 `BinaryContentService.upload(...)`가
`binaryContentRepository.save(...)`를 실제로 호출하는지 검증이 빠져 있습니다.
`BinaryContentServiceTest`의 성공 케이스에 `save()` 호출 검증을 추가하고, 가능하면 `ArgumentCaptor`
또는 `argThat`를 사용해 저장된 `BinaryContent`의 URL과 `COMPLETED` 상태까지 함께 확인하세요. 특히
`binaryContentService.upload`, `binaryContentRepository.save`,
`binaryContentStorage.store`를 기준으로 해당 계약이 지켜지는지 테스트를 보강하면 됩니다.

In `@src/test/java/com/codeit/team5/mopl/user/service/UserServiceTest.java`:
- Around line 226-242: The current UserServiceTest case only proves that
UserService.update throws and leaves the in-memory User unchanged, but it does
not verify transactional rollback because this is a Mockito unit test without
the `@Transactional` proxy. Update the test around UserService.update,
userRepository.findWithProfileImageById, and binaryContentService.upload so it
either becomes a Spring integration test that reloads the User after the failure
and asserts both the name and profileImage were rolled back, or else
rename/retarget the test to only assert exception propagation.

---

Outside diff comments:
In
`@src/test/java/com/codeit/team5/mopl/content/controller/ContentControllerTest.java`:
- Around line 114-124: In the ContentControllerTest response assertions, add
negative coverage for the removed thumbnailUploadStatus field so the contract
change is enforced even if serialization regresses. Update the main
create/content response checks around the existing jsonPath assertions to verify
that thumbnailUploadStatus does not exist, and apply the same doesNotExist()
check in the other affected response tests that already reference the
ContentControllerTest response payloads, especially the list and detail paths
tied to the same DTO shape.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e18fa6fc-e97b-4b90-9494-34569c47a010

📥 Commits

Reviewing files that changed from the base of the PR and between d880462 and a729656.

📒 Files selected for processing (19)
  • src/main/java/com/codeit/team5/mopl/binarycontent/entity/BinaryContent.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/entity/BinaryContentUploadStatus.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/event/BinaryContentUploadEvent.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListener.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentService.java
  • src/main/java/com/codeit/team5/mopl/content/dto/response/ContentResponse.java
  • src/main/java/com/codeit/team5/mopl/content/entity/Content.java
  • src/main/java/com/codeit/team5/mopl/content/mapper/ContentMapper.java
  • src/main/java/com/codeit/team5/mopl/content/service/ContentService.java
  • src/main/java/com/codeit/team5/mopl/user/entity/User.java
  • src/main/java/com/codeit/team5/mopl/user/repository/UserRepository.java
  • src/main/java/com/codeit/team5/mopl/user/service/UserService.java
  • src/main/resources/db/migration/V12__reduce_binary_content_upload_status.sql
  • src/test/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListenerTest.java
  • src/test/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentServiceTest.java
  • src/test/java/com/codeit/team5/mopl/content/controller/ContentControllerTest.java
  • src/test/java/com/codeit/team5/mopl/content/repository/ContentRepositoryTest.java
  • src/test/java/com/codeit/team5/mopl/content/service/ContentServiceTest.java
  • src/test/java/com/codeit/team5/mopl/user/service/UserServiceTest.java
💤 Files with no reviewable changes (6)
  • src/main/java/com/codeit/team5/mopl/binarycontent/entity/BinaryContentUploadStatus.java
  • src/test/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListenerTest.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/event/BinaryContentUploadEvent.java
  • src/main/java/com/codeit/team5/mopl/content/mapper/ContentMapper.java
  • src/main/java/com/codeit/team5/mopl/content/dto/response/ContentResponse.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListener.java

Comment thread src/main/java/com/codeit/team5/mopl/user/entity/User.java
Comment thread src/main/java/com/codeit/team5/mopl/user/service/UserService.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/test/java/com/codeit/team5/mopl/user/service/UserServiceTest.java (1)

205-214: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

이미지 관련 테스트에 실제 userId를 주입해 ownerId 전달을 검증하세요.

User.create(...)id를 채우지 않는데, 여기서는 eq(user.getId())로 stubbing/verify를 걸어서 결국 binaryContentService.upload(..., null, ...)를 기대하게 됩니다. 그래서 이 테스트들은 “업로드 키 생성에 대상 사용자 ID를 넘긴다”는 실제 계약을 충분히 못 잡습니다. ReflectionTestUtils.setField(user, "id", userId)로 ID를 넣고 eq(userId)로 맞추는 쪽이 더 정확합니다.

예시 수정
         UUID userId = UUID.randomUUID();
         User user = User.create("user@example.com", "encoded-password", "기존이름");
+        ReflectionTestUtils.setField(user, "id", userId);
         FileRequest image = new FileRequest(new byte[]{1, 2, 3}, "profile.jpg");
 ...
-        when(binaryContentService.upload(eq(StorageDirectory.PROFILE), eq(user.getId()), any()))
+        when(binaryContentService.upload(eq(StorageDirectory.PROFILE), eq(userId), any()))
                 .thenReturn(BinaryContent.completed("http://localhost/profiles/key.jpg"));
 ...
-        verify(binaryContentService).upload(eq(StorageDirectory.PROFILE), eq(user.getId()), any());
+        verify(binaryContentService).upload(eq(StorageDirectory.PROFILE), eq(userId), any());

Also applies to: 224-224, 231-236, 260-261

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/codeit/team5/mopl/user/service/UserServiceTest.java` around
lines 205 - 214, The image-related tests are stubbing and verifying
binaryContentService.upload with user.getId(), but User.create(...) leaves the
id null so the test is not actually asserting the intended ownerId contract. In
UserServiceTest, set the created User’s id to the generated userId before using
it (for example via ReflectionTestUtils.setField on user) and update all upload
stubs/verifications to use eq(userId) instead of eq(user.getId()). Apply the
same change to the related image upload test cases so they consistently verify
that the target user ID is passed into binaryContentService.upload from the
service methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/codeit/team5/mopl/content/service/ContentService.java`:
- Around line 60-62: Move the thumbnail upload in ContentService so it happens
only after ContentStats has been successfully created and saved, using the
ContentService flow around attachThumbnail and contentStatsRepository.save. The
current order uploads via binaryContentService.upload before the DB write/flush,
which can leave an orphaned file if the transaction rolls back. Reorder the
logic so the ContentStats linkage is completed first, or alternatively add
rollback compensation to delete the uploaded thumbnail if the save fails.

In
`@src/test/java/com/codeit/team5/mopl/user/service/UserServiceImageRollbackIntegrationTest.java`:
- Around line 47-61: The integration test only covers a user with no existing
profile image, so it misses the rollback contract for image replacement
failures. Update UserServiceImageRollbackIntegrationTest to seed a user that
already has a BinaryContent.completed(...) profile image, then trigger the
update failure through UserService.update and verify both the name and the
existing profile image URL remain unchanged after the exception. Use the
existing helpers and assertions around userRepository, findWithProfileImageById,
and binaryContentStorage to keep the failure case fixed against regressions.

---

Outside diff comments:
In `@src/test/java/com/codeit/team5/mopl/user/service/UserServiceTest.java`:
- Around line 205-214: The image-related tests are stubbing and verifying
binaryContentService.upload with user.getId(), but User.create(...) leaves the
id null so the test is not actually asserting the intended ownerId contract. In
UserServiceTest, set the created User’s id to the generated userId before using
it (for example via ReflectionTestUtils.setField on user) and update all upload
stubs/verifications to use eq(userId) instead of eq(user.getId()). Apply the
same change to the related image upload test cases so they consistently verify
that the target user ID is passed into binaryContentService.upload from the
service methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d6055777-13d3-4aa2-b516-3e8ce3c58591

📥 Commits

Reviewing files that changed from the base of the PR and between a729656 and 92e908c.

📒 Files selected for processing (4)
  • src/main/java/com/codeit/team5/mopl/content/service/ContentService.java
  • src/test/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentServiceTest.java
  • src/test/java/com/codeit/team5/mopl/user/service/UserServiceImageRollbackIntegrationTest.java
  • src/test/java/com/codeit/team5/mopl/user/service/UserServiceTest.java

@dstle dstle requested a review from plzslp June 30, 2026 07:53
@dstle dstle force-pushed the feature/152-image-upload-sync branch from 40d93a8 to dd11409 Compare June 30, 2026 08:03
plzslp
plzslp previously approved these changes Jun 30, 2026

@plzslp plzslp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

schema.sql 파일 V12를 제가 V12__mapsid_content_stats.sql로 사용해서 V13으로 바꿔야할 것 같습니다.
다른 변경사항은 BinaryConent 업로드 로직이 동기 방식으로 잘 수정되어 있고, 프로필, 썸네일 수정시에 기존 BInaryConent 상태 변경 로직도 개별 엔티티로 잘 옮겨진 걸 확인했습니다.

아마 이전 PR인 콘텐츠 수집부분에서 Conflict가 생길 것 같은데 혹시라도 문제있는 부분은 말해주세요. ContentService 쪽은 주석 외에는 거의 건들지 않았기에 이미지 업로드 동기 방식 전환 PR 기준으로 수정하시면 될 것 같습니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentService.java`:
- Around line 23-28: `BinaryContentService.upload()` is doing external storage
I/O inside a `@Transactional` method, which can leave the uploaded object
orphaned if `binaryContentRepository.save(...)` or the surrounding transaction
fails. Refactor the `upload(...)` flow so `binaryContentStorage.store(...)` is
not executed under the database transaction, or add a compensating cleanup step
for rollback/after-completion using the `BinaryContentStorage` and
`BinaryContentRepository` integration points. Keep the transaction limited to
persisting the metadata after the file upload succeeds.

In `@src/main/java/com/codeit/team5/mopl/content/service/ContentService.java`:
- Around line 36-41: Update the class Javadoc on ContentService to match the new
thumbnail upload behavior: it currently says thumbnails are uploaded to S3
asynchronously via event-based processing, but this PR changes that flow to
synchronous handling. Rewrite the affected description so it accurately reflects
the synchronous upload path and no longer mentions event-based async behavior.

In `@src/test/java/com/codeit/team5/mopl/content/service/ContentServiceTest.java`:
- Around line 71-72: `ContentServiceTest` still declares an `eventPublisher`
mock and verifies it with `verifyNoInteractions`, but `ContentService` no longer
injects `ApplicationEventPublisher`, so this is dead setup/verification. Remove
the `eventPublisher` mock field and drop it from the `verifyNoInteractions`
call, keeping only the dependencies that `ContentService` actually uses in the
test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d2019b42-90ff-491e-87d1-4823d39b4da4

📥 Commits

Reviewing files that changed from the base of the PR and between 40d93a8 and 8c52bcc.

📒 Files selected for processing (20)
  • src/main/java/com/codeit/team5/mopl/binarycontent/entity/BinaryContent.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/entity/BinaryContentUploadStatus.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/event/BinaryContentUploadEvent.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListener.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentService.java
  • src/main/java/com/codeit/team5/mopl/content/dto/response/ContentResponse.java
  • src/main/java/com/codeit/team5/mopl/content/entity/Content.java
  • src/main/java/com/codeit/team5/mopl/content/mapper/ContentMapper.java
  • src/main/java/com/codeit/team5/mopl/content/service/ContentService.java
  • src/main/java/com/codeit/team5/mopl/user/entity/User.java
  • src/main/java/com/codeit/team5/mopl/user/repository/UserRepository.java
  • src/main/java/com/codeit/team5/mopl/user/service/UserService.java
  • src/main/resources/db/migration/V13__reduce_binary_content_upload_status.sql
  • src/test/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListenerTest.java
  • src/test/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentServiceTest.java
  • src/test/java/com/codeit/team5/mopl/content/controller/ContentControllerTest.java
  • src/test/java/com/codeit/team5/mopl/content/repository/ContentRepositoryTest.java
  • src/test/java/com/codeit/team5/mopl/content/service/ContentServiceTest.java
  • src/test/java/com/codeit/team5/mopl/user/service/UserServiceImageRollbackIntegrationTest.java
  • src/test/java/com/codeit/team5/mopl/user/service/UserServiceTest.java
💤 Files with no reviewable changes (6)
  • src/main/java/com/codeit/team5/mopl/binarycontent/entity/BinaryContentUploadStatus.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/event/BinaryContentUploadEvent.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListener.java
  • src/main/java/com/codeit/team5/mopl/content/mapper/ContentMapper.java
  • src/test/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListenerTest.java
  • src/main/java/com/codeit/team5/mopl/content/dto/response/ContentResponse.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentService.java`:
- Around line 23-28: `BinaryContentService.upload()` is doing external storage
I/O inside a `@Transactional` method, which can leave the uploaded object
orphaned if `binaryContentRepository.save(...)` or the surrounding transaction
fails. Refactor the `upload(...)` flow so `binaryContentStorage.store(...)` is
not executed under the database transaction, or add a compensating cleanup step
for rollback/after-completion using the `BinaryContentStorage` and
`BinaryContentRepository` integration points. Keep the transaction limited to
persisting the metadata after the file upload succeeds.

In `@src/main/java/com/codeit/team5/mopl/content/service/ContentService.java`:
- Around line 36-41: Update the class Javadoc on ContentService to match the new
thumbnail upload behavior: it currently says thumbnails are uploaded to S3
asynchronously via event-based processing, but this PR changes that flow to
synchronous handling. Rewrite the affected description so it accurately reflects
the synchronous upload path and no longer mentions event-based async behavior.

In `@src/test/java/com/codeit/team5/mopl/content/service/ContentServiceTest.java`:
- Around line 71-72: `ContentServiceTest` still declares an `eventPublisher`
mock and verifies it with `verifyNoInteractions`, but `ContentService` no longer
injects `ApplicationEventPublisher`, so this is dead setup/verification. Remove
the `eventPublisher` mock field and drop it from the `verifyNoInteractions`
call, keeping only the dependencies that `ContentService` actually uses in the
test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d2019b42-90ff-491e-87d1-4823d39b4da4

📥 Commits

Reviewing files that changed from the base of the PR and between 40d93a8 and 8c52bcc.

📒 Files selected for processing (20)
  • src/main/java/com/codeit/team5/mopl/binarycontent/entity/BinaryContent.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/entity/BinaryContentUploadStatus.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/event/BinaryContentUploadEvent.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListener.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentService.java
  • src/main/java/com/codeit/team5/mopl/content/dto/response/ContentResponse.java
  • src/main/java/com/codeit/team5/mopl/content/entity/Content.java
  • src/main/java/com/codeit/team5/mopl/content/mapper/ContentMapper.java
  • src/main/java/com/codeit/team5/mopl/content/service/ContentService.java
  • src/main/java/com/codeit/team5/mopl/user/entity/User.java
  • src/main/java/com/codeit/team5/mopl/user/repository/UserRepository.java
  • src/main/java/com/codeit/team5/mopl/user/service/UserService.java
  • src/main/resources/db/migration/V13__reduce_binary_content_upload_status.sql
  • src/test/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListenerTest.java
  • src/test/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentServiceTest.java
  • src/test/java/com/codeit/team5/mopl/content/controller/ContentControllerTest.java
  • src/test/java/com/codeit/team5/mopl/content/repository/ContentRepositoryTest.java
  • src/test/java/com/codeit/team5/mopl/content/service/ContentServiceTest.java
  • src/test/java/com/codeit/team5/mopl/user/service/UserServiceImageRollbackIntegrationTest.java
  • src/test/java/com/codeit/team5/mopl/user/service/UserServiceTest.java
💤 Files with no reviewable changes (6)
  • src/main/java/com/codeit/team5/mopl/binarycontent/entity/BinaryContentUploadStatus.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/event/BinaryContentUploadEvent.java
  • src/main/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListener.java
  • src/main/java/com/codeit/team5/mopl/content/mapper/ContentMapper.java
  • src/test/java/com/codeit/team5/mopl/binarycontent/listener/BinaryContentUploadListenerTest.java
  • src/main/java/com/codeit/team5/mopl/content/dto/response/ContentResponse.java
🛑 Comments failed to post (3)
src/main/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentService.java (1)

23-28: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# upload()를 호출하는 지점과 그 트랜잭션 경계 확인
rg -nP --type=java -C3 '\bbinaryContentService\.upload\s*\('
# 스토리지 store/delete 인터페이스에 보상 삭제 수단이 있는지 확인
ast-grep run --pattern 'interface BinaryContentStorage { $$$ }' --lang java $(fd -t f BinaryContentStorage.java)

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and related service/interface definitions.
git ls-files | rg 'BinaryContentService\.java|BinaryContentStorage\.java|ContentService\.java|UserService\.java'

# Show compact structure for the target service and storage interface.
for f in $(git ls-files | rg 'BinaryContentService\.java|BinaryContentStorage\.java'); do
  echo "### $f"
  ast-grep outline "$f" --view expanded || true
done

# Find all call sites of upload().
rg -n --type=java 'binaryContentService\.upload\s*\(' src/main/java

# Show transactional annotations around callers if present.
rg -n --type=java '`@Transactional`|binaryContentService\.upload\s*\(' src/main/java/com/codeit/team5/mopl -C 3

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 33854


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant files in bounded slices.
for f in src/main/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentService.java \
         src/main/java/com/codeit/team5/mopl/binarycontent/storage/BinaryContentStorage.java \
         src/main/java/com/codeit/team5/mopl/content/service/ContentService.java \
         src/main/java/com/codeit/team5/mopl/user/service/UserService.java; do
  if [ -f "$f" ]; then
    echo "### $f ($(wc -l < "$f") lines)"
    cat -n "$f" | sed -n '1,220p'
  fi
done

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 18368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the storage abstraction supports cleanup/delete and how the S3/local impls behave.
rg -n --type=java 'void delete\(|delete\(' src/main/java/com/codeit/team5/mopl/binarycontent/storage src/main/java/com/codeit/team5/mopl/binarycontent/service src/main/java/com/codeit/team5/mopl/binarycontent/entity -C 2

# Inspect the concrete storage implementations around store/toUrl methods.
for f in src/main/java/com/codeit/team5/mopl/binarycontent/storage/s3/S3BinaryContentStorage.java \
         src/main/java/com/codeit/team5/mopl/binarycontent/storage/local/LocalBinaryContentStorage.java; do
  echo "### $f ($(wc -l < "$f") lines)"
  cat -n "$f" | sed -n '1,220p'
done

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect whether any async / after-commit mechanism already exists for binary content.
rg -n --type=java 'TransactionPhase\.AFTER_COMMIT|TransactionSynchronization|`@TransactionalEventListener`|binaryContentStorage|BinaryContentUploadStatus' src/main/java/com/codeit/team5/mopl -C 2

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 12605


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in src/main/java/com/codeit/team5/mopl/binarycontent/storage/s3/S3BinaryContentStorage.java \
         src/main/java/com/codeit/team5/mopl/binarycontent/storage/local/LocalBinaryContentStorage.java; do
  echo "### $f ($(wc -l < "$f") lines)"
  cat -n "$f" | sed -n '1,220p'
done

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 4766


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the concrete storage implementations and any cleanup/delete support.
for f in src/main/java/com/codeit/team5/mopl/binarycontent/storage/s3/S3BinaryContentStorage.java \
         src/main/java/com/codeit/team5/mopl/binarycontent/storage/local/LocalBinaryContentStorage.java; do
  echo "### $f ($(wc -l < "$f") lines)"
  cat -n "$f" | sed -n '1,220p'
done

rg -n --type=java 'delete\(|remove\(' src/main/java/com/codeit/team5/mopl/binarycontent -C 2

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 4766


외부 스토리지 업로드는 트랜잭션 밖으로 분리하거나 보상 삭제를 붙여야 합니다. BinaryContentService.upload()@Transactional 안에서 먼저 store()를 호출해 S3/로컬 I/O 동안 DB 커넥션을 붙잡고, 이후 save()나 상위 트랜잭션이 실패하면 업로드된 객체만 남습니다. BinaryContentStorage에는 삭제/롤백 처리 수단도 없어서 상태 불일치를 정리할 방법이 없습니다. 업로드를 분리해 메타데이터만 짧게 저장하거나, afterCompletion 기반 삭제를 넣는 쪽이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/codeit/team5/mopl/binarycontent/service/BinaryContentService.java`
around lines 23 - 28, `BinaryContentService.upload()` is doing external storage
I/O inside a `@Transactional` method, which can leave the uploaded object
orphaned if `binaryContentRepository.save(...)` or the surrounding transaction
fails. Refactor the `upload(...)` flow so `binaryContentStorage.store(...)` is
not executed under the database transaction, or add a compensating cleanup step
for rollback/after-completion using the `BinaryContentStorage` and
`BinaryContentRepository` integration points. Keep the transaction limited to
persisting the metadata after the file upload succeeds.
src/main/java/com/codeit/team5/mopl/content/service/ContentService.java (1)

36-41: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

클래스 Javadoc이 이번 변경과 모순됩니다 — "비동기 업로드(이벤트 기반)" 문구를 동기 처리로 갱신해 주세요.

이 PR의 본질이 썸네일 업로드를 이벤트 기반 비동기에서 동기 처리로 전환하는 것인데, 정작 클래스 설명에는 여전히 썸네일 이미지는 S3에 비동기 업로드(이벤트 기반)되며라고 적혀 있습니다. 코드와 문서가 어긋나면, 나중에 이 클래스를 읽는 사람이 실제로는 존재하지 않는 이벤트 리스너를 찾으러 다니게 되어 유지보수 비용이 늘어납니다. 문서는 코드만큼 자주 검증되지 않으니, 이런 동작 전환 시점에 함께 고쳐두는 게 안전합니다.

📝 제안 수정
- * <p>외부 API 수집 콘텐츠({`@link` TmdbContentService}, {`@link` SportsDbContentService})와 달리
- * 썸네일 이미지는 S3에 비동기 업로드(이벤트 기반)되며, 태그는 최대 10개 제한이 적용된다.</p>
+ * <p>외부 API 수집 콘텐츠({`@link` TmdbContentService}, {`@link` SportsDbContentService})와 달리
+ * 썸네일 이미지는 {`@link` BinaryContentService#upload}를 통해 동기적으로 S3에 업로드되며,
+ * 업로드 실패 시 트랜잭션 롤백으로 기존 상태가 보존된다. 태그는 최대 10개 제한이 적용된다.</p>
📝 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.

/**
 * 관리자가 직접 등록하는 콘텐츠의 CRUD를 담당하는 서비스.
 *
 * <p>외부 API 수집 콘텐츠({`@link` TmdbContentService}, {`@link` SportsDbContentService})와 달리
 * 썸네일 이미지는 {`@link` BinaryContentService#upload}를 통해 동기적으로 S3에 업로드되며,
 * 업로드 실패 시 트랜잭션 롤백으로 기존 상태가 보존된다. 태그는 최대 10개 제한이 적용된다.</p>
 */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/codeit/team5/mopl/content/service/ContentService.java`
around lines 36 - 41, Update the class Javadoc on ContentService to match the
new thumbnail upload behavior: it currently says thumbnails are uploaded to S3
asynchronously via event-based processing, but this PR changes that flow to
synchronous handling. Rewrite the affected description so it accurately reflects
the synchronous upload path and no longer mentions event-based async behavior.
src/test/java/com/codeit/team5/mopl/content/service/ContentServiceTest.java (1)

71-72: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# ContentService가 ApplicationEventPublisher에 의존하는지 확인
rg -nP 'ApplicationEventPublisher|eventPublisher|publishEvent' src/main/java/com/codeit/team5/mopl/content/service/ContentService.java

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ContentService =="
fd -a 'ContentService.java' src/main/java src/test/java
echo
echo "== ContentServiceTest outline =="
ast-grep outline src/test/java/com/codeit/team5/mopl/content/service/ContentServiceTest.java --view expanded
echo
echo "== Relevant mentions in test =="
rg -n -C 2 'eventPublisher|verifyNoInteractions|verify\(' src/test/java/com/codeit/team5/mopl/content/service/ContentServiceTest.java
echo
echo "== Relevant mentions in service =="
rg -n -C 2 'ApplicationEventPublisher|eventPublisher|publishEvent' src/main/java/com/codeit/team5/mopl/content/service/ContentService.java || true

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 5714


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ContentServiceTest top =="
sed -n '1,110p' src/test/java/com/codeit/team5/mopl/content/service/ContentServiceTest.java | cat -n
echo
echo "== ContentService top =="
sed -n '1,140p' src/main/java/com/codeit/team5/mopl/content/service/ContentService.java | cat -n

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 12166


eventPublisher mock과 관련 검증을 제거하세요.

ContentServiceApplicationEventPublisher를 더 이상 주입받지 않아서, 이 mock은 @InjectMocks 대상이 아닙니다. 지금의 verifyNoInteractions(..., eventPublisher)는 항상 통과하는 죽은 검증이라 테스트 의도를 흐립니다. eventPublisher 필드와 해당 인자를 verifyNoInteractions에서 빼고, 실제로 사용하는 의존성만 검증하도록 정리하는 편이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/codeit/team5/mopl/content/service/ContentServiceTest.java`
around lines 71 - 72, `ContentServiceTest` still declares an `eventPublisher`
mock and verifies it with `verifyNoInteractions`, but `ContentService` no longer
injects `ApplicationEventPublisher`, so this is dead setup/verification. Remove
the `eventPublisher` mock field and drop it from the `verifyNoInteractions`
call, keeping only the dependencies that `ContentService` actually uses in the
test.

@plzslp plzslp left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

수고하셨습니다!

@dstle dstle merged commit a75e081 into dev Jun 30, 2026
2 checks passed
@dstle dstle deleted the feature/152-image-upload-sync branch June 30, 2026 08:22
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.

[Feat] 이미지 업로드 로직을 동기로 변경

2 participants