[Feat] 피드 삭제 api 개발 및 post soft delete 전략 도입#173
Conversation
Walkthrough피드 삭제 기능이 추가되어 컨트롤러, 유스케이스, 서비스, 퍼시스턴스, JPA 엔티티, 레포지토리, 예외·스웨거 정의 및 관련 통합·단위 테스트가 추가/수정되었습니다. 삭제 시 연관된 댓글·댓글좋아요·포스트좋아요·태그·컨텐츠·저장피드 등이 일괄 삭제 또는 soft delete 됩니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant FeedCommandController
participant FeedDeleteService
participant FeedCommandPort
participant CommentCommandPort
participant PostLikeCommandPort
Client->>FeedCommandController: DELETE /feeds/{feedId} (userId)
FeedCommandController->>FeedDeleteService: deleteFeed(feedId, userId)
FeedDeleteService->>FeedCommandPort: getByIdOrThrow(feedId)
FeedDeleteService->>FeedCommandPort: feed.validateDeletable(userId)
FeedDeleteService->>CommentCommandPort: softDeleteAllByPostId(feedId)
FeedDeleteService->>PostLikeCommandPort: deleteAllByPostId(feedId)
FeedDeleteService->>FeedCommandPort: delete(feed) -- 삭제: 태그/컨텐츠/저장피드 삭제 후 feed soft-delete
FeedCommandController-->>Client: 204 No Content
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Assessment against linked issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ 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)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java (1)
13-14: VoteJpaEntity와 동일한 CASCADE 설정 호환성 확인이 필요합니다.
RecordJpaEntity에도@OnDelete(action = OnDeleteAction.CASCADE)애노테이션이 추가되었는데, 이는VoteJpaEntity와 동일한 패턴입니다. soft delete 전략과 DATABASE 레벨 CASCADE 삭제 간의 호환성을 확인해야 합니다.이전에
VoteJpaEntity에서 요청한 검증이RecordJpaEntity에도 동일하게 적용됩니다.Also applies to: 21-21
🧹 Nitpick comments (3)
src/main/java/konkuk/thip/feed/application/port/in/FeedDeleteUseCase.java (1)
4-6: 피드 삭제 Use Case 인터페이스가 잘 설계되었습니다.Clean Architecture 원칙을 따르는 명확한 인터페이스입니다.
feedId와userId파라미터를 통해 삭제할 피드와 권한 검증에 필요한 정보를 모두 제공합니다.향후 유지보수성을 위해 JavaDoc 주석 추가를 고려해보세요:
+/** + * 피드 삭제를 처리하는 Use Case 인터페이스 + */ public interface FeedDeleteUseCase { + /** + * 피드를 삭제합니다. + * @param feedId 삭제할 피드 ID + * @param userId 요청한 사용자 ID (권한 검증용) + * @throws BusinessException 권한이 없거나 피드를 찾을 수 없는 경우 + */ void deleteFeed(Long feedId, Long userId); }src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java (1)
107-107: 세미콜론 중복 제거Line 107에 불필요한 세미콜론이 중복되어 있습니다.
- tagJpaRepository.deleteAllInBatch();; + tagJpaRepository.deleteAllInBatch();src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (1)
46-48: 피드 태그 연관관계 매핑이 올바름주석에 "삭제요"라는 오타가 있지만, 매핑 설정 자체는 CascadeType.REMOVE와 orphanRemoval = true로 적절히 구성되었습니다.
- // 삭제요 피드 태그 양방향 매핑 관계 + // 삭제용 피드 태그 양방향 매핑 관계
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java(2 hunks)src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java(3 hunks)src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java(2 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java(3 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/in/FeedDeleteUseCase.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java(1 hunks)src/main/java/konkuk/thip/feed/domain/Feed.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java(2 hunks)src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java(1 hunks)src/main/java/konkuk/thip/vote/adapter/out/jpa/VoteJpaEntity.java(1 hunks)src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java(2 hunks)src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteAPITest.java(3 hunks)src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java(1 hunks)src/test/java/konkuk/thip/feed/domain/FeedTest.java(1 hunks)src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java(1 hunks)src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-03T03:05:05.031Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
Applied to files:
src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java
📚 Learning: 2025-06-29T09:47:31.299Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
Applied to files:
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.javasrc/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java
📚 Learning: 2025-07-23T17:41:55.507Z
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.
Applied to files:
src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteAPITest.javasrc/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java
🔇 Additional comments (31)
src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java (1)
195-198: 피드 삭제 API에 대한 Swagger 문서화가 적절합니다.FEED_DELETE enum 상수가 올바른 에러 코드들(FEED_NOT_FOUND, FEED_ACCESS_FORBIDDEN)과 함께 추가되어, 피드 삭제 API의 예외 상황을 잘 문서화하고 있습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java (2)
9-10: Optional import 추가가 적절합니다.새로 추가된 메소드의 반환 타입을 위해 필요한 import입니다.
19-19: Soft delete 전략을 위한 메소드가 적절하게 추가되었습니다.
findByPostIdAndStatus메소드는 Spring Data JPA의 메소드 네이밍 컨벤션을 따르며, Optional 반환 타입으로 null 안전성을 보장합니다. StatusType을 활용한 soft delete 전략에 필요한 기능입니다.src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java (1)
80-80: 테스트 정리 로직이 batch 삭제로 개선되었습니다.
deleteAllInBatch()는 단일 쿼리로 여러 엔티티를 삭제하여 성능상 이점을 제공합니다. Soft delete 전략과 cascade 관계가 복잡해진 상황에서 적절한 변경입니다.src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (1)
19-19: Feed 삭제를 위한 CommandPort 메소드가 적절하게 추가되었습니다.
delete(Feed feed)메소드는 CQRS 패턴의 CommandPort 컨벤션에 부합하며, 도메인 엔티티를 매개변수로 받는 올바른 구조입니다. 피드 삭제 기능 구현에 필요한 포트 메소드입니다.src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java (1)
81-81: 테스트 정리 성능이 batch 삭제로 개선되었습니다.다른 테스트 클래스들과 일관되게
deleteAllInBatch()로 변경되어 테스트 실행 성능이 향상되었습니다. Soft delete 전략 도입에 맞춘 적절한 개선입니다.src/main/java/konkuk/thip/feed/domain/Feed.java (2)
132-132: 메시지 업데이트가 적절합니다.삭제 기능 추가에 맞춰 예외 메시지가 "수정/삭제"로 업데이트되어 사용자에게 명확한 권한 정보를 제공합니다.
136-138: 삭제 권한 검증 메서드가 잘 구현되었습니다.현재는
validateCreator를 호출하는 단순한 구현이지만, 도메인의 의도를 명확히 하고 향후 삭제 전용 검증 로직을 추가할 수 있는 확장성을 제공합니다.src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteAPITest.java (3)
115-115: 테스트 검증 로직 개선이 적절합니다.
findByCommentIdAndStatus메서드를 사용하여 soft delete 검증을 단일 assertion으로 간소화한 것이 좋습니다. 기존의 2단계 검증보다 가독성과 효율성이 향상되었습니다.
135-135: 일관된 검증 방식 적용이 우수합니다.대댓글 삭제 테스트에서도 동일한 패턴으로 soft delete 검증을 개선한 것이 좋습니다. 테스트 코드의 일관성이 유지되었습니다.
157-157: 검증 로직 통일이 적절합니다.모든 댓글 삭제 테스트에서 동일한 검증 방식을 사용하여 코드 품질과 유지보수성이 향상되었습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (3)
26-26: 필요한 import 추가가 적절합니다.ACTIVE 상태 상수를 import하여 soft delete 전략을 지원하는 변경이 좋습니다.
44-44: Soft delete 전략에 맞는 조회 로직 개선입니다.
findByPostIdAndStatus를 사용하여 활성 상태의 피드만 조회하도록 변경한 것이 soft delete 전략에 적합합니다.
111-117: 피드 삭제 구현이 적절합니다.피드 존재 여부 검증 후 삭제하는 로직이 간단하고 명확하게 구현되었습니다. 예외 처리도 적절합니다.
src/test/java/konkuk/thip/feed/domain/FeedTest.java (1)
350-358: 삭제 권한 검증 테스트가 적절합니다.작성자가 아닌 유저의 삭제 시도 시
FEED_ACCESS_FORBIDDEN예외가 발생하는지 검증하는 로직이 올바르게 구현되었습니다.src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java (2)
13-14: 필요한 import 추가가 적절합니다.ArrayList와 List import 추가가 새로운 필드를 위해 필요한 변경입니다.
58-60: Cascade delete 전략을 위한 양방향 매핑이 적절합니다.댓글 삭제 시 관련 좋아요도 함께 삭제되도록
CascadeType.REMOVE와orphanRemoval = true설정이 올바르게 구현되었습니다. 주석으로 용도를 명시한 것도 좋습니다.src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java (1)
1-30: 피드 삭제 서비스 구현이 우수합니다.서비스 구조와 로직이 잘 설계되었습니다:
@Transactional어노테이션으로 트랜잭션 경계 설정이 적절- 3단계 삭제 로직이 명확하고 순서가 적절 (조회 → 권한검증 → 삭제)
- TODO 주석으로 S3 이미지 삭제에 대한 향후 개선 계획을 명시한 것이 좋음
- 의존성 주입과 예외 처리가 적절하게 구현됨
src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java (4)
4-4: 적절한 임포트 추가소프트 삭제와 연관관계 매핑을 위한 필요한 임포트들이 정확히 추가되었습니다.
Also applies to: 10-13
21-21: 소프트 삭제 전략이 올바르게 구현됨@SQLDelete 애노테이션을 통해 물리적 삭제 대신 status 컬럼을 'INACTIVE'로 업데이트하는 소프트 삭제 전략이 적절히 구현되었습니다.
48-50: 포스트 좋아요 연관관계 매핑이 적절함CascadeType.REMOVE와 orphanRemoval = true 설정으로 게시물 삭제 시 관련 좋아요도 함께 삭제되도록 올바르게 구성되었습니다.
44-46: 댓글 엔티티 소프트 삭제 적용 확인 완료댓글만을 물리 삭제하지 않고 상태만 ‘INACTIVE’로 변경하도록
@SQLDelete(sql = "UPDATE comments SET status = 'INACTIVE' WHERE comment_id = ?")가CommentJpaEntity에 선언되어 있습니다.
따라서cascade = CascadeType.REMOVE+orphanRemoval = true설정 시에도 소프트 삭제가 정상 동작하여 추가 조치가 필요 없습니다.src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java (4)
44-48: 통합 테스트 설정이 적절함@SpringBootTest, @activeprofiles("test"), @AutoConfigureMockMvc 설정이 피드 삭제 API 통합 테스트에 적합하게 구성되었습니다.
76-93: 테스트 데이터 설정이 포괄적임피드 삭제 시 연쇄 삭제되어야 하는 모든 관련 엔티티들(태그, 댓글, 좋아요, 저장된 피드 등)을 적절히 생성하여 완전한 테스트 시나리오를 구성했습니다.
95-110: 테스트 정리 로직이 소프트 삭제 전략에 맞게 구현됨소프트 삭제로 인해 기존 deleteAll() 대신 deleteAllInBatch()를 사용하여 테스트 데이터를 물리적으로 삭제하는 것이 올바른 접근입니다.
112-141: 삭제 검증 로직이 철저함소프트 삭제된 피드(INACTIVE 상태)와 연관 엔티티들의 물리적 삭제를 모두 검증하여 삭제 API의 동작을 완전히 확인했습니다.
src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java (2)
18-18: 의존성 추가가 적절함FeedDeleteUseCase 의존성이 올바르게 추가되고 생성자 주입을 통해 관리되고 있습니다.
Also applies to: 39-39
96-107: 피드 삭제 엔드포인트가 잘 구현됨OpenAPI 문서화, 예외 처리 설명, 파라미터 설정이 다른 엔드포인트와 일관성 있게 구현되었으며, 삭제 후 적절한 응답을 반환합니다.
src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (3)
9-9: 삭제 지원을 위한 임포트가 적절히 추가됨연관 엔티티 및 데이터베이스 레벨 cascade 삭제를 위한 필요한 임포트들이 정확히 추가되었습니다.
Also applies to: 15-16, 18-18
26-26: 데이터베이스 레벨 cascade 삭제 설정이 적절함@onDelete(action = OnDeleteAction.CASCADE) 애노테이션을 통해 데이터베이스 레벨에서도 연쇄 삭제가 보장되도록 설정되었습니다.
42-44: 저장된 피드 연관관계 매핑이 올바름삭제용 양방향 매핑이 CascadeType.REMOVE와 orphanRemoval = true로 적절히 설정되어 피드 삭제 시 저장 관계도 함께 정리됩니다.
src/main/java/konkuk/thip/vote/adapter/out/jpa/VoteJpaEntity.java
Outdated
Show resolved
Hide resolved
src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java(2 hunks)src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java(1 hunks)src/test/java/konkuk/thip/feed/domain/FeedTest.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/test/java/konkuk/thip/feed/domain/FeedTest.java
- src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-23T17:41:55.507Z
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.
Applied to files:
src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java
⏰ 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 (4)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java (4)
1-48: 테스트 클래스 구조와 import 구성이 적절합니다.통합 테스트에 필요한 모든 의존성이 올바르게 import되었고, Spring Boot 테스트 어노테이션 구성도 적절합니다. 한국어 DisplayName도 명확하게 테스트 목적을 설명하고 있습니다.
50-75: 의존성 주입이 체계적으로 구성되었습니다.피드 삭제와 관련된 모든 Repository가 적절히 주입되어 있어 테스트 데이터 설정과 검증이 가능합니다. 필드 선언도 테스트에 필요한 엔티티들로 구성되어 있습니다.
76-93: 테스트 데이터 설정이 포괄적이고 현실적입니다.피드 삭제 시나리오를 검증하기 위해 필요한 모든 연관 엔티티(사용자, 카테고리, 도서, 태그, 피드, 댓글, 좋아요, 저장된 피드)가 적절히 생성되었습니다. 특히 cascading delete 동작을 확인하기 위한 복합적인 관계 설정이 잘 되어 있습니다.
112-141: 테스트 검증 로직이 soft delete 전략을 정확히 검증합니다.각 연관 엔티티의 삭제 동작(soft delete vs hard delete)을 명확히 구분하여 검증하고 있습니다. 특히 피드와 댓글은 INACTIVE 상태로 soft delete되고, 매핑 테이블들은 완전히 삭제되는 것을 올바르게 확인하고 있습니다.
| } | ||
|
|
||
| public void validateDeletable(Long userId) { | ||
| validateCreator(userId); |
There was a problem hiding this comment.
LGTM 이 메서드 굉장히 유용하게 쓰이네요 굳굳
| // 2. 피드 삭제 권한 검증 | ||
| feed.validateDeletable(userId); | ||
|
|
||
| // TODO S3 이미지 삭제 이벤트 기반 처리 or 배치 삭제 |
There was a problem hiding this comment.
굳 이건 동기적으로 삭제하는게 아니라 일단 TODO로 정의하고 리펙단계에서 일괄적으로 처리하시죠
| // 삭제용 피드 저장 양방향 매핑 관계 | ||
| @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<>(); |
There was a problem hiding this comment.
이러면 피드 삭제시에 이와 1:n 으로 연관된 친구들은 실제로 삭제가 되고, feed에 해당하는 post 는 soft delete 가 되는 구조겠네요. 굳굳
There was a problem hiding this comment.
OneToMany는 소프트 딜리트 전파가 된다고 하셨던 것 같은데 맞나요?? @hd0rable
There was a problem hiding this comment.
엇 아니용 전파안되고 그 엔티티에 sqlDelete 어노테이션 적용안되어있으면 하드딜리트되는 걸 확인했습니당
There was a problem hiding this comment.
그래서 SavedFeed,FeedTag는 엔티티자체에 소프트 딜리트 어노테이션이 적용되지않기때문에 하드딜리트됩니닷
There was a problem hiding this comment.
엇 저는 연쇄 삭제가 일어날때 SQLDelete가 달려있는 엔티티는 소프트 딜리트가 되는게 맞는지 궁금합니다!
| @DiscriminatorValue("FEED") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @OnDelete(action = OnDeleteAction.CASCADE) |
There was a problem hiding this comment.
오호 찾아보니 해당 어노테이션은 joined 전략으로 연결된 posts 테이블의 hard delete 가 발생할 경우, 이와 연관된 row의 hard delete 가 발생하도록 하는 hibernate 의 어노테이션이네요
그런데 저희는 posts table 에서 soft delete 를 적용하니(= DB에 delete 쿼리가 아니라 update 쿼리가 날라가니) 실제로 @onDelete 가 동작하지는 않을 거 같은데, 이는 가독성을 위해 추가하신 어노테이션인 걸까요?? 궁금해서 여쭤봅니다
There was a problem hiding this comment.
아니요.. 이어노테이션을 적용하지않으면 post는 sotf delete 되지만 해당 자식 테이블인 record,vote,feed의 row가 사라지는 문제가 발생해서 이 처리를 해줘야한다고하네요.. 실제로 이부분에서 많이 헤매서 개발속도가 늦어졌습니다.. 관련하여 노션에 문서화해두었으니 확인해주시면 감사하겠습니다!!
There was a problem hiding this comment.
저도 찾아보니 @OnDelete 어노테이션은 하드 딜리트를 전파하는 기능이지, 소프트 딜리트를 전파하는 기능이 아니어서 삭제 전파 용도로는 동작하지 않는 것이 맞는 것 같습니다.
그렇다면 왜 @OnDelete를 붙였을때는 Feed의 row가 삭제되지 않고, 붙이지 않았을 때는 Feed의 row가 삭제되는지를 찾아봤습니다. 원인은 Hibernate의 JOINED 상속시 삭제 처리 순서에 있었습니다.
Hibernate에서는 JOINED 전략의 하위 엔티티에 대해 삭제할 때 다음 순서로 동작합니다.
- 서브 테이블 DELETE
- 부모 테이블 DELETE
현재 저희는 1번 단계에서 @SQLDelete가 적용되어 있지 않기 때문에(Feed 테이블에 status가 없어 적용 불가) Feed row가 하드 딜리트로 삭제됩니다.
이후 2번 단계에서는 부모(Post)에 @SQLDelete가 적용되어 있어 실제 DELETE 대신 UPDATE로 status만 INACTIVE로 변경됩니다.
결론적으로, 현재 희진님이 추가하신 @onDelete는 DB 연쇄 삭제 용도라기보다는 서브 테이블 삭제를 Hibernate가 생략하도록 만드는 효과로 동작하고 있다고 보는 게 맞을 것 같습니다.
지금은 원하는 동작이 나오고 있으니 그대로 둬도 무방하겠지만, @SQLDelete와 @OnDelete는 철학이 다르기 때문에 혼용 시 주의가 필요해보입니다.
개인적으로는 Post ↔ Feed, Record, Vote 같이 상속 관계에 있는 엔티티에서는 @SQLDelete를 제거하고, delete 호출 시 JPQL로 명시적으로 UPDATE를 날리는 방식이 더 명확하지 않을까 싶은데..
@hd0rable @seongjunnoh 두 분 의견이 궁금합니다!!
There was a problem hiding this comment.
오 자세한 설명 감사합니다!!!
@onDelete 어노테이션 덕분에 피드 -> 포스트 로의 delete 연산이 수행된 것이 아니라, 포스트 -> 피드 로의 delete 연산이 발생하는데, 이때 포스트는 delete 쿼리가 나가는 hard delete 가 아니므로 결론적으로 피드에도 delete 쿼리가 나가지 않아 피드의 row가 삭제되지 않는 상황이군요!!
그러면 현준님이 제안하신 방식으로 피드 삭제를 진행한다면
- 영속성 adapter 의 delete 메서드 내부에서 FeedJpaEntity 의 부모인 PostJpaEntity 에 대하여 status update 메서드 호출(@SQLDelete 사용 X)
- 이후 FeedJpaEntity 와 연관관계를 맺고 있는 모든 엔티티를 삭제
이런 플로우가 되는 걸까요??
2번의 과정을 위해 추가적인 코드가 필요하겠지만, 이 방식이 더 명시적이고 덜 헷갈릴 것 같습니다!! (잘 모르는 jpa 기술들에 의존하니 이해하기도 어렵네요 하하)
추가로 이렇게 수정하면 @onetomany CascadeType.REMOVE 속성에 의해 연관된 엔티티들을 삭제하는 것이 아니므로, 현준님이 리뷰에서 언급하신 1:n 관계인 엔티티들 삭제시 n+1 문제 발생 도 해결할 수 있을 것 같습니다!!
-> 연관된 모든 엔티티들 조회 + 이들 일괄 삭제 : 이렇게 2번의 쿼리로 해결되지 않을까요 ??
There was a problem hiding this comment.
현준님이 제안하신대로 @SQLDelete 없이 post의 status를 업데이트 한 후 save하여 변경감지로 인해 피드가 소프트 딜리트 되도록 수정했습니다!
추가로 이렇게 수정하면 @onetomany CascadeType.REMOVE 속성에 의해 연관된 엔티티들을 삭제하는 것이 아니므로, 현준님이 리뷰에서 언급하신 1:n 관계인 엔티티들 삭제시 n+1 문제 발생 도 해결할 수 있을 것 같습니다!!
-> 연관된 모든 엔티티들 조회 + 이들 일괄 삭제 : 이렇게 2번의 쿼리로 해결되지 않을까요 ??
이부부분에 대해서는 피드 삭제 시의 연관된 모든 엔티티를 배치 쿼리로 삭제하므로 연관된 엔티티당 1회 쿼리가 발생하여
피드 댓글의 좋아요 DELETE 1쿼리 (서브쿼리로 일괄 삭제)
피드 댓글 소프트 딜리트 UPDATE 1쿼리
피드 좋아요 DELETE 1쿼리
피드 태그 관계 DELETE 1쿼리
피드 콘텐츠 DELETE 1쿼리
피드 저장 관계 DELETE 1쿼리
피드 쿼리 소프트 딜리트 UPDATE 1쿼리
| @Override | ||
| public Optional<Feed> findById(Long id) { | ||
| return feedJpaRepository.findById(id) | ||
| return feedJpaRepository.findByPostIdAndStatus(id, ACTIVE) |
| // 삭제용 게시물 댓글 양방향 매핑 관계 | ||
| @OneToMany(mappedBy = "postJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true) | ||
| private List<CommentJpaEntity> commentList = new ArrayList<>(); | ||
|
|
||
| // 삭제용 게시물 좋아요 양방향 매핑 관계 | ||
| @OneToMany(mappedBy = "postJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true) | ||
| private List<PostLikeJpaEntity> postLikeList = new ArrayList<>(); |
| // then: 1) 피드 soft delete (status=INACTIVE) | ||
| assertThat(feedJpaRepository.findByPostIdAndStatus(feed.getPostId(), INACTIVE)).isPresent(); | ||
|
|
||
| // 2) 피드 태그 관계 삭제 | ||
| assertTrue(feedTagJpaRepository.findAll().isEmpty()); | ||
|
|
||
| // 3) 콘텐츠 삭제 | ||
| assertTrue(contentJpaRepository.findAll().isEmpty()); | ||
|
|
||
| // 4) 댓글 삭제 soft delete | ||
| assertThat(commentJpaRepository.findById(comment.getCommentId())).isPresent(); | ||
| assertThat(commentJpaRepository.findById(comment.getCommentId()).get().getStatus()).isEqualTo(INACTIVE); | ||
|
|
||
| // 5) 댓글 좋아요 삭제 | ||
| long commentLikeCountAfter = commentLikeJpaRepository.count(); | ||
| assertThat(commentLikeCountAfter).isEqualTo(0); | ||
|
|
||
| // 6) 피드 저장 관계 삭제 | ||
| assertTrue(savedFeedJpaRepository.findAll().isEmpty()); |
There was a problem hiding this comment.
수고하셨습니다~~ 새로운 어노테이션이 보여 조금 찾아보느라 리뷰가 늦었습니다!!
카톡에서 말씀드렸던 것처럼 현재 OneToMany에 의해서 삭제 쿼리가 나가는 것을 보니 Feed 하나에 Content 3개가 매핑되어 있을때 Content를 한번에 삭제시키는 것이 아닌 3번의 delete 쿼리를 호출하고 있어, N+1 문제가 발생하고 있는 것 같네요. 우선 저희 연관관계가 너무 복잡하다 보니 OneToMany를 이용하고 추후에 삭제가 이루어지는 순서를 확인해서 Adapter 쪽에서 저희가 명시적으로 delete를 해주는게 좋아보입니다!
| @DiscriminatorValue("FEED") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @OnDelete(action = OnDeleteAction.CASCADE) |
There was a problem hiding this comment.
저도 찾아보니 @OnDelete 어노테이션은 하드 딜리트를 전파하는 기능이지, 소프트 딜리트를 전파하는 기능이 아니어서 삭제 전파 용도로는 동작하지 않는 것이 맞는 것 같습니다.
그렇다면 왜 @OnDelete를 붙였을때는 Feed의 row가 삭제되지 않고, 붙이지 않았을 때는 Feed의 row가 삭제되는지를 찾아봤습니다. 원인은 Hibernate의 JOINED 상속시 삭제 처리 순서에 있었습니다.
Hibernate에서는 JOINED 전략의 하위 엔티티에 대해 삭제할 때 다음 순서로 동작합니다.
- 서브 테이블 DELETE
- 부모 테이블 DELETE
현재 저희는 1번 단계에서 @SQLDelete가 적용되어 있지 않기 때문에(Feed 테이블에 status가 없어 적용 불가) Feed row가 하드 딜리트로 삭제됩니다.
이후 2번 단계에서는 부모(Post)에 @SQLDelete가 적용되어 있어 실제 DELETE 대신 UPDATE로 status만 INACTIVE로 변경됩니다.
결론적으로, 현재 희진님이 추가하신 @onDelete는 DB 연쇄 삭제 용도라기보다는 서브 테이블 삭제를 Hibernate가 생략하도록 만드는 효과로 동작하고 있다고 보는 게 맞을 것 같습니다.
지금은 원하는 동작이 나오고 있으니 그대로 둬도 무방하겠지만, @SQLDelete와 @OnDelete는 철학이 다르기 때문에 혼용 시 주의가 필요해보입니다.
개인적으로는 Post ↔ Feed, Record, Vote 같이 상속 관계에 있는 엔티티에서는 @SQLDelete를 제거하고, delete 호출 시 JPQL로 명시적으로 UPDATE를 날리는 방식이 더 명확하지 않을까 싶은데..
@hd0rable @seongjunnoh 두 분 의견이 궁금합니다!!
| // 삭제용 피드 저장 양방향 매핑 관계 | ||
| @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<>(); |
There was a problem hiding this comment.
OneToMany는 소프트 딜리트 전파가 된다고 하셨던 것 같은데 맞나요?? @hd0rable
| @Override | ||
| public void delete(Feed feed) { | ||
| FeedJpaEntity feedJpaEntity = feedJpaRepository.findById(feed.getId()) | ||
| .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); | ||
|
|
||
| feedJpaRepository.delete(feedJpaEntity); | ||
| } |
There was a problem hiding this comment.
p3: 흠 크게 문제가 될 것 같진않지만 이미 서비스 로직에서 findBy를 하고 온 상태라 id만 넘기면 어떨까 싶긴합니다!!
There was a problem hiding this comment.
엇근데 그러면 서비스에서 무조건 id를 찾는다는 의존성이 생길것같아서 deleteById로 차라리 서비스에서 조회하고 온경우/조회하지않은경우 사용하도록 deleteById 함수를 하나 뚫어놓는건 어떨까요?
There was a problem hiding this comment.
앗근데 직접 엔티티의 status를 인액티브로 바꾸려면 어차피 조회를 다시해야하네욧..id만으로는 delete를 하지못하럿같습니다!
| // then: 1) 피드 soft delete (status=INACTIVE) | ||
| assertThat(feedJpaRepository.findByPostIdAndStatus(feed.getPostId(), INACTIVE)).isPresent(); | ||
|
|
||
| // 2) 피드 태그 관계 삭제 | ||
| assertTrue(feedTagJpaRepository.findAll().isEmpty()); | ||
|
|
||
| // 3) 콘텐츠 삭제 | ||
| assertTrue(contentJpaRepository.findAll().isEmpty()); | ||
|
|
||
| // 4) 댓글 삭제 soft delete | ||
| assertThat(commentJpaRepository.findById(comment.getCommentId())).isPresent(); | ||
| assertThat(commentJpaRepository.findById(comment.getCommentId()).get().getStatus()).isEqualTo(INACTIVE); | ||
|
|
||
| // 5) 댓글 좋아요 삭제 | ||
| long commentLikeCountAfter = commentLikeJpaRepository.count(); | ||
| assertThat(commentLikeCountAfter).isEqualTo(0); | ||
|
|
||
| // 6) 피드 저장 관계 삭제 | ||
| assertTrue(savedFeedJpaRepository.findAll().isEmpty()); |
There was a problem hiding this comment.
삭제 전략을 각 엔티티에 따라 엄청 세세하게 테스트 하셨네요!! LGTM!!
# Conflicts: # src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java # src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java
Test Results370 tests 370 ✅ 31s ⏱️ Results for commit 2319004. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (5)
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (2)
170-170: 메시지 띄어쓰기 통일(맞춤법): “게시물입니다.”파일 내 메시지들이 “입니다.”/“ 입니다.”가 혼재합니다. 본 라인은 붙여쓰기가 맞춤법상 권장됩니다.
- POST_ALREADY_DELETED(HttpStatus.BAD_REQUEST, 180001, "이미 삭제된 게시물 입니다."), + POST_ALREADY_DELETED(HttpStatus.BAD_REQUEST, 180001, "이미 삭제된 게시물입니다."),
169-171: 삭제 API 재삭제 요청 처리 로직 확인 및 API 계약 검토 필요– PostJpaEntity.softDelete()에서 상태가 INACTIVE인 경우
· InvalidStateException(POST_ALREADY_DELETED) 발생
· ErrorCode.POST_ALREADY_DELETED에 매핑된 HttpStatus.BAD_REQUEST(400)로 응답
– FeedCommandPersistenceAdapter#delete 흐름: softDelete() 호출 후 save 처리 확인
– GlobalExceptionHandler의 BusinessExceptionHandler에서 InvalidStateException → 400으로 변환됨현재 코드베이스의 “이미 좋아요/언팔로우” 유사 케이스와 일관되나, 삭제 API의 멱등성을 고려해 아래 방안을 API 계약 차원에서 합의하세요:
- 재삭제 시에도 200 또는 204로 성공 응답하여 멱등성 보장
- 에러 유지 시 409 (Conflict) 또는 410 (Gone) 적용
위 검토 후 API 명세에 반영 바랍니다.
src/main/java/konkuk/thip/post/application/port/out/PostLikeCommandPort.java (1)
8-8: 포트 계약에 삭제 영향 행 수 반환 고려(선택)대량 삭제의 결과를 상위 계층에서 검증/로깅하려면 int 반환이 유용합니다. 현재 void여도 동작에는 문제 없습니다만, 일관된 모니터링을 원하시면 변경을 검토해 주세요.
- void deleteAllByPostId(Long postId); + int deleteAllByPostId(Long postId);포트 시그니처 변경 시 어댑터/서비스/테스트의 연쇄 반영이 필요합니다.
src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java (1)
26-26: 명명/계약 일관성 및 책임 범위 명시 제안메서드가 댓글의 soft delete를 수행하고, 그 전에 댓글 좋아요 물리 삭제가 선행되는 도메인 규칙이라면 Javadoc로 책임과 순서를 명시해 주세요. 포트 내 다른 메서드(delete vs softDelete)와의 용어 일관성도 검토 부탁드립니다.
예)
- softDeleteAllByPostId는 “댓글 좋아요 제거 후 댓글 soft delete” 수행을 명시
- 또는 삭제 정책(soft/hard)을 인자로 추상화한 별도 유스케이스로 분리
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/SavedFeedJpaRepository.java (1)
17-18: 단순 조회는 파생 쿼리로 대체 가능하며, soft delete 정합성도 점검해주세요
- 현재 JPQL은 파생 메서드로 대체 가능합니다. 장점: 필드명 변경 시 컴파일 타임 검출에 유리, 유지보수성 향상.
- 예:
List<SavedFeedJpaEntity> findAllByUserJpaEntityUserId(Long userId);- 만약 연관 Feed가 soft delete일 때 제외되어야 한다면, 현 전략(삭제 전에 관련 SavedFeed를 하드 삭제)으로 충분한지, 또는 별도 필터링이 필요한지 확인 권장합니다.
- N+1 우려가 있다면 별도 읽기 전용 메서드에
join fetch를 고려하세요.권장 변경안:
- @Query("SELECT sf FROM SavedFeedJpaEntity sf WHERE sf.userJpaEntity.userId = :userId") - List<SavedFeedJpaEntity> findAllByUserId(@Param("userId") Long userId); + List<SavedFeedJpaEntity> findAllByUserJpaEntityUserId(Long userId);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java(3 hunks)src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java(1 hunks)src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java(1 hunks)src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java(1 hunks)src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java(2 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java(4 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Content/ContentJpaRepository.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/SavedFeedJpaRepository.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java(2 hunks)src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java(1 hunks)src/main/java/konkuk/thip/post/application/port/out/PostLikeCommandPort.java(1 hunks)src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java(2 hunks)src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
- src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java
- src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java
- src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java
- src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java
- src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java
- src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java
- src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java
- src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java
⏰ 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/konkuk/thip/common/exception/code/ErrorCode.java (1)
169-171: 에러 코드 중복 검사 완료
전역 검사 결과 중복된 에러 코드가 없으며, 코드180001도 유일하게 존재합니다. 추가 변경 불필요합니다.src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java (1)
47-51: 이미 상위 서비스에서 트랜잭션이 보장됩니다
FeedDeleteService.deleteFeed메서드에@Transactional이 적용되어 있어,PostLikeCommandPersistenceAdapter.deleteAllByPostId는 이미 트랜잭션 내부에서 호출됩니다.
따라서 어댑터 메서드에 별도로@Transactional을 추가할 필요는 없습니다.
- 리포지토리 메서드를
int반환으로 변경하여 삭제 건수를 로깅하면 운영 가시성 향상에 도움이 됩니다.src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java (1)
6-8: 필요한 import 추가, 적절합니다벌크 업데이트용 @Modifying, @query, @param 도입은 적절합니다.
src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java (2)
6-6: CommentLikeJpaRepository 의존성 추가, 적절합니다댓글 삭제(소프트)와 함께 좋아요 정리를 위한 의존성 주입은 타당합니다.
34-34: 필드 주입(생성자 기반) 추가, 적절합니다생성자 주입 방식으로 CommentLikeJpaRepository 추가는 일관되고 안전합니다.
src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java
Show resolved
Hide resolved
...in/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java
Show resolved
Hide resolved
...n/java/konkuk/thip/feed/adapter/out/persistence/repository/Content/ContentJpaRepository.java
Show resolved
Hide resolved
...n/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/SavedFeedJpaRepository.java
Show resolved
Hide resolved
| @Modifying | ||
| @Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.feedJpaEntity.postId = :feedId") | ||
| int deleteAllByFeedId(@Param("feedId") Long feedId); |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
신규 bulk delete: clearAutomatically 추가 권장 + 설계 일관성 확인
- bulk 연산 후 1차 캐시를 자동 정리하도록
@Modifying(clearAutomatically = true)를 권장합니다. - 반환 타입을
int로 둔 점 좋습니다(삭제 건수 확인 가능). - 의도: Feed는 soft delete, SavedFeed는 하드 삭제로 보입니다. 도메인 의도라면 일관됩니다. 향후 SavedFeed도 soft delete 대상이라면
DELETE대신 soft 플래그 업데이트로 전환되어야 합니다.
권장 변경안:
- @Modifying
+ @Modifying(clearAutomatically = true)
@Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.feedJpaEntity.postId = :feedId")
int deleteAllByFeedId(@Param("feedId") Long feedId);추가 확인 포인트:
- 경로
sf.userJpaEntity.userId/sf.feedJpaEntity.postId가 실제 엔티티 필드와 일치하는지. - 호출부가 항상 트랜잭션 내부인지(서비스/어댑터
@Transactional) — bulk delete 특성상 중요.
🏁 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차 캐시 자동 정리 적용 권장
- 대상 메서드:
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/SavedFeedJpaRepository.java
deleteAllByFeedId(@param("feedId") Long feedId) - 쿼리 경로(sf.feedJpaEntity.postId, sf.userJpaEntity.userId)와 반환 타입(int)은 모두 정확합니다.
- 이 메서드는 FeedDeleteService(@transactional) 내부에서 호출되어 트랜잭션 경계가 보장됩니다.
- SavedFeedJpaEntity에는 @SQLDelete/@where가 적용되어 있지 않아 하드 삭제가 의도된 설계이며, 도메인 의도에도 부합합니다.
- 향후 SavedFeed에도 soft delete를 적용해야 할 경우 DELETE 대신 상태 플래그 업데이트로 전환하세요.
권장 변경안:
- @Modifying
+ @Modifying(clearAutomatically = true)
@Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.feedJpaEntity.postId = :feedId")
int deleteAllByFeedId(@Param("feedId") Long feedId);📝 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.
| @Modifying | |
| @Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.feedJpaEntity.postId = :feedId") | |
| int deleteAllByFeedId(@Param("feedId") Long feedId); | |
| @Modifying(clearAutomatically = true) | |
| @Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.feedJpaEntity.postId = :feedId") | |
| int deleteAllByFeedId(@Param("feedId") Long feedId); |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/SavedFeedJpaRepository.java
around lines 23 to 25, the deleteAllByFeedId method performs a bulk delete but
does not clear the first-level cache automatically. To fix this, add the
clearAutomatically = true attribute to the @Modifying annotation to ensure the
persistence context is cleared after the bulk operation, preventing stale
entities in the cache.
There was a problem hiding this comment.
@Modifying(clearAutomatically = true) 이 어노테이션이 무슨 역할을 하는데?
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
There was a problem hiding this comment.
@coderabbitai @Modifying(clearAutomatically = true)는 무슨 역할을 하는 어노테이션이야?
There was a problem hiding this comment.
@buzz0331, @Modifying(clearAutomatically = true) 어노테이션은 Spring Data JPA에서 다음과 같은 역할을 합니다:
주요 기능
1. 영속성 컨텍스트 자동 정리
- Bulk 연산(DELETE, UPDATE) 실행 후 1차 캐시를 자동으로 비웁니다
- JPA의 영속성 컨텍스트에 남아있는 엔티티들을 clear() 처리합니다
왜 필요한가?
캐시 불일치 문제 방지:
// 예시: 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 반환)현재 코드에 적용이 필요한 이유
deleteAllByFeedId 메서드는 bulk DELETE 연산이므로:
- 여러 SavedFeed 엔티티를 한 번에 삭제합니다
- 이 엔티티들이 이미 영속성 컨텍스트에 로드되어 있을 수 있습니다
clearAutomatically = true없이는 삭제된 엔티티가 캐시에 남아 데이터 불일치를 일으킬 수 있습니다
특히 FeedDeleteService처럼 하나의 트랜잭션 내에서 여러 연관 엔티티를 다룰 때 이런 설정이 중요합니다.
There was a problem hiding this comment.
@coderabbitai 그렇다면 @Modifying 어노테이션의 default는 뭘로 설정되어 있는데 다 false로 설정되어 있나?
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for @Modifying annotation definition in Spring Data JPA
# First, let's look for any @Modifying usage in the codebase to understand the pattern
rg "@Modifying" -A 1 -B 1 --type java
# Count how many @Modifying annotations are used without explicit parameters
echo "---"
echo "Modifying annotations without explicit parameters:"
rg "@Modifying\s*$" --type java | wc -l
# Count how many have clearAutomatically set
echo "Modifying annotations with clearAutomatically:"
rg "@Modifying.*clearAutomatically" --type java | wc -l
Length of output: 5436
🌐 Web query:
Spring Data JPA @Modifying annotation default values clearAutomatically flushAutomatically
💡 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:
- @Modifying annotation marks a query as a modifying query (for INSERT, UPDATE, DELETE, or DDL) rather than a read-only query[6].
- clearAutomatically:
- Default: false
- If set to true, the persistence context is cleared automatically after executing the modifying query. This is useful to prevent any out-of-sync issues between your context and the actual database, especially after bulk operations[2][6][9].
- flushAutomatically:
- Default: false
- If set to true, it causes the persistence context to be flushed before executing the modifying query, ensuring that all pending changes are synchronized with the database before performing the operation[6][7].
| Attribute | Default Value | Purpose |
|---|---|---|
| clearAutomatically | false | Clears the persistence context after the query |
| flushAutomatically | false | Flushes the persistence context before the query |
Why change defaults:
- Setting clearAutomatically = true is recommended for bulk updates/deletes to avoid unexpected results due to dirty entities remaining in memory[2][4].
- Setting flushAutomatically = true can prevent pending changes from being lost if the persistence context is cleared after the operation, which can occur when using both options together[4].
Clarification of Ambiguity:
- Some older or informal sources may suggest different defaults, but the official Spring Data JPA documentation lists both as false by default in all major versions[6][7][8].
- Both attributes are only meaningful when using @Modifying in combination with a custom @query on a repository method[6].
If further clarification is needed on a specific version or scenario, please specify.
Citations:
- 1: https://www.geeksforgeeks.org/advance-java/spring-data-jpa-modifying-annotation/
- 2: https://www.tomaszezula.com/spring-data-and-custom-queries-best-practices/
- 3: add flushAutomatically attribute to @Modifying annotation [DATAJPA-806] spring-projects/spring-data-jpa#1167
- 4: https://l-lin.github.io/programming-languages/java/update-queries-with-JPA
- 5: https://wesome.org/spring-data-jpa-modifying-queries
- 6: https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/Modifying.html
- 7: https://docs.spring.io/spring-data/jpa/docs/1.11.12.RELEASE/api/org/springframework/data/jpa/repository/Modifying.html
- 8: https://docs.spring.io/spring-data/jpa/docs/2.4.5/api/index.html?org%2Fspringframework%2Fdata%2Fjpa%2Frepository%2FModifying.html
- 9: https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html
- 10: @Modifying clearAutomatically default value is true [DATAJPA-31] spring-projects/spring-data-jpa#470
@buzz0331, 네, @Modifying 어노테이션의 기본값은 모두 false로 설정되어 있습니다:
기본값 정리
| 속성 | 기본값 | 설명 |
|---|---|---|
| clearAutomatically | false |
쿼리 실행 후 영속성 컨텍스트를 자동으로 비우지 않음 |
| flushAutomatically | false |
쿼리 실행 전 영속성 컨텍스트를 자동으로 flush하지 않음 |
현재 코드베이스 상황
코드베이스를 확인한 결과:
- 12개의
@Modifying어노테이션이 파라미터 없이 사용 중 - 0개가
clearAutomatically를 명시적으로 설정 - 즉, 모든 bulk 연산이 기본값(
false)을 사용 중
영향받는 메서드들:
SavedFeedJpaRepository: deleteByUserIdAndFeedId, deleteAllByFeedIdPostLikeJpaRepository: deleteByUserIdAndPostId, deleteAllByPostIdCommentJpaRepository: softDeleteAllByPostIdCommentLikeJpaRepository: deleteByUserIdAndCommentId, deleteAllByCommentId, deleteAllByPostIdFeedTagJpaRepository: deleteAllByFeedIdContentJpaRepository: deleteAllByFeedId- 등등...
잠재적 문제
기본값 사용 시 다음과 같은 상황이 발생할 수 있습니다:
// 예시: FeedDeleteService 내부
Feed feed = feedRepository.findById(feedId); // 1차 캐시에 로드
savedFeedRepository.deleteAllByFeedId(feedId); // DB에서만 삭제, 캐시는 유지
// feed와 연관된 SavedFeed들이 캐시에 남아있어 불일치 발생 가능특히 피드 삭제처럼 여러 연관 엔티티를 한 트랜잭션에서 처리하는 경우, 캐시 불일치로 인한 예기치 않은 동작이 발생할 수 있습니다.
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java
Show resolved
Hide resolved
buzz0331
left a comment
There was a problem hiding this comment.
굿굿 수고하셨습니다~~~ 연관관계가 많아서 확실히 어질어질하네여 😵💫
| // 3. 피드 삭제 | ||
| commentCommandPort.softDeleteAllByPostId(feedId); | ||
| postLikeCommandPort.deleteAllByPostId(feedId); | ||
| feedCommandPort.delete(feed); | ||
| } |
There was a problem hiding this comment.
오호 port로 너무 잘 나눠두셨네요 너무 좋습니다!!
- CommentCommandPort#softDeleteAllByPostId : 댓글 좋아요 하드 딜리트 -> 댓글 소프트 딜리트
- PostLikeCommandPort#deleteAllByPostId : 게시글 좋아요 하드 딜리트
- FeedCommandPort#delete : 피드 태그 관계 하드 딜리트 -> 컨텐츠 하드 딜리트 -> 저장된 피드 하드 딜리트 -> 피드 소프트 딜리트
| @Modifying | ||
| @Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.feedJpaEntity.postId = :feedId") | ||
| int deleteAllByFeedId(@Param("feedId") Long feedId); |
There was a problem hiding this comment.
@coderabbitai @Modifying(clearAutomatically = true)는 무슨 역할을 하는 어노테이션이야?
|
|
||
| @Modifying | ||
| @Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId = :postId") |
There was a problem hiding this comment.
오호 이러면 단일 Comment 삭제시에는 그냥 delete로 SQLDelete 어노테이션을 사용하고 다른 엔티티에 연관된 Comment 삭제시에는 위 쿼리를 사용해서 벌크 삭제를 하는 식이겠네요! 좋습니다!!



#️⃣ 연관된 이슈
📝 작업 내용
📸 스크린샷
💬 리뷰 요구사항
간단한 api인데 상속구조 관련 소프트 딜리트시 처리해줘야하는 어노테이션을 하나빼먹어서 개발속도가 느려졌습니다.. 분발하겟습니다.. 관련해서 노션에 문서화 해두었습니다(진짜 별거아닌데 혼자 헤맴)
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
신규 기능
테스트
기타