-
Notifications
You must be signed in to change notification settings - Fork 0
[CEEZ-62] feat: 앨범 만료 시 스토리지 삭제 이벤트 분리 및 Outbox 기반 실패 처리 추가 #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "CEEZ-62-\uC0AC\uC9C4-\uC0AD\uC81C-\uD2B8\uB79C\uC7AD\uC158"
Conversation
Walkthrough앨범 만료 시 저장소 직접 삭제를 이벤트 기반으로 전환합니다. 사진 삭제는 트랜잭션 내에서 DB에서 제거하고, 커밋 후 Changes
Sequence Diagram(s)sequenceDiagram
actor Trigger
participant AlbumExpirationService
participant DB as Database
participant EventPublisher as ApplicationEventPublisher
participant EventHandler as AlbumStorageDeleteEventHandler
participant StorageUtil as ObjectStorageDeleteUtil
participant OutboxWriter as StorageDeleteOutboxWriter
Trigger->>AlbumExpirationService: expireAlbum()
AlbumExpirationService->>DB: delete photo records (transaction)
DB-->>AlbumExpirationService: confirm deletion
AlbumExpirationService->>EventPublisher: publish AlbumStorageDeleteEvent (AFTER_COMMIT)
Note over AlbumExpirationService: transaction commits
EventPublisher->>EventHandler: handle(event)
alt deletion succeeds
EventHandler->>StorageUtil: deletePhotoObjects(...) for each target
StorageUtil-->>EventHandler: success
else retries exhausted
EventHandler->>OutboxWriter: save(albumId, payloadJson, reason)
OutboxWriter->>DB: persist StorageDeleteOutbox (REQUIRES_NEW)
DB-->>OutboxWriter: saved
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✏️ Tip: You can disable this entire section by setting 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. Comment |
There was a problem hiding this 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 (3)
build.gradle (1)
36-37: 중복 의존성 선언 제거 필요
spring-boot-starter-aop가 라인 66에 이미 선언되어 있습니다. 중복 선언을 제거해 주세요.♻️ 제안된 수정
implementation 'org.springframework.retry:spring-retry' - implementation 'org.springframework.boot:spring-boot-starter-aop'src/main/java/com/cheeeese/album/application/AlbumStorageDeleteEventHandler.java (1)
51-61:@Transactional제거를 고려해보세요.
recover()메서드의@Transactional은 불필요합니다.outboxWriter.save()가 이미Propagation.REQUIRES_NEW로 자체 트랜잭션을 관리하고 있으므로, 여기서 별도 트랜잭션 경계를 설정할 필요가 없습니다.제안된 변경
@Recover - @Transactional public void recover(Exception e, AlbumStorageDeleteEvent event) {src/main/java/com/cheeeese/album/application/AlbumExpirationService.java (1)
116-122: 주석 처리된 코드 제거를 권장합니다.Line 118의
// photoRepository.deleteAll(photosToDelete);는 삭제해주세요. 사용하지 않는 주석 코드는 코드베이스를 혼란스럽게 만듭니다.제안된 변경
// DB 삭제(트랜잭션 내) if (!photosToDelete.isEmpty()) { - // photoRepository.deleteAll(photosToDelete); photoRepository.deleteAllInBatch(photosToDelete); log.info("[AlbumExpiration] Album id={} deleted photos count={}", album.getId(), photosToDelete.size()); }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
build.gradlesrc/main/java/com/cheeeese/album/application/AlbumExpirationService.javasrc/main/java/com/cheeeese/album/application/AlbumStorageDeleteEvent.javasrc/main/java/com/cheeeese/album/application/AlbumStorageDeleteEventHandler.javasrc/main/java/com/cheeeese/album/application/StorageDeleteOutboxWriter.javasrc/main/java/com/cheeeese/album/domain/StorageDeleteOutbox.javasrc/main/java/com/cheeeese/album/infrastructure/persistence/StorageDeleteOutboxRepository.javasrc/main/java/com/cheeeese/global/config/RetryConfig.javasrc/main/java/com/cheeeese/global/util/ObjectStorageDeleteUtil.java
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: dahyun24
Repo: Say-Cheeeese/BE PR: 113
File: src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoRepository.java:155-157
Timestamp: 2026-01-09T17:28:49.242Z
Learning: PhotoRepository의 findAllByAlbumId와 findAllByAlbumIdAndIdNotIn 메서드는 의도적으로 isDeleted 필터를 생략합니다. 이 메서드들은 AlbumExpirationService에서 7일 만료 후 앨범의 모든 사진(isDeleted = TRUE/FALSE 모두)을 물리적으로 삭제(하드 delete)하기 위해 사용되며, 프로젝트에서 유일한 하드 delete 로직입니다.
Learnt from: zyovn
Repo: Say-Cheeeese/BE PR: 114
File: src/main/java/com/cheeeese/album/application/AlbumService.java:234-246
Timestamp: 2026-01-12T16:03:32.619Z
Learning: 블랙리스트(추방) 처리 시 해당 사용자의 사진들은 하드 삭제(물리적 삭제)를 수행합니다. 이는 AlbumService의 removeUserFromAlbum 메서드에서 photoRepository.deleteAllByIds()를 통해 구현되며, 소프트 삭제가 아닌 의도적인 설계입니다.
📚 Learning: 2025-11-13T12:56:22.161Z
Learnt from: dahyun24
Repo: Say-Cheeeese/BE PR: 58
File: src/main/java/com/cheeeese/cheese4cut/application/Cheese4cutService.java:149-156
Timestamp: 2025-11-13T12:56:22.161Z
Learning: In src/main/java/com/cheeeese/cheese4cut/application/Cheese4cutService.java, the finalizeCheese4cut method intentionally re-sorts photos using findAllByIdInOrderByLikesDescCreatedDesc(request.photoIds()) instead of preserving the client's requested order. This is a defensive measure to ensure photos are always ordered by likes (DESC) and creation time (DESC), regardless of what order the client sends, preventing incorrect ordering from client errors.
Applied to files:
src/main/java/com/cheeeese/album/application/AlbumExpirationService.java
📚 Learning: 2026-01-09T17:28:49.242Z
Learnt from: dahyun24
Repo: Say-Cheeeese/BE PR: 113
File: src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoRepository.java:155-157
Timestamp: 2026-01-09T17:28:49.242Z
Learning: PhotoRepository의 findAllByAlbumId와 findAllByAlbumIdAndIdNotIn 메서드는 의도적으로 isDeleted 필터를 생략합니다. 이 메서드들은 AlbumExpirationService에서 7일 만료 후 앨범의 모든 사진(isDeleted = TRUE/FALSE 모두)을 물리적으로 삭제(하드 delete)하기 위해 사용되며, 프로젝트에서 유일한 하드 delete 로직입니다.
Applied to files:
src/main/java/com/cheeeese/album/application/AlbumExpirationService.javasrc/main/java/com/cheeeese/album/application/AlbumStorageDeleteEventHandler.java
📚 Learning: 2025-10-31T13:17:52.523Z
Learnt from: dahyun24
Repo: Say-Cheeeese/BE PR: 35
File: src/main/java/com/cheeeese/photo/application/PhotoService.java:46-52
Timestamp: 2025-10-31T13:17:52.523Z
Learning: In src/main/java/com/cheeeese/photo/application/PhotoService.java, the getRecentThumbnailUrls method intentionally returns only the first thumbnail URL when photos.size() < 5, rather than returning all available thumbnails. This is according to product requirements: 0 photos → empty list, 1-4 photos → single thumbnail (most recent), 5 photos → all 5 thumbnails.
Applied to files:
src/main/java/com/cheeeese/album/application/AlbumExpirationService.java
📚 Learning: 2026-01-12T16:03:32.619Z
Learnt from: zyovn
Repo: Say-Cheeeese/BE PR: 114
File: src/main/java/com/cheeeese/album/application/AlbumService.java:234-246
Timestamp: 2026-01-12T16:03:32.619Z
Learning: 블랙리스트(추방) 처리 시 해당 사용자의 사진들은 하드 삭제(물리적 삭제)를 수행합니다. 이는 AlbumService의 removeUserFromAlbum 메서드에서 photoRepository.deleteAllByIds()를 통해 구현되며, 소프트 삭제가 아닌 의도적인 설계입니다.
Applied to files:
src/main/java/com/cheeeese/album/application/AlbumExpirationService.javasrc/main/java/com/cheeeese/album/application/AlbumStorageDeleteEventHandler.java
🔇 Additional comments (11)
src/main/java/com/cheeeese/global/config/RetryConfig.java (1)
1-9: LGTM!Spring Retry 활성화를 위한 표준 설정입니다.
@Retryable어노테이션 사용을 위해 필요한 구성이 올바르게 추가되었습니다.src/main/java/com/cheeeese/album/application/AlbumStorageDeleteEvent.java (1)
5-14: LGTM!Java record를 활용한 깔끔한 도메인 이벤트 설계입니다. 불변 객체로 이벤트 데이터를 안전하게 전달할 수 있습니다.
src/main/java/com/cheeeese/album/domain/StorageDeleteOutbox.java (1)
10-41: LGTM!Outbox 패턴 구현을 위한 엔티티가 잘 설계되었습니다. 정적 팩토리 메서드와 protected 기본 생성자를 사용하여 객체 생성을 적절히 캡슐화했습니다.
테스트 시
LocalDateTime.now()직접 호출로 인해 시간 의존성이 생길 수 있습니다. 필요시Clock을 주입받는 방식을 고려할 수 있으나, 현재 Outbox 용도로는 충분합니다.src/main/java/com/cheeeese/album/infrastructure/persistence/StorageDeleteOutboxRepository.java (1)
1-7: LGTM!표준
JpaRepository인터페이스 구현입니다.향후 운영 시 배치 재처리를 위한 커스텀 쿼리 메서드(예:
findAllByCreatedAtBefore, 상태 기반 조회 등)가 필요할 수 있습니다. 현재 구현은 기본 기능으로 충분합니다.src/main/java/com/cheeeese/album/application/StorageDeleteOutboxWriter.java (1)
10-19: LGTM!Outbox Writer가
Propagation.REQUIRES_NEW를 사용하는 것은 올바른 설계입니다. 이를 통해 호출자의 트랜잭션 상태와 관계없이 outbox 엔트리가 독립적으로 저장됩니다.src/main/java/com/cheeeese/global/util/ObjectStorageDeleteUtil.java (2)
25-47: LGTM! Strict/Non-strict 패턴이 잘 분리되어 있습니다.기존 호출자는 영향받지 않으면서, 이벤트 핸들러에서 재시도가 필요한 경우 strict 버전을 사용할 수 있어 유연합니다.
68-86: LGTM!예외 메시지에 bucket과 key 정보가 포함되어 있어 디버깅에 유용합니다. 로그는
throwOnFailure값과 관계없이 항상 남기므로 모니터링에도 적합합니다.src/main/java/com/cheeeese/album/application/AlbumStorageDeleteEventHandler.java (2)
25-45: 부분 실패 시 재시도 동작 확인 필요루프 중간에 실패하면 이미 삭제된 객체들도 재시도 시 다시 삭제를 시도합니다. S3 delete는 idempotent하므로 기능상 문제는 없지만, 이 동작이 의도된 것인지 확인이 필요합니다.
또한
@TransactionalEventListener와@Retryable조합이 예상대로 동작하는지 테스트해보시기 바랍니다. Spring의 이벤트 리스너 프록시와 Retry 프록시가 동일 메서드에 적용될 때 순서에 따라 동작이 달라질 수 있습니다.
63-70: LGTM!JSON 직렬화 실패 시 fallback으로 최소한의 정보(albumId, count)를 보존하는 방어적 코딩이 좋습니다.
src/main/java/com/cheeeese/album/application/AlbumExpirationService.java (2)
124-132: LGTM! 이벤트 기반 접근 방식이 잘 설계되었습니다.트랜잭션 내에서 이벤트를 발행하고
AFTER_COMMIT핸들러에서 스토리지 삭제를 처리하는 방식이 올바릅니다. 이를 통해:
- DB 삭제가 커밋된 후에만 스토리지 삭제가 실행됩니다
- 트랜잭션 롤백 시 스토리지 삭제가 수행되지 않아 일관성이 유지됩니다
Based on learnings,
findAllByAlbumId가isDeleted필터 없이 모든 사진을 조회하는 것은 의도된 하드 삭제 로직입니다.
106-114: LGTM!삭제 전에 URL 정보를 수집하여 이벤트 payload를 구성하는 것이 올바른 순서입니다. DB에서 삭제된 후에는 URL 정보에 접근할 수 없기 때문입니다.
zyovn
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고하셨습니다~~
나중에 outbox 관련 삭제 스케쥴러나 ttl 추가해도 좋을 것 같아요!
뭔가 계속 영구 보관할 필요는 없을 것 같으니..
🔗 연관된 이슈
🚀 변경 유형
📝 작업 내용
CEEZ-35에서의 작업
CEEZ-62에 추가한 내용
AlbumExpirationService에서는AlbumStorageDeleteEvent발행으로 위임storage_delete_outbox테이블에 payload 저장💬 리뷰 요구사항
📜 리뷰 규칙
Reviewer는 아래 P5 Rule을 참고하여 리뷰를 진행합니다.
P5 Rule을 통해 Reviewer는 Reviewee에게 리뷰의 의도를 보다 정확히 전달할 수 있습니다.
Summary by CodeRabbit
Refactor
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.