-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] 피드 삭제 api 개발 및 post soft delete 전략 도입 #173
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
Changes from all commits
ad142e8
db6bbd7
23532af
7a0c157
21c6349
00e3554
fbf6677
c2005a0
bba7132
5bc02f6
2253804
4974b39
67bd0b0
1f62822
8af91a9
2329047
5bfeab9
56a466a
3ce0ae0
c394231
f1338dc
163ec4f
f0c11af
120d2d8
6498788
40cc963
e69a8f2
a888a83
67812e5
2319004
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,9 +3,17 @@ | |
| import konkuk.thip.comment.adapter.out.jpa.CommentJpaEntity; | ||
| import konkuk.thip.common.entity.StatusType; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Modifying; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| public interface CommentJpaRepository extends JpaRepository<CommentJpaEntity, Long>, CommentQueryRepository { | ||
| Optional<CommentJpaEntity> findByCommentIdAndStatus(Long commentId, StatusType status); | ||
|
|
||
| @Modifying | ||
| @Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId = :postId") | ||
| void softDeleteAllByPostId(@Param("postId") Long postId); | ||
hd0rable marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+15
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오호 이러면 단일 Comment 삭제시에는 그냥 delete로 SQLDelete 어노테이션을 사용하고 다른 엔티티에 연관된 Comment 삭제시에는 위 쿼리를 사용해서 벌크 삭제를 하는 식이겠네요! 좋습니다!! |
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,4 +30,14 @@ public interface CommentLikeJpaRepository extends JpaRepository<CommentLikeJpaEn | |
|
|
||
| @Query("SELECT c.commentJpaEntity.commentId FROM CommentLikeJpaEntity c WHERE c.userJpaEntity.userId = :userId AND c.commentJpaEntity.commentId IN :commentIds") | ||
| Set<Long> findCommentIdsLikedByUser(@Param("commentIds") Set<Long> commentIds, @Param("userId") Long userId); | ||
|
|
||
| @Modifying | ||
| @Query(""" | ||
| DELETE FROM CommentLikeJpaEntity cl | ||
| WHERE cl.commentJpaEntity.commentId IN ( | ||
| SELECT c.commentId FROM CommentJpaEntity c | ||
| WHERE c.postJpaEntity.postId = :postId | ||
| ) | ||
| """) | ||
| void deleteAllByPostId(@Param("postId") Long postId); | ||
hd0rable marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| @Entity | ||
|
|
@@ -34,6 +35,14 @@ public class FeedJpaEntity extends PostJpaEntity { | |
| @OneToMany(mappedBy = "postJpaEntity", cascade = CascadeType.ALL, orphanRemoval = true) | ||
| private List<ContentJpaEntity> contentList; | ||
|
|
||
| // 삭제용 피드 저장 양방향 매핑 관계 | ||
| @OneToMany(mappedBy = "feedJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true) | ||
| private List<SavedFeedJpaEntity> savedFeeds = new ArrayList<>(); | ||
|
|
||
| // 삭제용 피드 태그 양방향 매핑 관계 | ||
| @OneToMany(mappedBy = "feedJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true) | ||
| private List<FeedTagJpaEntity> feedTags = new ArrayList<>(); | ||
|
Comment on lines
+38
to
+44
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LGTM 확인했습니다
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이러면 피드 삭제시에 이와 1:n 으로 연관된 친구들은 실제로 삭제가 되고, feed에 해당하는 post 는 soft delete 가 되는 구조겠네요. 굳굳
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OneToMany는 소프트 딜리트 전파가 된다고 하셨던 것 같은데 맞나요?? @hd0rable
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 엇 아니용 전파안되고 그 엔티티에 sqlDelete 어노테이션 적용안되어있으면 하드딜리트되는 걸 확인했습니당
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그래서 SavedFeed,FeedTag는 엔티티자체에 소프트 딜리트 어노테이션이 적용되지않기때문에 하드딜리트됩니닷
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 엇 저는 연쇄 삭제가 일어날때 SQLDelete가 달려있는 엔티티는 소프트 딜리트가 되는게 맞는지 궁금합니다!
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 맞습니다~~
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 화긴이요~~ |
||
|
|
||
| @Builder | ||
| public FeedJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Boolean isPublic, int reportCount, BookJpaEntity bookJpaEntity, List<ContentJpaEntity> contentList) { | ||
| super(content, likeCount, commentCount, userJpaEntity); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,12 +11,16 @@ | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| public interface SavedFeedJpaRepository extends JpaRepository<SavedFeedJpaEntity, Long> { | ||||||||||||||||||||||||||||||||
| @Modifying | ||||||||||||||||||||||||||||||||
| @Query(value = "DELETE FROM saved_feeds WHERE user_id = :userId AND post_id = :feedId", nativeQuery = true) | ||||||||||||||||||||||||||||||||
| @Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.userJpaEntity.userId = :userId AND sf.feedJpaEntity.postId = :feedId") | ||||||||||||||||||||||||||||||||
| void deleteByUserIdAndFeedId(@Param("userId") Long userId, @Param("feedId") Long feedId); | ||||||||||||||||||||||||||||||||
hd0rable marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Query(value = "SELECT * FROM saved_feeds WHERE user_id = :userId", nativeQuery = true) | ||||||||||||||||||||||||||||||||
| @Query("SELECT sf FROM SavedFeedJpaEntity sf WHERE sf.userJpaEntity.userId = :userId") | ||||||||||||||||||||||||||||||||
| List<SavedFeedJpaEntity> findAllByUserId(@Param("userId") Long userId); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Query("SELECT s.feedJpaEntity.postId FROM SavedFeedJpaEntity s WHERE s.userJpaEntity.userId = :userId AND s.feedJpaEntity.postId IN :feedIds") | ||||||||||||||||||||||||||||||||
| Set<Long> findSavedFeedIdsByUserIdAndFeedIds(@Param("userId") Long userId, @Param("feedIds") Set<Long> feedIds); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Modifying | ||||||||||||||||||||||||||||||||
| @Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.feedJpaEntity.postId = :feedId") | ||||||||||||||||||||||||||||||||
| int deleteAllByFeedId(@Param("feedId") Long feedId); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain신규 bulk delete: clearAutomatically 추가 권장 + 설계 일관성 확인
권장 변경안: - @Modifying
+ @Modifying(clearAutomatically = true)
@Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.feedJpaEntity.postId = :feedId")
int deleteAllByFeedId(@Param("feedId") Long feedId);추가 확인 포인트:
🏁 Script executed: #!/bin/bash
# 1) 엔티티 필드 경로 검증(간이): 관련 엔티티의 필드명을 확인
rg -n --hidden --ignore-case "class SavedFeedJpaEntity|userJpaEntity|feedJpaEntity" -g "**/*.java"
# 2) soft delete 적용 대상 확인: SavedFeed/Content/FeedTag/Feed의 @SQLDelete/@Where 존재 여부
rg -n "@SQLDelete|@Where" -g "**/*JpaEntity.java"
# 3) 본 메서드 호출부 확인(일관된 사용/트랜잭션 경계 확인)
rg -n "deleteAllByFeedId\\(" -g "**/*.java"
# 4) Feed 삭제 유스케이스/어댑터 트랜잭션 확인
rg -n "@Transactional|FeedDeleteService|FeedCommandPersistenceAdapter" -g "src/**/*.java"Length of output: 164629 bulk 삭제 후 1차 캐시 자동 정리 적용 권장
권장 변경안: - @Modifying
+ @Modifying(clearAutomatically = true)
@Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.feedJpaEntity.postId = :feedId")
int deleteAllByFeedId(@Param("feedId") Long feedId);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Modifying(clearAutomatically = true) 이 어노테이션이 무슨 역할을 하는데? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 왜 안알려줘
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @coderabbitai There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @buzz0331, 주요 기능1. 영속성 컨텍스트 자동 정리
왜 필요한가?캐시 불일치 문제 방지: // 예시: clearAutomatically = false (기본값)인 경우
SavedFeedJpaEntity entity = repository.findById(1L); // 1차 캐시에 저장됨
repository.deleteAllByFeedId(feedId); // DB에서는 삭제되지만 캐시는 그대로
SavedFeedJpaEntity stale = repository.findById(1L); // 캐시에서 가져옴 (삭제된 데이터!)clearAutomatically = true 설정 시: SavedFeedJpaEntity entity = repository.findById(1L);
repository.deleteAllByFeedId(feedId); // DB 삭제 + 캐시 자동 정리
SavedFeedJpaEntity fresh = repository.findById(1L); // DB에서 새로 조회 (null 반환)현재 코드에 적용이 필요한 이유
특히
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @coderabbitai 그렇다면 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: Length of output: 5436 🌐 Web query: 💡 Result: The default values for the Spring Data JPA @Modifying annotation are clearAutomatically = false and flushAutomatically = false, meaning neither automatic clearing nor flushing of the persistence context is performed unless configured explicitly[6][7][8]. Context and Supporting Details:
Why change defaults:
Clarification of Ambiguity:
If further clarification is needed on a specific version or scenario, please specify. Citations:
@buzz0331, 네, 기본값 정리
현재 코드베이스 상황코드베이스를 확인한 결과:
영향받는 메서드들:
잠재적 문제기본값 사용 시 다음과 같은 상황이 발생할 수 있습니다: // 예시: FeedDeleteService 내부
Feed feed = feedRepository.findById(feedId); // 1차 캐시에 로드
savedFeedRepository.deleteAllByFeedId(feedId); // DB에서만 삭제, 캐시는 유지
// feed와 연관된 SavedFeed들이 캐시에 남아있어 불일치 발생 가능특히 피드 삭제처럼 여러 연관 엔티티를 한 트랜잭션에서 처리하는 경우, 캐시 불일치로 인한 예기치 않은 동작이 발생할 수 있습니다. |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package konkuk.thip.feed.application.port.in; | ||
|
|
||
|
|
||
| public interface FeedDeleteUseCase { | ||
| void deleteFeed(Long feedId, Long userId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package konkuk.thip.feed.application.service; | ||
|
|
||
| import jakarta.transaction.Transactional; | ||
| import konkuk.thip.comment.application.port.out.CommentCommandPort; | ||
| import konkuk.thip.feed.application.port.in.FeedDeleteUseCase; | ||
| import konkuk.thip.feed.application.port.out.FeedCommandPort; | ||
| import konkuk.thip.feed.domain.Feed; | ||
| import konkuk.thip.post.application.port.out.PostLikeCommandPort; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class FeedDeleteService implements FeedDeleteUseCase { | ||
|
|
||
| private final FeedCommandPort feedCommandPort; | ||
| private final CommentCommandPort commentCommandPort; | ||
| private final PostLikeCommandPort postLikeCommandPort; | ||
|
|
||
| @Override | ||
| @Transactional | ||
| public void deleteFeed(Long feedId, Long userId) { | ||
|
|
||
| // 1. 피드 조회 및 검증 | ||
| Feed feed = feedCommandPort.getByIdOrThrow(feedId); | ||
|
|
||
| // 2. 피드 삭제 권한 검증 | ||
| feed.validateDeletable(userId); | ||
|
|
||
| // TODO S3 이미지 삭제 이벤트 기반 처리 or 배치 삭제 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 굳 이건 동기적으로 삭제하는게 아니라 일단 TODO로 정의하고 리펙단계에서 일괄적으로 처리하시죠 |
||
| // 3. 피드 삭제 | ||
| commentCommandPort.softDeleteAllByPostId(feedId); | ||
| postLikeCommandPort.deleteAllByPostId(feedId); | ||
| feedCommandPort.delete(feed); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -129,10 +129,14 @@ public void validateLike(Long userId){ | |
|
|
||
| private void validateCreator(Long userId) { | ||
| if (!this.creatorId.equals(userId)) { | ||
| throw new InvalidStateException(FEED_ACCESS_FORBIDDEN, new IllegalArgumentException("피드 작성자만 피드를 수정할 수 있습니다.")); | ||
| throw new InvalidStateException(FEED_ACCESS_FORBIDDEN, new IllegalArgumentException("피드 작성자만 피드를 수정/삭제할 수 있습니다.")); | ||
| } | ||
| } | ||
|
|
||
| public void validateDeletable(Long userId) { | ||
| validateCreator(userId); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LGTM 이 메서드 굉장히 유용하게 쓰이네요 굳굳 |
||
| } | ||
|
|
||
| public void updateContent(Long userId, String newContent) { | ||
| validateCreator(userId); | ||
| this.content = newContent; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.