Conversation
|
Warning Rate limit exceeded@hd0rable has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 9 minutes and 20 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
Walkthrough이 변경 사항은 기록(Record) 및 피드(Feed) 삭제 API의 도입 및 관련 도메인, JPA, 서비스 계층의 확장에 중점을 둡니다. 삭제 시 연관된 댓글, 좋아요, 태그, 저장된 엔티티 등을 일관성 있게 소프트 삭제 또는 영구 삭제하도록 처리하는 로직과, 삭제 권한 검증, 예외 코드 추가, 통합 테스트 및 단위 테스트 추가가 포함되어 있습니다. Changes
Sequence Diagram(s)기록(Record) 삭제 플로우sequenceDiagram
participant User
participant RecordCommandController
participant RecordDeleteService
participant RoomParticipantValidator
participant RecordCommandPort
participant CommentCommandPort
participant PostLikeCommandPort
User->>RecordCommandController: DELETE /rooms/{roomId}/record/{recordId}
RecordCommandController->>RecordDeleteService: deleteRecord(command)
RecordDeleteService->>RoomParticipantValidator: validateParticipant(userId, roomId)
RecordDeleteService->>RecordCommandPort: findById(recordId)
RecordDeleteService->>Record: validateDeletable(userId, roomId)
RecordDeleteService->>CommentCommandPort: softDeleteAllByPostId(recordId)
RecordDeleteService->>PostLikeCommandPort: deleteAllByPostId(recordId)
RecordDeleteService->>RecordCommandPort: delete(record)
RecordDeleteService-->>RecordCommandController: roomId 반환
RecordCommandController-->>User: BaseResponse 반환
피드(Feed) 삭제 플로우sequenceDiagram
participant User
participant FeedCommandController
participant FeedDeleteService
participant FeedCommandPort
participant CommentCommandPort
participant PostLikeCommandPort
User->>FeedCommandController: DELETE /feeds/{feedId}
FeedCommandController->>FeedDeleteService: deleteFeed(feedId, userId)
FeedDeleteService->>FeedCommandPort: findById(feedId)
FeedDeleteService->>Feed: validateDeletable(userId)
FeedDeleteService->>CommentCommandPort: softDeleteAllByPostId(feedId)
FeedDeleteService->>PostLikeCommandPort: deleteAllByPostId(feedId)
FeedDeleteService->>FeedCommandPort: delete(feed)
FeedDeleteService-->>FeedCommandController: void 반환
FeedCommandController-->>User: BaseResponse 반환
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ 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: 12
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)
51-58: 동일 우려: Record 삭제도 물리 삭제. 소프트 삭제 정책 여부 확인Feed 삭제와 동일한 패턴으로 물리 삭제입니다. 프로젝트의 포스트(Record) 전반이 소프트 삭제 정책이라면
@SQLDelete/@Where또는 상태 업데이트 방식으로 정렬이 필요합니다. 위 Feed 코멘트의 검증 스크립트가 RecordJpaEntity도 함께 점검합니다.
🧹 Nitpick comments (24)
src/main/java/konkuk/thip/record/adapter/in/web/response/RecordDeleteResponse.java (1)
3-7: roomId가 null이 될 수 없다면 primitive(long)로 변경 + Swagger 메타데이터 추가 제안응답에서 roomId는 null이 의미 없을 가능성이 높습니다. NPE 여지를 줄이기 위해 primitive 사용이 안전합니다. 또한 스펙 노출을 위해 Schema 어노테이션을 권장합니다.
package konkuk.thip.record.adapter.in.web.response; +import io.swagger.v3.oas.annotations.media.Schema; + - public record RecordDeleteResponse(Long roomId) { - public static RecordDeleteResponse of(Long roomId) { +@Schema(description = "기록 삭제 응답") +public record RecordDeleteResponse( + @Schema(description = "방 ID", requiredMode = Schema.RequiredMode.REQUIRED) + long roomId +) { + public static RecordDeleteResponse of(long roomId) { return new RecordDeleteResponse(roomId); } }src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java (1)
24-24: 메서드 명명 일관성 제안: saveRecord → save같은 레벨의 FeedCommandPort는 save라는 이름을 사용하고 있습니다. RecordCommandPort도 saveRecord 대신 save로 맞추면 팀 내 가독성과 일관성이 좋아집니다. 추후 대규모 리네임 부담을 줄이려면 지금 통일을 고려해 주세요.
src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingDetailViewApiTest.java (1)
81-81: deleteAllInBatch 전환은 OK, 단 JPA 리스너/소프트삭제 우회에 유의InBatch는 영속성 컨텍스트/엔티티 리스너/@SQLDelete를 우회합니다. 테스트에서 의도한 바라면 좋습니다. 성능 일관성을 위해 voteItem도 InBatch로 맞추는 것을 고려해 볼 수 있습니다.
- voteItemJpaRepository.deleteAll(); + voteItemJpaRepository.deleteAllInBatch();참고: 현재 순서(아이템 → 투표 → 참가자 → 방 …)라면 FK 제약 충돌 없이 안전합니다. 다른 레포들도 일괄 삭제로 통일하려면 동일한 FK 고려가 필요합니다.
src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java (1)
81-81: deleteAllInBatch 전환은 OK, voteItem도 일괄 삭제로 정렬 제안테스트 성능/일관성 측면에서 voteItem도 InBatch로 맞추면 좋습니다. 리스너/소프트삭제가 teardown 단계에서 필요 없다면 안전합니다.
- voteItemJpaRepository.deleteAll(); + voteItemJpaRepository.deleteAllInBatch();src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)
123-123: 메시지 용어 일관성 개선 제안동일 섹션(Record) 내 기존 메시지는 영문 도메인명(“RECORD”)을 사용하지만, 본 항목만 한글 “기록”을 사용하고 있습니다. 용어를 통일하는 편이 조회/로그 분석 시 혼선을 줄입니다.
아래처럼 메시지를 맞추는 것을 제안합니다.
- RECORD_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 130003, "기록 접근 권한이 없습니다."), + RECORD_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 130003, "RECORD 접근 권한이 없습니다."),src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java (2)
124-124: deleteAllInBatch 사용에 따른 부작용 점검 필요deleteAllInBatch는 Hibernate 엔티티 리스너/영속성 컨텍스트 동기화/@SQLDelete(소프트 삭제)를 우회합니다. 피드 도메인에 소프트 삭제나 연관 엔티티 정리 로직이 걸려 있다면 테스트 정합성에 영향이 있을 수 있습니다. 현재 케이스(단순 정리)에서는 성능상 이점이 있으나, 아래를 확인해 주세요.
- 이 테스트 클래스에서 Feed는 하드 삭제해도 무관한가?
- 같은 트랜잭션/영속성 컨텍스트에서 Feed를 참조 중인 엔티티가 없는가? (stale state 방지)
필요 시 deleteAll로 복구하거나, batch 삭제 후 EntityManager clear를 고려해 주세요.
184-184: 테스트 내 데이터 정리 일관성 및 FK 제약 확인테스트 본문에서도 deleteAllInBatch로 변경되었습니다. 본 호출이 외래키 제약(예: 댓글/태그 등 다른 엔티티의 Feed FK)과 충돌하지 않는지, 그리고 소프트 삭제 일관성 요구사항이 없는지 점검 바랍니다. 문제 없다면 주석으로 “하드 삭제 의도”를 남기면 유지보수성이 좋아집니다.
src/main/java/konkuk/thip/feed/application/port/in/FeedDeleteUseCase.java (1)
4-6: Record/Feed 삭제 UseCase 시그니처 통일 제안 — Command 객체 사용RecordDeleteUseCase는 Command 객체를 사용하지만, Feed는 개별 파라미터(Long, Long)를 사용하고 있어 application 계층의 API 스타일이 불일치합니다. 확장성(추가 파라미터), 테스트 용이성, 추적성 측면에서 Command로의 통일을 권장합니다.
아래와 같이 변경을 제안드립니다.
-public interface FeedDeleteUseCase { - void deleteFeed(Long feedId, Long userId); -} +public interface FeedDeleteUseCase { + void deleteFeed(FeedDeleteCommand command); +}추가: Command DTO 예시
package konkuk.thip.feed.application.port.in.dto; import lombok.Builder; @Builder public record FeedDeleteCommand(Long feedId, Long userId) {}src/main/java/konkuk/thip/record/application/port/in/RecordDeleteUseCase.java (1)
5-7: 반환값(Long)의 의미 명확화 — Javadoc 추가 제안PR 요약상 반환값은 roomId로 보입니다. 인터페이스에 Javadoc을 추가해 반환 의미를 명확히 하면 오용 방지와 가독성이 향상됩니다.
public interface RecordDeleteUseCase { - Long deleteRecord(RecordDeleteCommand command); + /** + * Record 삭제를 수행하고, 해당 Record가 속한 Room의 ID를 반환합니다. + * @param command recordId, roomId, userId 등을 포함한 삭제 커맨드 + * @return roomId + */ + Long deleteRecord(RecordDeleteCommand command); }src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java (1)
55-58: 테스트 편의 메서드 추가는 유용하나, 중복을 줄여 일관성을 높이세요
updateCommentCount(int)는 테스트 가독성을 높입니다. 다만 Feed/Record에는 존재하고 Vote에는 부재합니다. 상위PostJpaEntity에@VisibleForTesting protected void setCommentCountForTest(int)같은 메서드로 공통화하면 테스트 코드가 더 일관적입니다.src/test/java/konkuk/thip/feed/domain/FeedTest.java (2)
350-358: 삭제 권한 테스트 보강 제안: 예외 메시지까지 검증기능은 맞습니다. 기존 테스트들처럼 예외 메시지까지 검증하면 일관성이 좋아집니다.
InvalidStateException ex = assertThrows(InvalidStateException.class, () -> feed.validateDeletable(OTHER_USER_ID)); assertEquals(FEED_ACCESS_FORBIDDEN, ex.getErrorCode()); + assertTrue(ex.getCause().getMessage().contains("피드 작성자만 피드를 수정/삭제할 수 있습니다."));
360-366: 작성자 삭제 성공 케이스 LGTM + 비공개 케이스 추가 제안작성자면 공개/비공개 무관하게 삭제 가능함을 드러내기 위해 비공개 피드 케이스도 하나 더 추가(또는 파라미터라이즈)하면 커버리지가 더 명확해집니다.
src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java (2)
134-138: RECORD_DELETE 스웨거 응답 목록 보강: USER_NOT_FOUND/ROOM_NOT_FOUND 추가 권장이 PR의 플로우(룸 참가자 검증, 기록-룸 일치 검증)를 고려하면 아래 오류도 문서화하는 게 일관됩니다.
- RECORD_DELETE(new LinkedHashSet<>(Set.of( - ROOM_ACCESS_FORBIDDEN, - RECORD_NOT_FOUND, - RECORD_ACCESS_FORBIDDEN - ))), + RECORD_DELETE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + ROOM_NOT_FOUND, + ROOM_ACCESS_FORBIDDEN, + RECORD_NOT_FOUND, + RECORD_ACCESS_FORBIDDEN + ))),인증/유저 조회 오류를 글로벌하게 처리하여 문서화 대상이 아니라면 알려주세요.
200-203: FEED_DELETE 스웨거 응답 목록 보강: USER_NOT_FOUND 추가 권장 (Room 관련 코드 미포함은 적절)Feed는 Room에 속하지 않으므로 ROOM_* 코드가 없는 점은 팀 컨벤션과 일치합니다. 다만 FEED_UPDATE 등과의 일관성을 위해 USER_NOT_FOUND 추가를 권장합니다.
- FEED_DELETE(new LinkedHashSet<>(Set.of( - FEED_NOT_FOUND, - FEED_ACCESS_FORBIDDEN - ))), + FEED_DELETE(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + FEED_NOT_FOUND, + FEED_ACCESS_FORBIDDEN + ))),src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java (2)
3-3: @transactional import 일관성 정리 권장 (Spring 애너테이션 사용 고려)RecordDeleteService와 동일하게 jakarta.transaction.Transactional이 사용되었습니다. 프로젝트 전체 정책에 맞춰 org.springframework.transaction.annotation.Transactional로 통일하는 것을 권장합니다.
레포 전체 사용 현황 점검 스크립트는 RecordDeleteService 코멘트를 참고하세요.
적용 예:
-import jakarta.transaction.Transactional; +import org.springframework.transaction.annotation.Transactional;
26-26: S3 이미지 삭제 TODO — 이벤트 기반/아웃박스 패턴 제안 가능삭제 트랜잭션 이후 비동기로 S3 정리를 권장합니다. FeedDeletedEvent 발행 → 비동기 컨슈머가 이미지 키를 수집/삭제(S3 Multi-Object Delete), 필요 시 아웃박스 패턴으로 신뢰성 보강. 원하시면 이벤트/리스너/아웃박스 스켈레톤 코드를 제공하겠습니다.
src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java (1)
96-107: 삭제 응답 상태코드 204(No Content) 고려 (선택 사항)REST 관점에서는 본문 없는 삭제 성공 시 204가 일반적입니다. 다만 현재 프로젝트가 BaseResponse 래퍼로 200을 일관 사용한다면 그대로 유지해도 괜찮습니다.
src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java (1)
95-110:@Transactional애너테이션은 불필요합니다
테스트 메서드에서 생성된 데이터는 개별 트랜잭션으로 실행-롤백되므로,tearDown()에 별도 트랜잭션을 열어deleteAllInBatch()를 수행할 필요가 없습니다. 오히려 테스트 수행 시간을 늘리고, 예상치 못한 플러시/락 이슈를 유발할 수 있습니다. 단순 정리 목적이라면@Transactional제거를 권장합니다.src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java (1)
86-99:tearDown()의 트랜잭션 처리 과다
FeedDeleteAPITest와 동일하게@Transactional을 부착한 상태에서 대량deleteAllInBatch()호출은 불필요합니다. 롤백 메커니즘 또는@DataJpaTest식 격리 전략을 사용하는 편이 간결합니다.src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java (1)
50-56: 엔드포인트 경로 일관성 제안
생성 API는/rooms/{roomId}/record(단수), 삭제 API는/rooms/{roomId}/record/{recordId}(단수)로 정의돼 있습니다. REST 관례상 컬렉션-리소스는 복수형(/records)을, 단건 리소스는/records/{id}형태를 사용합니다. 초기 설계 변경 고려가 가능하다면 아래처럼 통일하는 것이 가독성과 유지보수에 유리합니다.
/rooms/{roomId}/records
/rooms/{roomId}/records/{recordId}src/test/java/konkuk/thip/record/domain/RecordTest.java (3)
9-9: static wildcard import 지양, 사용 상수만 명시적 import로 변경 권장와일드카드 static import는 네임스페이스 오염 및 자동 리팩터링의 정확도를 떨어뜨립니다. 현재 테스트에서 사용하는 상수만 명시적으로 import해주세요.
-import static konkuk.thip.common.exception.code.ErrorCode.*; +import static konkuk.thip.common.exception.code.ErrorCode.COMMENT_COUNT_UNDERFLOW; +import static konkuk.thip.common.exception.code.ErrorCode.POST_LIKE_COUNT_UNDERFLOW; +import static konkuk.thip.common.exception.code.ErrorCode.RECORD_ACCESS_FORBIDDEN;
23-26: 테스트 상수는 static final로 선언해 의도를 명확히 하세요JUnit은 테스트 인스턴스를 매 테스트마다 생성하므로, 상수 값은 클래스 수준의 static final이 적합합니다. 대문자 네이밍도 상수 컨벤션과 잘 맞습니다.
- private final Long OTHER_USER_ID = 2L; + private static final Long OTHER_USER_ID = 2L; - - private final Long ROOM_ID = 1L; + private static final Long ROOM_ID = 1L; - private final Long OTHER_ROOM_ID = 2L; + private static final Long OTHER_ROOM_ID = 2L;추가로, CREATOR_ID도 동일하게 static final로 맞추면 일관성이 좋아집니다(변경 라인 외 참고):
// 함께 적용 권장(라인 22) private static final Long CREATOR_ID = 1L;
193-197: DisplayName 용어/띄어쓰기 수정 및 테스트 메서드 네이밍 일관화 제안
- '피드' → '기록' (도메인 용어 일치)
- '삭제 할' → '삭제할' (띄어쓰기)
- 메서드명은 기존 패턴과 맞춰 noException으로 일관화 권장
- @DisplayName("validateDeletable: 피드의 작성자인 경우 피드를 삭제 할 수 있다.") + @DisplayName("validateDeletable: 기록의 작성자인 경우 기록을 삭제할 수 있다.") - void validateDeletable_byCreator_Success(){ + void validateDeletable_byCreator_noException(){src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java (1)
45-50: 대량 삭제 시 퍼포먼스 확인 필요
commentList,postLikeList모두cascade = REMOVE+orphanRemoval = true로 설정되어 있어, 댓글·좋아요가 많은 게시물을 삭제하면 JPA 가 자식 수만큼 DELETE 쿼리를 개별 발행합니다.
대량 데이터 환경에서는 배치 삭제나 DB-레벨 cascade 로 전환하는 것이 성능-효율적일 수 있으니 추후 모니터링을 권장합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (33)
src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java(2 hunks)src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java(2 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/in/web/RecordCommandController.java(3 hunks)src/main/java/konkuk/thip/record/adapter/in/web/response/RecordDeleteResponse.java(1 hunks)src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java(2 hunks)src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java(2 hunks)src/main/java/konkuk/thip/record/adapter/out/persistence/repository/RecordJpaRepository.java(1 hunks)src/main/java/konkuk/thip/record/application/port/in/RecordDeleteUseCase.java(1 hunks)src/main/java/konkuk/thip/record/application/port/in/dto/DummyQuery.java(0 hunks)src/main/java/konkuk/thip/record/application/port/in/dto/DummyResult.java(0 hunks)src/main/java/konkuk/thip/record/application/port/in/dto/RecordDeleteCommand.java(1 hunks)src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java(1 hunks)src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java(1 hunks)src/main/java/konkuk/thip/record/domain/Record.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/record/adapter/in/web/RecordDeleteAPITest.java(1 hunks)src/test/java/konkuk/thip/record/domain/RecordTest.java(4 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)
💤 Files with no reviewable changes (2)
- src/main/java/konkuk/thip/record/application/port/in/dto/DummyQuery.java
- src/main/java/konkuk/thip/record/application/port/in/dto/DummyResult.java
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.
📚 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/record/application/port/out/RecordCommandPort.javasrc/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.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/record/adapter/in/web/RecordDeleteAPITest.javasrc/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.javasrc/test/java/konkuk/thip/record/domain/RecordTest.java
📚 Learning: 2025-07-26T06:09:00.850Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.
Applied to files:
src/main/java/konkuk/thip/common/exception/code/ErrorCode.javasrc/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.javasrc/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.javasrc/main/java/konkuk/thip/record/application/service/RecordDeleteService.javasrc/main/java/konkuk/thip/record/domain/Record.javasrc/test/java/konkuk/thip/record/domain/RecordTest.javasrc/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.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/record/adapter/out/persistence/RecordCommandPersistenceAdapter.javasrc/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.javasrc/main/java/konkuk/thip/record/adapter/out/persistence/repository/RecordJpaRepository.javasrc/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java
🧬 Code Graph Analysis (3)
src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java (4)
src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (1)
Entity(21-77)src/main/java/konkuk/thip/vote/adapter/out/jpa/VoteJpaEntity.java (1)
Entity(16-54)src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java (1)
Entity(15-59)src/main/java/konkuk/thip/record/domain/Record.java (1)
Getter(14-116)
src/test/java/konkuk/thip/feed/domain/FeedTest.java (1)
src/test/java/konkuk/thip/record/domain/RecordTest.java (1)
DisplayName(12-219)
src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(34-324)
🔇 Additional comments (20)
src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java (1)
24-24: delete(Record) 추가 적절CommandPort에 삭제 계약을 노출한 선택이 도메인 주도 설계와 CQRS 관점에서 타당합니다. 서비스 트랜잭션 경계 내에서 호출되도록만 유지하면 됩니다.
src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (2)
19-19: delete(Feed) 추가 적절삭제 계약 노출이 서비스/어댑터 전반의 기능 구현에 필요하며, 다른 메서드들과도 시그니처 일관성이 있습니다.
19-19: FeedDeleteService에 Room 검증 로직이 포함되어 있지 않음을 확인했습니다. 팀 컨벤션에 부합합니다.
- 확인 파일:
- src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java
- Feed 도메인·포트 계층 전체에서
Room,roomId,RoomParticipant,RoomCommandPort참조 미발견따라서 추가 수정은 필요하지 않습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java (1)
19-19: 상태 기반 조회 메서드 추가 적절Optional 반환 및 StatusType 필터링으로 삭제/비활성화 레코드 배제를 명시화한 점 좋습니다. 도메인 일관성 측면에서도 적합합니다.
src/main/java/konkuk/thip/record/adapter/out/persistence/repository/RecordJpaRepository.java (1)
9-11: findByPostIdAndStatus 도입 적절 — 소프트 딜리트 필터링 OK
Optional<RecordJpaEntity> findByPostIdAndStatus(...)는 null-안전성과 소프트 딜리트 상태 필터링 요구에 부합합니다. 상위PostJpaEntity의postId/status속성 경로도 올바릅니다.src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteAPITest.java (1)
115-116: 검증 단순화 LGTMINACTIVE 상태 단일 쿼리 확인으로 의도와 결과가 명확해졌습니다. 테스트 안정성과 가독성 측면에서 적절합니다.
Also applies to: 135-136, 157-158
src/main/java/konkuk/thip/feed/domain/Feed.java (2)
132-132: 권한 오류 메시지 개선 LGTM삭제 기능 추가에 맞춰 메시지를 “수정/삭제”로 정비한 점 좋습니다. 테스트와도 일치합니다.
136-139: validateDeletable 도입 LGTM — 현재 정책을 잘 캡슐화삭제 권한이 곧 작성자 검증임을 명시하는 얇은 래퍼로 적절합니다. 정책 변경 시 이 지점을 확장하면 됩니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)
44-49: ACTIVE 상태 필터링으로 조회 변경 LGTM소프트 삭제/비활성화된 피드를 배제하는 안전한 조회로 일관성이 좋아졌습니다.
src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)
47-49: ACTIVE 상태 필터링으로 조회 변경 LGTM비활성화된 기록을 배제하여 조회 일관성 확보에 도움됩니다.
src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java (2)
23-31: 검증 순서 합리적 — 팀 컨벤션(Record는 Room 소속)과 일치
- 방 참여자 검증(사용자 ∈ Room)
- 기록 조회
- 기록의 Room 일치/작성자 권한 검증
THIP 러닝(Record/Vote는 Room 소속, Feed는 아님)과 정확히 부합합니다. 👍
33-36: 삭제 및 반환값(roomId) 처리 적절delete 후 roomId 반환은 컨트롤러 응답 모델과도 잘 맞습니다. 트랜잭션 경계 내에서 일관성 있게 처리되어 있습니다.
src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java (2)
21-25: 조회 → 권한 검증 순서 적절 (Feed는 Room 검증 불필요)Feed가 Room에 속하지 않는 구조를 고려하면 현재 순서가 가장 단순하고 명확합니다.
28-29: 도메인 삭제 포트 호출 적절도메인 규칙 검증 후 포트에 위임하는 구조가 깔끔합니다.
src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java (1)
39-39: 의존성 주입 추가 적절삭제 유스케이스 주입 위치와 접근 제어(생성자 주입) 모두 적절합니다.
src/test/java/konkuk/thip/record/domain/RecordTest.java (4)
37-37: ROOM_ID 상수 사용으로 테스트 가독성 향상, LGTM하드코딩 값 대신 의미 있는 상수 사용은 의도 전달과 유지보수에 유리합니다.
182-190: 비작성자 삭제 제한과 에러코드 검증이 명확합니다도메인 규칙(작성자만 삭제 가능)과 RECORD_ACCESS_FORBIDDEN 에러코드 검증이 잘 반영되어 있습니다. 예외 유형/코드 모두 체크한 점 좋습니다.
200-206: Room 일치 시 예외 없음 검증, 러닝과 일치합니다Record는 Room에 속한다는 팀 컨벤션(러닝)에 부합하는 유효성 테스트입니다. 정상 경로를 간결히 확인한 점 좋습니다.
208-217: Room 불일치 시 접근 거부 코드(RECORD_ACCESS_FORBIDDEN) 검증 적절권한/소속 불일치 시 공통 접근 거부 코드로 수렴시킨 설계와 테스트가 일치합니다. 예외 타입과 에러코드 모두 검증되어 신뢰성이 높습니다.
src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (1)
42-49: cascade = REMOVE + orphanRemoval ⇒ 자식은 물리 삭제, 부모는 소프트 삭제부모
FeedJpaEntity가 ‘INACTIVE’ 로만 업데이트될 때 자식(SavedFeedJpaEntity,FeedTagJpaEntity)은 실제 DELETE 됩니다.
도메인 관점에서 “소프트 삭제된 피드”가 자식을 모두 잃는 것이 의도된 동작인지 재확인이 필요합니다.
일관된 보존 정책을 위해 자식도 soft delete 컬럼을 갖게 하거나, 부모를 물리 삭제하도록 설계 변경을 고려해 주세요.
| @OneToMany(mappedBy = "commentJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true) | ||
| private List<CommentLikeJpaEntity> commentLikeList = new ArrayList<>(); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
컬렉션 필드에 @Builder.Default 누락 — Lombok Builder 사용 시 NPE 가능성
클래스에 @builder가 적용되어 있어 필드 초기화(new ArrayList<>())가 빌더 경로에서 무시됩니다. 테스트나 팩토리에서 빌더로 엔티티를 만들면 commentLikeList가 null이 되어 NPE가 발생할 수 있습니다. 아래처럼 @Builder.Default를 추가해 주세요.
- @OneToMany(mappedBy = "commentJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true)
- private List<CommentLikeJpaEntity> commentLikeList = new ArrayList<>();
+ @OneToMany(mappedBy = "commentJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true)
+ @Builder.Default
+ private List<CommentLikeJpaEntity> commentLikeList = new ArrayList<>();📝 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.
| @OneToMany(mappedBy = "commentJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true) | |
| private List<CommentLikeJpaEntity> commentLikeList = new ArrayList<>(); | |
| @OneToMany(mappedBy = "commentJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true) | |
| @Builder.Default | |
| private List<CommentLikeJpaEntity> commentLikeList = new ArrayList<>(); |
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java at
lines 59-60, the commentLikeList field is initialized with new ArrayList<>(),
but since the class uses Lombok's @Builder, this initialization is ignored in
the builder path, causing potential NullPointerExceptions. Fix this by adding
the @Builder.Default annotation to the commentLikeList field to ensure it is
properly initialized when using the builder.
There was a problem hiding this comment.
근데 삭제용이라 .get해서 안쓸건데 @Builder.Default로 초기화가 필요한가?
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!
src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java
Outdated
Show resolved
Hide resolved
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java
Outdated
Show resolved
Hide resolved
src/main/java/konkuk/thip/record/application/port/in/dto/RecordDeleteCommand.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/vote/adapter/out/jpa/VoteJpaEntity.java
Outdated
Show resolved
Hide resolved
src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java
Outdated
Show resolved
Hide resolved
buzz0331
left a comment
There was a problem hiding this comment.
수고하셨습니다~~ 앞 pr과 비슷한 로직인 것 같아 앞 pr 리뷰 참고해주시면 될 것 같습니다!!
| // 2-1. 기록 삭제 권한 검증 | ||
| record.validateRoomId(command.roomId()); | ||
| record.validateDeletable(command.userId()); | ||
|
|
There was a problem hiding this comment.
p3: 이렇게 나누지 않고 validateDeletable 내부에서 validateRoomId 검증 로직까지 한번에 수행하는 것 어떤가요?? 캡슐화가 더 잘 지켜질 것 같습니다!!
| @Test | ||
| @DisplayName("기록을 삭제하면 [soft delete]되고, 연관된 댓글, 댓글 좋아요도 모두 삭제된다") | ||
| void deleteRecord_success() throws Exception { | ||
|
|
||
| // when | ||
| mockMvc.perform(delete("/rooms/{roomId}/record/{recordId}", room.getRoomId(), record.getPostId()) | ||
| .requestAttr("userId", user.getUserId())) | ||
| .andExpect(status().isOk()); | ||
|
|
||
|
|
||
| // then: 1) 기록 soft delete (status=INACTIVE) | ||
| assertThat(recordJpaRepository.findByPostIdAndStatus(record.getPostId(), INACTIVE)).isPresent(); | ||
|
|
||
| // 2) 댓글 삭제 soft delete | ||
| assertThat(commentJpaRepository.findByCommentIdAndStatus(comment.getCommentId(),INACTIVE)).isPresent(); | ||
|
|
||
| // 3) 댓글 좋아요 삭제 | ||
| long commentLikeCountAfter = commentLikeJpaRepository.count(); | ||
| assertThat(commentLikeCountAfter).isEqualTo(0); | ||
| } |
seongjunnoh
left a comment
There was a problem hiding this comment.
피드 삭제와 많이 유사하네요! 고생하셨습니다!
| @Test | ||
| @DisplayName("기록을 삭제하면 [soft delete]되고, 연관된 댓글, 댓글 좋아요도 모두 삭제된다") | ||
| void deleteRecord_success() throws Exception { | ||
|
|
||
| // when | ||
| mockMvc.perform(delete("/rooms/{roomId}/record/{recordId}", room.getRoomId(), record.getPostId()) | ||
| .requestAttr("userId", user.getUserId())) | ||
| .andExpect(status().isOk()); | ||
|
|
||
|
|
||
| // then: 1) 기록 soft delete (status=INACTIVE) | ||
| assertThat(recordJpaRepository.findByPostIdAndStatus(record.getPostId(), INACTIVE)).isPresent(); | ||
|
|
||
| // 2) 댓글 삭제 soft delete | ||
| assertThat(commentJpaRepository.findByCommentIdAndStatus(comment.getCommentId(),INACTIVE)).isPresent(); | ||
|
|
||
| // 3) 댓글 좋아요 삭제 | ||
| long commentLikeCountAfter = commentLikeJpaRepository.count(); | ||
| assertThat(commentLikeCountAfter).isEqualTo(0); | ||
| } |
| void tearDown() { | ||
| voteItemJpaRepository.deleteAll(); | ||
| voteJpaRepository.deleteAll(); | ||
| voteJpaRepository.deleteAllInBatch(); |
…lushAutomatically = true) 속성 추가로 영속성 1차 캐시 삭제(#160)
…175-record-delete # Conflicts: # src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java # src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/SavedFeedJpaRepository.java
…lushAutomatically = true) 속성 추가로 영속성 1차 캐시 삭제(#175)
Test Results374 tests 374 ✅ 32s ⏱️ Results for commit 21f7a9d. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Actionable comments posted: 8
🔭 Outside diff range comments (1)
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)
78-80: update 경로도 ACTIVE 필터 사용 권장삭제된 피드가 수정되지 않도록
findByPostIdAndStatus(..., ACTIVE)를 사용하는 것이 안전합니다.- FeedJpaEntity feedJpaEntity = feedJpaRepository.findById(feed.getId()) - .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); + FeedJpaEntity feedJpaEntity = feedJpaRepository.findByPostIdAndStatus(feed.getId(), ACTIVE) + .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND));
♻️ Duplicate comments (2)
src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java (2)
28-35: 검증 캡슐화 범위 재점검(중복 검증 축소)
validateDeletable(userId, roomId)가 “기록-방 일치”까지 내포한다면, 외부 별도 검증을 줄여 응집도를 높일 수 있습니다. 기존 피드백과 유사 제안입니다.
3-3: @transactional import 일관성 (Spring으로 통일 권장)프로젝트 전반과 일관되게
org.springframework.transaction.annotation.Transactional사용을 권장합니다. 이전 코멘트와 동일 제안입니다.-import jakarta.transaction.Transactional; +import org.springframework.transaction.annotation.Transactional;
🧹 Nitpick comments (10)
src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java (2)
12-14: 업데이트 카운트 반환하도록 시그니처 변경 제안영향 행 수를 반환하면 존재하지 않는 ID 등 예외 상황을 상위 계층에서 명확히 처리할 수 있습니다.
다음과 같이 변경을 제안합니다:
- @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("UPDATE RecentSearchJpaEntity r SET r.modifiedAt = CURRENT_TIMESTAMP WHERE r.recentSearchId = :recentSearchId") - void updateModifiedAt(@Param("recentSearchId") Long recentSearchId); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE RecentSearchJpaEntity r SET r.modifiedAt = CURRENT_TIMESTAMP WHERE r.recentSearchId = :recentSearchId") + int updateModifiedAt(@Param("recentSearchId") Long recentSearchId);호출부에서 반환값이 0이면 대상 미존재로 간주하여 예외/로그 처리할 수 있습니다.
13-13: 시간 소스 일관성 검토 (DB CURRENT_TIMESTAMP vs 애플리케이션 시간)JPQL의
CURRENT_TIMESTAMP는 DB 시간을 사용합니다. 시스템의 다른 감사 필드가 스프링 데이터 감사(@LastModifiedDate) 혹은 애플리케이션 클럭을 사용한다면 시간 소스가 혼재할 수 있습니다. 테스트 재현성/시간대 정책(예: UTC 고정) 관점에서 일관성 확인을 권장합니다. 필요 시 애플리케이션 레벨에서 설정(setter) 후 flush로 통일하는 것도 방법입니다.src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java (1)
26-26: 의도(소프트 삭제) 명시 및 사용 가이드 주석 제안메서드가 소프트 삭제(상태 전환)임을 Javadoc으로 명시해 주세요. 대량 업데이트의 특성상 엔티티 리스너/도메인 이벤트가 동작하지 않는다는 점도 함께 적어두면 오해를 줄일 수 있습니다.
다음처럼 간단한 주석 추가를 권장합니다:
+ /** + * 주어진 postId에 속한 모든 댓글을 소프트 삭제(STATUS=INACTIVE)합니다. + * 대량 업데이트로 엔티티 리스너/도메인 이벤트는 트리거되지 않습니다. + */ void softDeleteAllByPostId(Long postId);src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java (1)
47-50: 벌크 삭제 트랜잭션 보장 및 영속성 컨텍스트 동기화 확인
- 해당 벌크 삭제는 반드시 트랜잭션 내에서 실행되어야 합니다. 상위 서비스 계층에서
@Transactional이 보장되지 않는다면 본 메서드에 부여를 고려해 주세요.PostLikeJpaRepository.deleteAllByPostId에@Modifying(clearAutomatically = true, flushAutomatically = true)가 선언되어 있는지 확인해 주세요. 없으면 1차 캐시 불일치가 날 수 있습니다.필요 시 아래와 같이 메서드에 트랜잭션을 부여할 수 있습니다:
@Override + @org.springframework.transaction.annotation.Transactional public void deleteAllByPostId(Long postId) { postLikeJpaRepository.deleteAllByPostId(postId); }또는 import를 추가하고 어노테이션을 정식으로 사용합니다:
import org.springframework.transaction.annotation.Transactional; // ... @Override @Transactional public void deleteAllByPostId(Long postId) { ... }src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java (1)
15-17: Enum literal 사용 및 영향 행 수 반환으로 가시성 향상
- 문자열
'INACTIVE'대신 JPQL enum literal을 사용하면 타입 안정성과 리팩터링 안전성이 높아집니다.- 영향받은 행 수를 반환하면 테스트/모니터링 시 유용합니다.
- 선택적으로 이미 INACTIVE인 행은 제외하여 불필요한 쓰기를 줄일 수 있습니다.
아래와 같이 수정 제안드립니다:
- @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId = :postId") - void softDeleteAllByPostId(@Param("postId") Long postId); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + UPDATE CommentJpaEntity c + SET c.status = konkuk.thip.common.entity.StatusType.INACTIVE + WHERE c.postJpaEntity.postId = :postId + AND c.status <> konkuk.thip.common.entity.StatusType.INACTIVE + """) + int softDeleteAllByPostId(@Param("postId") Long postId);참고: 메서드 시그니처 변경 시 이를 호출하는 어댑터/서비스에서 반환값 사용 여부를 점검해야 합니다.
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java (1)
24-26: flush/clear 자동화는 👍. 삭제 건수 반환 고려
- 벌크 삭제 후 1차 캐시 정합성 확보에 유리합니다.
- 운영 관점에서 영향도/모니터링을 위해 삭제 건수를 반환받는 패턴이 유용합니다.
제안 diff:
- void deleteByUserIdAndPostId(@Param("userId") Long userId, @Param("postId") Long postId); + int deleteByUserIdAndPostId(@Param("userId") Long userId, @Param("postId") Long postId);src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java (2)
44-45: 반환값은 실제 삭제된 엔티티의 roomId 사용 권장입력값 반환 대신
record.getRoomId()가 의미적으로 정확합니다.- return command.roomId(); + return record.getRoomId();
39-40: 주석 정정(도메인 용어 혼동)“피드 게시물 좋아요 삭제” → “게시물 좋아요 삭제”로 정정 제안. 기록 삭제 서비스 맥락에 맞춰주세요.
- // 3-2. 피드 게시물 좋아요 삭제 + // 3-2. 게시물 좋아요 삭제src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/SavedFeedJpaRepository.java (1)
14-16: 반환형 일관화(옵션)동일한 삭제 메서드들 간 반환형을
int로 통일하면 호출부에서도 일관된 처리(삭제 건수 로깅 등)가 가능합니다.-void deleteByUserIdAndFeedId(@Param("userId") Long userId, @Param("feedId") Long feedId); +int deleteByUserIdAndFeedId(@Param("userId") Long userId, @Param("feedId") Long feedId);src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)
9-9: 패키지 네이밍 컨벤션(nit)
repository.Content.ContentJpaRepository처럼 대문자 패키지는 자바 관례와 다릅니다. 가능하면 소문자 패키지로 리팩터링 고려 바랍니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (27)
src/main/java/konkuk/thip/book/adapter/out/persistence/repository/SavedBookJpaRepository.java(1 hunks)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(2 hunks)src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java(1 hunks)src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(2 hunks)src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java(2 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(5 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/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java(1 hunks)src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java(1 hunks)src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java(2 hunks)src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java(1 hunks)src/main/java/konkuk/thip/record/domain/Record.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)src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java(1 hunks)src/test/java/konkuk/thip/record/domain/RecordTest.java(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (12)
- src/main/java/konkuk/thip/record/adapter/out/jpa/RecordJpaEntity.java
- src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java
- src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java
- src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteAPITest.java
- src/test/java/konkuk/thip/record/adapter/in/web/RecordDeleteAPITest.java
- src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java
- src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java
- src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java
- src/test/java/konkuk/thip/record/domain/RecordTest.java
- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
- src/main/java/konkuk/thip/record/domain/Record.java
- src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.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 (18)
src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java (2)
12-14: flush/clear 옵션 추가 적절 — 동기화 이슈 예방
@Modifying(clearAutomatically = true, flushAutomatically = true)추가로 벌크 업데이트 후 1차 캐시 불일치/오염을 줄일 수 있습니다. 전체 코드베이스의 일관성 측면에서도 (+)입니다.
12-14:clearAutomatically=true부작용 불필요 확인
updateModifiedAt는 오직RecentSearchCommandPersistenceAdapter.touch()내부에서만 정의되어 있으나, 서비스 레이어에서 해당touch()를 호출하는 코드는 존재하지 않습니다.- 영속성 컨텍스트가 비워진 뒤 같은 트랜잭션 내에서 이미 조회해둔 엔티티의 지연 로딩 필드에 접근하거나 상태를 변경하는 로직이 현재 없습니다.
- 따라서 LazyInitializationException이나 상태 유실 우려는 없으므로, 본 리뷰 코멘트는 현 코드베이스에 적용되지 않습니다.
Likely an incorrect or invalid review comment.
src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java (3)
6-6: 의존성 주입 추가 LGTM댓글 좋아요 일괄 삭제를 위해
CommentLikeJpaRepository의존성 추가는 타당합니다.
34-34: 필드 주입 LGTM
@RequiredArgsConstructor와 함께 final 필드로 주입되어 구성 안전성이 확보됩니다.
95-99: 트랜잭션 경계가 서비스 계층에서 이미 보장됩니다.
- src/main/java/konkuk/thip/feed/application/service/FeedDeleteService.java
• deleteFeed(Long, Long) 메서드에 @transactional 적용 (라인 21–22)- src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java
• deleteRecord(RecordDeleteCommand) 메서드에 @transactional 적용 (라인 25–26)따라서 CommentCommandPersistenceAdapter.softDeleteAllByPostId()에 별도 @transactional을 추가할 필요가 없습니다.
src/main/java/konkuk/thip/book/adapter/out/persistence/repository/SavedBookJpaRepository.java (1)
13-15: 트랜잭션 경계 확인 및 반환 타입 검토
deleteByUserIdAndBookId는BookSavedService.changeSavedBook(...)내부에서@Transactional이 적용된 상태로 호출되고 있으므로 JPA 트랜잭션 경계는 확보되어 있습니다.@Modifying(clearAutomatically = true, flushAutomatically = true)적용으로 1차 캐시 동기화 문제를 자동으로 방지할 수 있어 적절합니다. (단일 삭제 호출이므로 성능 영향은 거의 없으며, 대량 루프 호출 시에는 트랜잭션 경계를 외부로 옮기거나 배치 단위로 묶어 처리하는 것을 권장합니다.)- 삭제된 건수 확인이 필요하다면 반환 타입을
int로 변경하는 방안을 고려해 보세요.제안 diff (선택 사항):
- void deleteByUserIdAndBookId(Long userId, Long bookId); + int deleteByUserIdAndBookId(Long userId, Long bookId);src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Content/ContentJpaRepository.java (1)
5-7: 필요 import 추가 적절
@Modifying,@Query,@Paramimport 추가는 변경된 메서드 구현에 필요한 최소한의 보완으로 적절합니다.src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java (2)
32-33: 이미 삭제된 기록 처리/상태 필터 확인
getByIdOrThrow가 ACTIVE만 조회하는지 확인 바랍니다. 소프트 삭제 정책이라면 상태 필터가 필요합니다(이미 삭제된 기록 재삭제 방지).
38-40: 잘못된 postId/recordId 혼용 지적
Record는 하나의 피드 게시물을 나타내며command.recordId()가 해당 게시물(post)의 ID로 사용되는 것이 맞습니다. 또한Record도메인에는getPostId()메서드가 존재하지 않아 제안하신 대체 코드(record.getPostId())는 컴파일 에러를 야기합니다.
기존대로commentCommandPort.softDeleteAllByPostId(command.recordId())및postLikeCommandPort.deleteAllByPostId(command.recordId())를 유지해주세요.Likely an incorrect or invalid review comment.
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java (3)
18-21: bulk delete 시 영속성 컨텍스트 처리 옵션 추가 적절
clearAutomatically,flushAutomatically적용 좋습니다. 대용량 삭제 시 1차 캐시 오염 방지에 유효합니다.
27-30: 댓글 단건 일괄 삭제 쿼리 옵션 일관화 👍옵션 일관성과 JPQL 명확성 측면에서 적절합니다.
35-41: Java 텍스트 블록 사용 버전 확인 요청
"""텍스트 블록은 JDK 15+ 필요. 빌드 타깃이 JDK 17이라면 OK입니다. 버전 정합성만 확인 부탁드립니다.src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/SavedFeedJpaRepository.java (3)
14-16: JPQL 전환 + @Modifying 옵션 적용 적절native → JPQL 전환 및 자동 flush/clear 옵션 적용 좋습니다.
18-19: findAllByUserId JPQL 전환 적절엔티티 연관을 활용하는 깔끔한 JPQL입니다.
24-26: feedId 기준 일괄 삭제 API 추가 👍삭제 건수 반환은 운영 모니터링에도 유용합니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (3)
46-50: ACTIVE 상태 필터 도입 적절소프트 삭제 정책과 일관됩니다. 태그 조인 후 매핑도 문제 없어 보입니다.
132-143: 연쇄 삭제 → softDelete 순서 적절. 트랜잭션 경계만 확인서비스 레이어에서 본 메서드 호출이 단일 트랜잭션으로 묶여 있는지 확인 바랍니다(부분 실패 방지).
82-84: orphanRemoval=true 설정 확인 – clear()만으로도 DB 삭제가 보장됩니다
FeedJpaEntity의contentList매핑에 이미orphanRemoval = true가 적용되어 있어
feedJpaEntity.getContentList().clear()호출 시 플러시 시점에 연관된ContentJpaEntity가 삭제됩니다.
따라서 별도의deleteAllByFeedId()호출 없이도 DB 삭제가 보장됩니다.
| @Modifying(clearAutomatically = true, flushAutomatically = true) | ||
| @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); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
DELETE 서브쿼리 → 경로 표현식으로 단순화 권장(가독성·성능)
서브쿼리 대신 연관 경로를 직접 사용하면 쿼리가 단순해집니다.
- @Modifying(clearAutomatically = true, flushAutomatically = true)
- @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);
+ @Modifying(clearAutomatically = true, flushAutomatically = true)
+ @Query("DELETE FROM CommentLikeJpaEntity cl WHERE cl.commentJpaEntity.postJpaEntity.postId = :postId")
+ void deleteAllByPostId(@Param("postId") Long postId);(옵션) 삭제 건수 활용이 필요하다면 반환형을 int로 통일하는 것도 고려해 주세요.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java
around lines 34 to 42, simplify the DELETE query by replacing the subquery with
a direct path expression using entity relationships to improve readability and
performance. Additionally, consider changing the method's return type to int to
return the number of deleted records if deletion count tracking is needed.
There was a problem hiding this comment.
오 서브쿼리를 안 사용해도 위와 같은 방식을 사용해도 될 것 같은데요??
@coderabbitai 저러면 commentJpaEntity에서 postJpaEntity를 참조할때 추가적인 SELECT 쿼리는 안나가지??
There was a problem hiding this comment.
@buzz0331 아니요, 추가적인 SELECT 쿼리는 발생하지 않습니다!
@Modifying 어노테이션이 붙은 DELETE 쿼리는 벌크 연산으로 실행되어, 엔티티를 로딩하지 않고 직접 SQL로 변환됩니다.
경로 표현식 cl.commentJpaEntity.postJpaEntity.postId는 다음과 같이 SQL JOIN으로 변환됩니다:
DELETE FROM comment_like cl
WHERE EXISTS (
SELECT 1 FROM comment c
JOIN post p ON c.post_id = p.post_id
WHERE cl.comment_id = c.comment_id
AND p.post_id = ?
)또는 Hibernate 버전에 따라:
DELETE FROM comment_like
WHERE comment_id IN (
SELECT c.comment_id FROM comment c
WHERE c.post_id = ?
)즉, 엔티티 로딩 없이 단일 DELETE 문으로 실행되므로 N+1 문제나 추가 SELECT는 발생하지 않습니다. 오히려 경로 표현식이 더 간결하고 JPA 스타일에 맞습니다!
There was a problem hiding this comment.
엇 근데 토끼가 추천해준방식이 내부적으로 조인을 사용한다고해서 수정안했었습니당
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.
@buzz0331 죄송합니다, 제가 성능 면에서 정확하지 않은 설명을 드렸네요!
실제로 이 경우엔 서브쿼리가 성능상 더 나을 수 있습니다:
서브쿼리 (현재 코드) 👍
DELETE FROM comment_like
WHERE comment_id IN (
SELECT comment_id FROM comment WHERE post_id = ?
)- comment 테이블의 post_id 인덱스로 빠르게 comment_id 목록 조회
- IN 절로 comment_like 삭제
- 두 단계로 명확하게 분리
경로 표현식 (제 제안)
DELETE FROM comment_like cl
WHERE EXISTS (
SELECT 1 FROM comment c
WHERE cl.comment_id = c.comment_id AND c.post_id = ?
)- 각 comment_like 행마다 EXISTS 체크 필요
- 데이터가 많을수록 오버헤드 증가 가능
제가 "성능 개선"이라고 단정적으로 말씀드린 건 잘못됐습니다. 가독성은 개선될 수 있지만, 성능은 오히려 현재 서브쿼리 방식이 더 효율적일 가능성이 높습니다.
현재 구현 그대로 유지하시는 게 좋겠습니다! 🙏
...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/post/adapter/out/persistence/PostLikeJpaRepository.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/post/application/port/out/PostLikeCommandPort.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/record/application/service/RecordDeleteService.java
Show resolved
Hide resolved
buzz0331
left a comment
There was a problem hiding this comment.
굿굿 궁금한점 리뷰 좀 남겨두었습니다!!
앞 pr이랑 커밋이 섞여있어서 앞 pr이 머지되고 rebase 되면 다시 확인해볼게욥
There was a problem hiding this comment.
엇 이건 왜 추가하신거죠? (어떤 기능인지 몰라서,,ㅎ)
There was a problem hiding this comment.
앞 pr에서 코래가 친절하게 설명해주었습니다~ ㅎㅎ
There was a problem hiding this comment.
피드 삭제 에서 토끼가 말해준것처럼 jpa의 영속성의 1차 캐시를 비워주는 역할을합니다! 다른 트랜잭션에서 수행되는 메서드라편 필요없겠지만 UPATE,DELETE 쿼리를 날릴 시에 같은 여러 엔티티들을 한 트랜잭션에 처리하기 때문에 캐시 불일치로인한 오류를 막기 위해 추가했습니닷
| @Modifying(clearAutomatically = true, flushAutomatically = true) | ||
| @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); |
There was a problem hiding this comment.
오 서브쿼리를 안 사용해도 위와 같은 방식을 사용해도 될 것 같은데요??
@coderabbitai 저러면 commentJpaEntity에서 postJpaEntity를 참조할때 추가적인 SELECT 쿼리는 안나가지??
The merge-base changed after approval.
#️⃣ 연관된 이슈
📝 작업 내용
📸 스크린샷
💬 리뷰 요구사항
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
신규 기능
버그 수정
테스트
문서화
기타