-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] hibernate filter 적용 #287
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
[feat] hibernate filter 적용 #287
Conversation
- status 필드값을 필터로 정의
- hibernate @filter 를 트랜잭션 경계 진입 시 자동 적용하는 AOP 추가 - 세션 단위에서 filter enable/disable 을 판단하여 filter 중복 적용 방지
- inactive 상태(= soft delete) 인 jpa entity 도 같이 조회 or 필터링 없이 모든 jpa entity 조회할 수 있도록 어노테이션 추가
- 등록한 hibernate filter 가 잘 동작하는지 확인하기 위해 테스트 코드 작성
- 누락된 nullable = false 추가 - jpa 상속 관계인 엔티티들은 single table 전략 이슈로 인해 nullable = true 유지
- status filter 를 적용하기 위한 트랜잭션을 서비스 메서드가 시작하도록 명시
- spring data jpa repository 가 제공하는 디폴트 findById 메서드는 내부적으로 바로 PK 기준으로 em.find() 로 동작하므로 filter 적용대상이 아님 - 따라서 소프트 딜리트 적용 대상 entity 를 단건조회할 시에는 custom 메서드를 정의하여 사용해야함
- soft delete 적용 대상인 entity 조회 시 custom 단건 조회 메서드 호출하도록 수정 - status 조건 코드 삭제
- soft delete 적용 대상인 entity 삭제 시 deleteAllInBatch 메서드 호출하도록 수정
WalkthroughBaseJpaEntity에 상태 필드와 Hibernate Filter를 추가하고 AOP로 필터를 제어하며(IncludeInactive/Unfiltered), 다수의 리포지토리 메서드를 ID 기반 단건 조회로 교체하고 상태 기반 조건을 제거/조정했습니다. 연관관계의 non-null 제약 강화, User에 대한 @SQLDelete 추가, 트랜잭션(readOnly) 애너테이션 및 관련 테스트 보강이 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant Service as Application Service
participant Aspect as StatusFilterAspect
participant EM as EntityManager
participant HS as Hibernate Session
participant Repo as Repository
Client->>Service: 읽기 메서드 호출
activate Service
Service->>Aspect: around advice 진입
Aspect->>EM: em.unwrap(Session)
EM-->>Aspect: Session
Aspect->>HS: statusFilter enable(ACTIVE) / or modify for IncludeInactive / disable for Unfiltered
Aspect-->>Service: proceed()
Service->>Repo: repository 조회 (findByXxx)
Repo-->>Service: 엔티티 반환
Service->>Aspect: advice 종료
Aspect->>HS: 필터 상태 복원/비활성화
deactivate Service
Service-->>Client: 결과 반환
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
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/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Test Results448 tests 448 ✅ 40s ⏱️ Results for commit 8c20e29. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (20)
src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchDeleteService.java (1)
21-27: Hibernate 필터 적용 경로로 getByIdOrThrow 구현 변경 필요
RecentSearchCommandPort.getByIdOrThrow의 기본 구현은JpaRepository.findById(EntityManager.find 호출)을 사용해 Hibernate 필터가 적용되지 않습니다.- 필터가 활성화된 JPQL/HQL 쿼리 혹은 커스텀 EntityManager 쿼리로 변경해 Hibernate 필터가 보장되도록 수정해야 합니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java (1)
43-49: Book 조회는 여전히 findById 사용 — 필터 우회로 INACTIVE 연관 허용 가능성여기서는
bookJpaRepository.findById(...)를 유지하고 있어 상태 필터가 적용되지 않습니다. 결과적으로 INACTIVE Book과의 연관이 생성될 수 있습니다.findByBookId(...)(필터 적용 JPQL)로 통일해 주세요.- BookJpaEntity bookJpaEntity = bookJpaRepository.findById(room.getBookId()).orElseThrow( + BookJpaEntity bookJpaEntity = bookJpaRepository.findByBookId(room.getBookId()).orElseThrow( () -> new EntityNotFoundException(BOOK_NOT_FOUND) );src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java (1)
12-40: Boolean 오토 언박싱 NPE 위험 제거 권장
Boolean파라미터를 바로!isPageFilter처럼 사용하면 null 입력 시 NPE가 발생합니다. 안전 비교로 치환하거나 primitive boolean으로 바꾸세요.- public void validateGroupRoomPostFilters(Integer pageStart, Integer pageEnd, Boolean isPageFilter, Boolean isOverview, int bookPageSize, double currentPercentage) { - if(!isPageFilter && !isOverview) { // 어떤 필터도 적용되지 않는 경우 + public void validateGroupRoomPostFilters(Integer pageStart, Integer pageEnd, Boolean isPageFilter, Boolean isOverview, int bookPageSize, double currentPercentage) { + boolean pageFilter = Boolean.TRUE.equals(isPageFilter); + boolean overview = Boolean.TRUE.equals(isOverview); + if(!pageFilter && !overview) { // 어떤 필터도 적용되지 않는 경우 ... - if(!isPageFilter && isOverview) { // 총평보기 필터만 적용된 경우 + if(!pageFilter && overview) { // 총평보기 필터만 적용된 경우 ... - if(isPageFilter && !isOverview) { // 페이지 필터만 적용된 경우는 pageStart와 pageEnd가 null이여도 됨 + if(pageFilter && !overview) { // 페이지 필터만 적용된 경우는 pageStart와 pageEnd가 null이여도 됨 ... - if(isPageFilter && isOverview) { // 페이지 필터와 총평보기 필터가 동시에 적용된 경우 + if(pageFilter && overview) { // 페이지 필터와 총평보기 필터가 동시에 적용된 경우 ... } - public void validateMyRoomPostFilters(Integer pageStart, Integer pageEnd, Boolean isPageFilter, Boolean isOverview, String sort) { + public void validateMyRoomPostFilters(Integer pageStart, Integer pageEnd, Boolean isPageFilter, Boolean isOverview, String sort) { // 모든 파라미터중 하나라도 null이 아닌 경우 예외 발생 - if (pageStart != null || pageEnd != null || isPageFilter || isOverview || sort != null) { + boolean pageFilter = Boolean.TRUE.equals(isPageFilter); + boolean overview = Boolean.TRUE.equals(isOverview); + if (pageStart != null || pageEnd != null || pageFilter || overview || sort != null) { ... }Also applies to: 42-46
src/main/java/konkuk/thip/roompost/application/service/manager/RoomProgressManager.java (1)
41-53: 평균 계산 시 0으로 나누기/Infinity 가능성참가자 1명만 남은 상태에서 해당 유저가 나가면 분모
(remainingParticipants.size() - 1)가 0이 됩니다. 현재 로직은double나눗셈으로Infinity가 저장될 수 있습니다. 안전 가드 추가가 필요합니다.- List<RoomParticipant> remainingParticipants = roomParticipantCommandPort.findAllByRoomId(room.getId()); - double total = remainingParticipants.stream() - .filter(p -> !p.getId().equals(removeRoomParticipantId)) // 나간 유저 제외 - .mapToDouble(RoomParticipant::getUserPercentage) - .sum(); - room.updateRoomPercentage(total / (remainingParticipants.size() - 1)); + List<RoomParticipant> remainingParticipants = roomParticipantCommandPort.findAllByRoomId(room.getId()); + int denom = remainingParticipants.size() - 1; // 나간 유저 제외한 인원 수 + if (denom <= 0) { + room.updateRoomPercentage(0); + } else { + double total = remainingParticipants.stream() + .filter(p -> !p.getId().equals(removeRoomParticipantId)) // 나간 유저 제외 + .mapToDouble(RoomParticipant::getUserPercentage) + .sum(); + room.updateRoomPercentage(total / denom); + }src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java (1)
32-49: Hibernate 상태 필터 기본 활성화로 INACTIVE 참조 누락 위험 → findUnusedBookIds() 호출 컨텍스트에 @IncludeInactive 또는 @unfiltered 어노테이션 적용 필요
- 대상 파일
- src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java
- src/main/java/konkuk/thip/book/application/service/BookCleanUpService.java
- 대안: “활성 참조가 없는 책” 의도 메서드/주석에 명시하고 검증용 테스트 추가
src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java (1)
60-68: 자식이 없는 루트 댓글의 ‘좋아요’ 체크 누락 버그parseAllCommentIds가 childrenMap.keySet()만 사용해, 자식이 없는 루트의 ID가 제외됩니다. 해당 루트의 좋아요 여부가 항상 false가 됩니다.
아래처럼 루트 리스트를 함께 받아 모두 포함하도록 수정하고, 호출부도 교체해 주세요.
- Set<Long> allCommentIds = parseAllCommentIds(childrenMap); + Set<Long> allCommentIds = parseAllCommentIds(rootsInOrder, childrenMap);- private Set<Long> parseAllCommentIds(Map<Long, List<CommentQueryDto>> childrenMap) { - Set<Long> allCommentIds = new HashSet<>(childrenMap.keySet()); // 루트 댓글들 - for (Long rootCommentId : childrenMap.keySet()) { - childrenMap.get(rootCommentId).stream() - .map(CommentQueryDto::commentId) - .forEach(allCommentIds::add); - } - return allCommentIds; - } + private Set<Long> parseAllCommentIds(List<CommentQueryDto> roots, + Map<Long, List<CommentQueryDto>> childrenMap) { + Set<Long> allCommentIds = roots.stream() + .map(CommentQueryDto::commentId) + .collect(Collectors.toCollection(HashSet::new)); + for (Map.Entry<Long, List<CommentQueryDto>> e : childrenMap.entrySet()) { + for (CommentQueryDto child : e.getValue()) { + allCommentIds.add(child.commentId()); + } + } + return allCommentIds; + }Also applies to: 47-49
src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java (1)
110-116: 테스트 데이터 정리 일관성: 배치 삭제로 통일필터 도입 후 deleteAll()은 ACTIVE만 지우고 INACTIVE가 남을 수 있어 테스트 간 간섭 위험이 있습니다. 아래를 배치 삭제로 통일해 주세요.
void tearDown() { roomParticipantJpaRepository.deleteAllInBatch(); - roomJpaRepository.deleteAll(); - bookJpaRepository.deleteAll(); - userJpaRepository.deleteAll(); + roomJpaRepository.deleteAllInBatch(); + bookJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); }src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java (1)
27-31: 소프트 딜리트 적용을 위해findByCommentId사용
Hibernate Filter가EntityManager.find기반의findById호출을 우회하므로, 비활성 댓글 조회를 방지하려면 다음과 같이 수정하세요.- CommentJpaEntity comment = commentJpaRepository.findById(commentId) + CommentJpaEntity comment = commentJpaRepository.findByCommentId(commentId) .orElseThrow(() -> new EntityNotFoundException(COMMENT_NOT_FOUND));src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java (1)
57-88: N+1 제거: 상위 투표 조회를 3쿼리(batch)로 리팩터링현재 상위 투표 목록 조회 후, 각 투표의 아이템을 투표 개수만큼 추가 조회하여 N+1이 발생합니다. 성능/부하 관점에서 상위 투표 ID 목록 1쿼리 → 투표 1쿼리 → 해당 아이템들 1쿼리로 줄이는 것을 권장합니다. (과거 러닝: Vote/VoteItem는 N+1 방지 권장)
제안 diff:
@@ - public List<RoomPlayingDetailViewResponse.CurrentVote> findTopParticipationVotesByRoom(Long roomId, int count) { - // 1. Fetch top votes by total participation count - List<VoteJpaEntity> topVotes = jpaQueryFactory - .select(vote) - .from(vote) - .join(voteItem).on(voteItem.voteJpaEntity.eq(vote)) // vote item이 없는 경우 포함 X - .where(vote.roomJpaEntity.roomId.eq(roomId)) - .groupBy(vote) - .orderBy(voteItem.count.sum().desc()) // 해당 투표에 참여한 총 참여자 수 기준 내림차순 정렬 - .limit(count) - .fetch(); - - // 2. Map to DTOs including vote items - return topVotes.stream() - .map(vote -> { - List<RoomPlayingDetailViewResponse.CurrentVote.VoteItem> voteItems = jpaQueryFactory - .select(voteItem) - .from(voteItem) - .where(voteItem.voteJpaEntity.eq(vote)) - .orderBy(voteItem.count.desc()) - .fetch() - .stream() - .map(item -> new RoomPlayingDetailViewResponse.CurrentVote.VoteItem(item.getItemName())) - .toList(); - return new RoomPlayingDetailViewResponse.CurrentVote( - vote.getContent(), - vote.getPage(), - vote.isOverview(), - voteItems - ); - }) - .toList(); - } + public List<RoomPlayingDetailViewResponse.CurrentVote> findTopParticipationVotesByRoom(Long roomId, int count) { + // 1) 상위 투표 ID 선별 (합계 동률 방지를 위해 postId 서브 정렬 추가) + List<Long> topVoteIds = jpaQueryFactory + .select(vote.postId) + .from(vote) + .join(voteItem).on(voteItem.voteJpaEntity.eq(vote)) + .where(vote.roomJpaEntity.roomId.eq(roomId)) + .groupBy(vote.postId) + .orderBy(voteItem.count.sum().desc(), vote.postId.asc()) + .limit(count) + .fetch(); + + if (topVoteIds.isEmpty()) { + return List.of(); + } + + // 2) 투표 본문 일괄 조회 + List<VoteJpaEntity> topVotes = jpaQueryFactory + .selectFrom(vote) + .where(vote.postId.in(topVoteIds)) + .fetch(); + + // 3) 투표 아이템 일괄 조회(안정적 정렬) + List<QVoteItemJpaEntity> items = jpaQueryFactory + .selectFrom(voteItem) + .where(voteItem.voteJpaEntity.postId.in(topVoteIds)) + .orderBy(voteItem.voteJpaEntity.postId.asc(), voteItem.count.desc(), voteItem.voteItemId.asc()) + .fetch(); + + // 4) 매핑 + var itemsByVoteId = items.stream().collect( + java.util.stream.Collectors.groupingBy(i -> i.getVoteJpaEntity().getPostId())); + + // topVoteIds 순서 보존 + var voteById = topVotes.stream().collect(java.util.stream.Collectors.toMap(VoteJpaEntity::getPostId, v -> v)); + return topVoteIds.stream().map(id -> { + VoteJpaEntity v = voteById.get(id); + List<RoomPlayingDetailViewResponse.CurrentVote.VoteItem> voteItems = + itemsByVoteId.getOrDefault(id, List.of()).stream() + .map(item -> new RoomPlayingDetailViewResponse.CurrentVote.VoteItem(item.getItemName())) + .toList(); + return new RoomPlayingDetailViewResponse.CurrentVote( + v.getContent(), + v.getPage(), + v.isOverview(), + voteItems + ); + }).toList(); + }src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java (2)
85-107: 무결점 페이지네이션 보장을 위한 2차 정렬 키 추가 필요createdAt만으로 정렬/커서(page size + 1, createdAt < cursor)하면 동일 타임스탬프 레코드가 경계에 있을 때 일부 항목이 다음 페이지에서 “스킵”됩니다. 안정 정렬을 위해 단조 증가 PK를 2차 정렬 키로 추가하고, 커서도 (createdAt, id) 쌍으로 운용하는 것을 권장합니다.
최소 변경(정렬 안정화만) 예:
- .orderBy(postLike.createdAt.desc()) + .orderBy(postLike.createdAt.desc(), postLike.postLikeId.desc())근본 해결(커서 2종) 방향:
- API 파라미터에 cursorId(Long)를 추가
- where에 “createdAt < cursorAt OR (createdAt = cursorAt AND id < cursorId)” 적용
원하시면 해당 서명 변경까지 포함한 전체 리팩터링 패치를 제안드리겠습니다.
116-139: 댓글 피드도 동일한 페이지네이션 안정성 이슈위 좋아요 쿼리와 동일한 이유로 comment.createdAt만 사용하면 스킵이 발생합니다. 2차 정렬 키(예: comment.commentId desc) 추가 및 쌍 커서로의 확장 고려가 필요합니다.
- .orderBy(comment.createdAt.desc()) + .orderBy(comment.createdAt.desc(), comment.commentId.desc())src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (1)
410-415: userId가 null일 때의 조건 처리: 불필요한 서브쿼리 및 잠재적 의미 왜곡비로그인 사용자를 허용하는 케이스라면 userJoinedRoom(userId).not()은 “user_id IS NULL” 조건으로 변질되거나 불필요한 exists 서브쿼리를 야기합니다. null 시 항상 true 조건으로 대체해 서브쿼리를 생략하세요.
+import com.querydsl.core.types.dsl.Expressions; @@ - return room.category.eq(category) + return room.category.eq(category) .and(room.startDate.after(LocalDate.now())) .and(room.isPublic.isTrue()) - .and(userJoinedRoom(userId).not()); + .and(userId != null ? userJoinedRoom(userId).not() : Expressions.TRUE.isTrue());src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java (1)
74-83: Book 조회에도 soft-delete 필터 적용 위해 findByBookId 파생 메서드 사용
BookJpaRepository에Optional<BookJpaEntity> findByBookId(Long bookId);추가BookCommandPersistenceAdapter에서findById(bookId)→findByBookId(bookId)변경- BookJpaEntity book = userJpaRepository.findById(userId) + BookJpaEntity book = bookJpaRepository.findByBookId(bookId) .orElseThrow(() -> new EntityNotFoundException(BOOK_NOT_FOUND));src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java (2)
60-69: saveAllVoteItems: 동일 voteId 보장 검증 추가 필요첫 요소의 voteId에 의존합니다. 혼합 입력 시 잘못된 매핑이 발생합니다. 간단한 방어 로직을 추가해 주세요.
Long voteId = voteItems.get(0).getVoteId(); +if (voteItems.stream().anyMatch(v -> !java.util.Objects.equals(v.getVoteId(), voteId))) { + throw new IllegalArgumentException("All voteItems must share the same voteId"); +} VoteJpaEntity voteJpaEntity = voteJpaRepository.findByPostId(voteId).orElseThrow( () -> new EntityNotFoundException(VOTE_NOT_FOUND) );
109-121: Hibernate 필터 우회 방지: findById 사용 대체
VoteItemJpaRepository에 Hibernate 필터가 적용된 JPQL 쿼리 메서드 추가@Query("select vi from VoteItemJpaEntity vi where vi.id = :id") Optional<VoteItemJpaEntity> findActiveById(@Param("id") Long id);VoteCommandPersistenceAdapter에서 아래 세 곳의findById(...)호출을 모두findActiveById(...)로 변경
findVoteItemById(Long id)saveVoteParticipant(...)내부updateVoteItem(...)내부- 이 수정으로 Hibernate
statusFilter가 적용되어INACTIVE상태의 VoteItem 조회 및 저장이 차단됩니다.src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java (1)
230-242: 부모 상태 필터 정책 결정 필요
본 메서드(행 230–242)에서만parentComment.status.eq(ACTIVE)필터가 적용되어 다른 댓글 조회 로직과 정책이 일치하지 않습니다. 정책에 따라 해당 필터를 유지하거나 제거하고, 부모 INACTIVE + 자식 ACTIVE 케이스에 대한 테스트를 추가하세요.src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (4)
74-82: order 보존 매핑 구간에도 null 필터 적용 필요위 이슈와 동일합니다. 아래 변경을 권장합니다.
- List<FeedJpaEntity> ordered = feedIds.stream() - .map(entityMap::get) - .toList(); + List<FeedJpaEntity> ordered = feedIds.stream() + .map(entityMap::get) + .filter(Objects::nonNull) + .toList();
96-104: null 안전성 보강(최신 피드 경로)동일 수정 적용 바랍니다.
- List<FeedJpaEntity> ordered = feedIds.stream() - .map(entityMap::get) - .toList(); + List<FeedJpaEntity> ordered = feedIds.stream() + .map(entityMap::get) + .filter(Objects::nonNull) + .toList();
183-191: null 안전성 보강(내 피드 경로)동일 수정 적용 바랍니다.
- List<FeedJpaEntity> ordered = feedIds.stream() - .map(entityMap::get) - .toList(); + List<FeedJpaEntity> ordered = feedIds.stream() + .map(entityMap::get) + .filter(Objects::nonNull) + .toList();
203-210: null 안전성 보강(특정 사용자 피드 경로)동일 수정 적용 바랍니다.
- List<FeedJpaEntity> ordered = feedIds.stream() - .map(entityMap::get) - .toList(); + List<FeedJpaEntity> ordered = feedIds.stream() + .map(entityMap::get) + .filter(Objects::nonNull) + .toList();
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java (1)
3-3: HelperService import 경로 변경: 이상 없음단순 경로 변경으로 보이며 동작 영향 없음. 포인트컷 유효성 점검은 앞 파일 코멘트 참고.
🧹 Nitpick comments (65)
src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchDeleteService.java (1)
21-24: NPE 방지: ID 비교는 Objects.equals 사용 권장recentSearch.getUserId()가 null일 경우 NPE 가능성이 있습니다. null‑safe 비교로 바꾸세요.
- if (!recentSearch.getUserId().equals(userId)) { + if (!java.util.Objects.equals(recentSearch.getUserId(), userId)) {src/main/java/konkuk/thip/recentSearch/application/service/RecentSearchGetService.java (1)
22-29: MAX_RECENT_SEARCHES 설정값 외부화 제안운영 중 조정 가능성을 고려하면 설정(property)로 분리하는 편이 유연합니다. 예: recent-search.max-count.
// 예시: @ConfigurationProperties 또는 @Value 사용 @Value("${recent-search.max-count:5}") private int maxRecentSearches;src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java (3)
99-99: 팔로워 조회 where 절 단순화 OK + 인덱스 권고상태 조건 제거는 필터 위임 관점에서 적절합니다. 성능을 위해 다음 복합 인덱스를 권고합니다: (following_user_id, created_at DESC).
119-119: 팔로잉 조회 where 절 단순화 OK필터 위임과 정합됩니다. (user_id, created_at DESC) 복합 인덱스도 함께 고려해 주세요.
31-34: fetchOne()—다건 조회 시 NonUniqueResultException 방지용 limit(1)/fetchFirst() 적용 권장
현재 JPA 엔티티나 마이그레이션에서(user_id, following_user_id)복합 유니크 제약이 선언되어 있지 않아,fetchOne()이 다건 결과에서 예외를 던질 수 있습니다. 다음과 같이limit(1)혹은fetchFirst()사용으로 방어하세요..selectFrom(following) .where(following.userJpaEntity.userId.eq(userId) .and(following.followingUserJpaEntity.userId.eq(targetUserId))) - .fetchOne(); + .limit(1) + .fetchOne(); // 또는 .fetchFirst()src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java (1)
28-30: 관계의 널 불가 제약 강화는 좋습니다. JPA 레벨에서도 optional=false로 일치시키세요DB 제약(nullable = false)와 JPA 메타데이터의 일관성을 위해 @manytoone(optional = false)를 권장합니다. 또한 마이그레이션에서 post_id NOT NULL 반영 여부를 확인해주세요. 컬럼명이 voteJpaEntity와 불일치(post_id)해 혼란을 줄 수 있으니 추후 리네이밍도 고려 바랍니다.
다음 패치를 제안합니다:
- @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "post_id", nullable = false)src/main/java/konkuk/thip/user/application/service/UserViewAliasChoiceService.java (1)
10-10: DB 접근이 없는 메서드에 @transactional은 불필요순수 Enum 매핑 조회만 수행하므로 트랜잭션 오버헤드를 줄이기 위해 제거를 권장합니다(상태 필터 AOP와도 무관).
-import org.springframework.transaction.annotation.Transactional; @@ - @Transactional(readOnly = true) public UserViewAliasChoiceResult getAllAliasesAndCategories() {Also applies to: 19-20
src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java (1)
9-10: @SQLDelete 적용은 적절합니다. status 매핑은 EnumType.STRING으로 확인되었습니다.
BaseJpaEntity의status필드는@Enumerated(EnumType.STRING)으로 매핑되어 있어'INACTIVE'리터럴이 일치합니다.- 운영·감사용으로 삭제 시
updatedAt동기화가 필요하다면 아래와 같이 보강을 검토하세요.- @SQLDelete(sql = "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?") + @SQLDelete(sql = "UPDATE users SET status = 'INACTIVE', updated_at = CURRENT_TIMESTAMP WHERE user_id = ?")build.gradle (1)
38-39: 테스트 소스에서 Lombok 애너테이션 처리 활성화 OKtestCompileOnly/testAnnotationProcessor 추가로 테스트 코드 Lombok 컴파일 문제가 방지됩니다. MapStruct+Lombok를 함께 쓰는 모듈이 있다면 lombok-mapstruct-binding 추가도 고려하세요(빌드 오류 시).
가능 시 추가:
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' testAnnotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordQueryRepositoryImpl.java (2)
66-67: 상태 조건 제거로 조인 대상(User)까지 필터가 전파되는 의미 변화 확인 필요상태 필터를 코드에서 제거했고(AOP+Hibernate Filter 의존), inner join(post.userJpaEntity)이라 사용자 INACTIVE 시 해당 게시글도 결과에서 탈락합니다. 의도라면 OK, 아니라면 leftJoin으로 완화가 필요합니다.
의도가 “사용자 비활성이어도 게시글은 보이게”라면:
- .join(post.userJpaEntity, user) + .leftJoin(post.userJpaEntity, user)(선택) 사용자 메타데이터가 필요해 null 허용 시 select DTO 필드 처리 보완 필요
116-117: 그룹 조회 쿼리도 동일한 의미 변화 적용됨여기도 상태 필터를 코드에서 제거하면서 join으로 인해 User 상태에 종속됩니다. 두 메서드(findMyRecords/findGroupRecords)의 정책 일관성을 결정해 주세요.
정책 결정을 주석으로 남기면 향후 유지보수에 도움이 됩니다.
src/test/java/konkuk/thip/room/adapter/in/web/RoomParticipantDeleteApiTest.java (1)
138-140: soft delete 검증 포인트 명확화 OK, 테스트 명칭만 정교화 제안상태가 INACTIVE로 전환됨을 직접 확인하는 접근은 좋습니다. 다만 테스트 설명(“삭제”)을 “비활성화”로 정리하면 혼선을 줄일 수 있습니다.
(선택) 방 참여자 조회 API/리포지토리가 INACTIVE를 기본적으로 숨기는지도 추가 검증하면 회귀를 막을 수 있습니다.
src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteParticipantJpaEntity.java (1)
29-32: 널 방지 가드 추가 제안
voteItemJpaEntity가 Not-Null 제약이므로 업데이트 메서드에도 NPE 방지 가드를 두면 좋습니다.- public VoteParticipantJpaEntity updateVoteItem(VoteItemJpaEntity voteItemJpaEntity) { - this.voteItemJpaEntity = voteItemJpaEntity; + public VoteParticipantJpaEntity updateVoteItem(VoteItemJpaEntity voteItemJpaEntity) { + this.voteItemJpaEntity = java.util.Objects.requireNonNull( + voteItemJpaEntity, "voteItemJpaEntity must not be null"); return this; }src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java (1)
77-78: deleteAllInBatch 전환은 테스트 정리 성능에 유리 — 부수효과만 유의
- 배치 삭제는 엔티티 라이프사이클 콜백을 우회합니다. 테스트에서 의도된 동작인지 확인 바랍니다.
- Redis 키 정리: 첫 테스트에서 설정한
searchRankDetailPrefix + yyyyMMdd키가 테스트 순서에 따라 잔존할 수 있습니다.@AfterEach에서scan또는keys로 prefix 기반 일괄 삭제를 권장합니다.예시:
@AfterEach void tearDown() { userJpaRepository.deleteAllInBatch(); // Redis cleanup var pattern = searchRankDetailPrefix + "*"; var keys = redisTemplate.keys(pattern); if (keys != null && !keys.isEmpty()) { redisTemplate.delete(keys); } }추가로,
setUp()내nicknameUpdatedAt(...)가 두 번 설정되어 첫 번째 값이 덮어쓰여집니다(라인 68~70 부근). 하나만 남기세요.src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (1)
35-37: 주석과 매핑 불일치 — nullable/optional 명시해주세요주석에 nullable=true라고 적혀 있으나 애노테이션에 명시가 없습니다. 혼동 방지와 스키마 명확화를 위해 명시적으로 설정하는 편이 안전합니다.
- @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "book_id") // RECORD, VOTE 로 인해 nullable = true로 설정 + @ManyToOne(fetch = FetchType.LAZY, optional = true) + @JoinColumn(name = "book_id", nullable = true) // RECORD, VOTE 로 인해 nullable = true로 설정src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java (1)
50-69: 가림 문자열 길이 과다 생성(성능/메모리) 및 길이 불일치항상
repeat+1번 붙여 원문보다 과도하게 생성합니다. 정확 길이만 생성/반환하도록 수정하세요.- int originalLength = contents.length(); - int blurLen = BLURRED_STRING.length(); - // 필요한 전체 반복 횟수 계산 - int repeat = originalLength / blurLen; - StringBuilder sb = new StringBuilder(originalLength); - // 몫 만큼 반복 - for (int i = 0; i < repeat + 1; i++) { - sb.append(BLURRED_STRING); - } - return sb.toString(); + int originalLength = contents.length(); + int blurLen = BLURRED_STRING.length(); + int repeat = (originalLength + blurLen - 1) / blurLen; // 올림 + StringBuilder sb = new StringBuilder(originalLength); + for (int i = 0; i < repeat; i++) { + sb.append(BLURRED_STRING); + } + sb.setLength(originalLength); // 정확 길이로 절단 + return sb.toString();src/main/java/konkuk/thip/roompost/adapter/out/jpa/RecordJpaEntity.java (1)
26-29: 주석과 매핑 의도 명확화: optional/nullable를 명시하세요.주석은 nullable=true를 지향하지만 어노테이션에는 명시가 없어 혼동 소지가 있습니다. 의도를 코드에 드러내 주세요.
- @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "room_id") // FEED 로 인해 nullable = true로 설정 + @ManyToOne(fetch = FetchType.LAZY, optional = true) + @JoinColumn(name = "room_id", nullable = true) // FEED 로 인해 nullable = true로 설정src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java (1)
51-58: 동일 패턴 LGTM + 미세 최적화 제안.동일 흐름으로 OK. 성능 미세화가 필요하다면 사용자 검증은 existsByUserId로 대체(엔티티 미로딩) 후 책 조회를 수행할 수 있습니다.
- UserJpaEntity user = userJpaRepository.findByUserId(userId) - .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); - - return bookJpaRepository.findJoiningRoomsBooksByUserId(user.getUserId()) + if (!userJpaRepository.existsByUserId(userId)) { + throw new EntityNotFoundException(USER_NOT_FOUND); + } + return bookJpaRepository.findJoiningRoomsBooksByUserId(userId) .stream() .map(bookMapper::toDomainEntity) .collect(Collectors.toList());src/main/java/konkuk/thip/feed/application/service/FeedRelatedWithBookService.java (1)
54-59: 중복 Set 생성 제거 및 빈 결과 시 DB 호출 생략으로 경로 최적화feedId 집합을 한 번만 계산하고, 비어 있으면 저장/좋아요 조회를 건너뛰면 호출 2회를 줄일 수 있습니다.
- // 사용자가 저장한 피드 ID를 조회 - Set<Long> savedFeedIds = feedQueryPort.findSavedFeedIdsByUserIdAndFeedIds( - feedQueryDtoCursorBasedList.contents().stream() - .map(FeedQueryDto::feedId) - .collect(Collectors.toSet()), - query.userId() - ); - - // 사용자가 좋아요한 피드 ID를 조회 - Set<Long> likedFeedIds = postLikeQueryPort.findPostIdsLikedByUser( - feedQueryDtoCursorBasedList.contents().stream() - .map(FeedQueryDto::feedId) - .collect(Collectors.toSet()), - query.userId() - ); + // 공통 feedId 집합 + Set<Long> feedIds = feedQueryDtoCursorBasedList.contents().stream() + .map(FeedQueryDto::feedId) + .collect(Collectors.toSet()); + + // 사용자가 저장/좋아요한 피드 ID 조회(빈 결과면 호출 생략) + Set<Long> savedFeedIds = feedIds.isEmpty() ? Set.of() + : feedQueryPort.findSavedFeedIdsByUserIdAndFeedIds(feedIds, query.userId()); + Set<Long> likedFeedIds = feedIds.isEmpty() ? Set.of() + : postLikeQueryPort.findPostIdsLikedByUser(feedIds, query.userId());Also applies to: 62-67
src/main/java/konkuk/thip/roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java (1)
27-33: JPA 의미반영을 위해 @manytoone(optional = false) 명시 권장DDL의 nullable=false와 함께 JPA 레벨에서도 optional=false를 선언해 의미/최적화를 일치시키는 것이 좋습니다.
- @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "room_id", nullable = false) private RoomJpaEntity roomJpaEntity; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity;src/main/java/konkuk/thip/common/annotation/persistence/IncludeInactive.java (2)
8-11: 주석/메타 애노테이션 보강 제안 (@documented 추가)런타임/타깃 지정은 적절합니다. Javadoc 노출과 툴링 가시성을 위해 @documented를 추가하는 것을 권장합니다.
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) +@Documented public @interface IncludeInactive { }
3-10: (선택) 명확한 사용 의도 주석 추가해당 애노테이션이 “필터를 끄는 것(Unfiltered)”이 아니라 “INACTIVE 포함” 파라미터 전환임을 Javadoc으로 명시해 두면, AOP 포인트컷/필터 파라미터 연계 시 혼동을 줄일 수 있습니다.
+/** + * 상태 기반 하이버네이트 필터(statusFilter) 사용 시, INACTIVE 데이터를 포함하도록 동작을 전환하기 위한 마커 애노테이션입니다. + * - 메서드/타입 수준 모두에서 사용 가능 + * - Unfiltered 와는 다르게 필터 자체를 해제하지 않고 포함 범위를 넓힙니다. + */ public @interface IncludeInactive {src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java (2)
176-178: NPE 방지 및 의도 명확화: Optional 즉시 검증findById(...).orElse(null) 사용은 NPE 위험이 있습니다. 테스트 실패 시 원인 파악을 위해 즉시 예외 or not-null 검증을 권장합니다.
-RoomParticipantJpaEntity member = roomParticipantJpaRepository.findById(memberParticipation.getRoomParticipantId()).orElse(null); -assertThat(member.getStatus()).isEqualTo(INACTIVE); +RoomParticipantJpaEntity member = roomParticipantJpaRepository + .findById(memberParticipation.getRoomParticipantId()) + .orElseThrow(); +assertThat(member.getStatus()).isEqualTo(INACTIVE);
160-168: (선택) 취소(idempotency) 시나리오 추가 제안이미 취소된 참여자에 대해 CANCEL을 다시 호출할 때의 처리(무해/에러)를 명시적으로 검증하는 테스트를 추가하면 소프트 삭제 전환 후 경계 조건 품질이 좋아집니다.
src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java (1)
84-86: 널 가드 추가로 테스트 실패 원인 가시성 향상findById() 결과에 대해 상태만 바로 검증하면 NPE로 실패 원인이 불명확할 수 있습니다. not-null 선검증을 권장합니다. (참고: findById는 필터 영향 없이 PK 조회)
FeedJpaEntity feedJpaEntity = feedJpaRepository.findById(feed.getPostId()).orElse(null); +assertThat(feedJpaEntity).isNotNull(); assertThat(feedJpaEntity.getStatus()).isEqualTo(INACTIVE);src/main/java/konkuk/thip/common/annotation/persistence/Unfiltered.java (1)
8-11: 개발자 경험 개선: Javadoc/Documented/Inherited 추가 제안사용 의도가 핵심 인프라에 해당하므로 문서화와 상속 전파가 있으면 좋습니다. 베이스 서비스에 붙였을 때 자식 서비스에도 적용되길 원하면
@Inherited가 유용합니다.아래와 같이 보완을 제안합니다.
package konkuk.thip.common.annotation.persistence; import java.lang.annotation.ElementType; +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited public @interface Unfiltered { }추가 Javadoc 예시:
+/** + * 서비스/메서드 범위에서 Hibernate 상태 필터를 비활성화합니다. + * - 기본: AOP가 세션 진입 시 필터를 활성화 + * - @Unfiltered: 해당 범위에서 필터 비활성화 + */src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java (1)
13-17: LGTM:findByRoomId도입으로 필터 적용이 가능해졌습니다.PK 기반
findById회피 목적에 부합합니다. 운영 트래픽에서 해당 컬럼으로 단건 조회가 빈번하다면 DB에 유니크 인덱스/인덱스 생성 여부도 확인해 주세요. (예:unique index ux_room_room_id(room_id)).src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java (1)
8-14:findById오남용 방지를 위한 @deprecated 재선언 제안
JpaRepository에서 상속된 기본findById는 Hibernate 필터를 우회해 soft-delete 대상도 조회할 수 있으므로, 인터페이스에 재선언 후@Deprecated로 경고를 노출해 안전한findByUserId사용을 유도하세요.public interface UserJpaRepository extends JpaRepository<UserJpaEntity, Long>, UserQueryRepository { /** * 소프트 딜리트 적용 대상 entity 단건 조회 메서드 */ Optional<UserJpaEntity> findByUserId(Long userId); + /** + * Hibernate 필터 미적용. 사용 금지. + */ + @Deprecated + @Override + Optional<UserJpaEntity> findById(Long id); }src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java (3)
62-67: voteItem inner join으로 ‘아이템 없는 투표’가 제외됨 — 비즈니스 의도 확인아이템이 아직 생성되지 않은 투표를 노출해야 한다면
leftJoin + coalesce(sum, 0)이 더 안전합니다. 유지가 의도라면 이 코멘트는 무시하셔도 됩니다.- .join(voteItem).on(voteItem.voteJpaEntity.eq(vote)) // vote item이 없는 경우 포함 X + .leftJoin(voteItem).on(voteItem.voteJpaEntity.eq(vote)) // 아이템 없는 투표도 포함 @@ - .orderBy(voteItem.count.sum().desc()) + .orderBy(voteItem.count.sum().coalesce(0).desc())
93-95: 로컬 Q타입 재선언 제거로 혼동 방지클래스 필드(
voteItem,voteParticipant)가 이미 존재합니다. 메서드 내 동일명 재선언은 가독성을 떨어뜨립니다. 아래처럼 제거해 주세요.- QVoteItemJpaEntity voteItem = QVoteItemJpaEntity.voteItemJpaEntity; - QVoteParticipantJpaEntity voteParticipant = QVoteParticipantJpaEntity.voteParticipantJpaEntity; + // 클래스 필드 사용같은 정리가
findVoteItemsByVoteId에도 필요합니다.Also applies to: 118-120
96-113: 일관된 정렬 보장
mapVoteItemsByVoteIds는 정렬이 없어 UI/서버간 응답의 결정성이 떨어질 수 있습니다.findVoteItemsByVoteId와 동일한 기준을 적용하세요.return jpaQueryFactory .select(new QVoteItemQueryDto( @@ )) .from(voteItem) .where(voteItem.voteJpaEntity.postId.in(voteIds)) + .orderBy(voteItem.count.desc(), voteItem.voteItemId.asc()) .fetch();src/test/java/konkuk/thip/roompost/adapter/in/web/RecordDeleteApiTest.java (2)
92-95: NULL 안전성 보강삭제가 하드 딜리트로 바뀌더라도 NPE가 나지 않도록 널 체크를 추가하세요.
- RecordJpaEntity deletedRecord = recordJpaRepository.findById(record.getPostId()).orElse(null); - assertThat(deletedRecord.getStatus()).isEqualTo(INACTIVE); + RecordJpaEntity deletedRecord = recordJpaRepository.findById(record.getPostId()).orElse(null); + assertThat(deletedRecord).isNotNull(); + assertThat(deletedRecord.getStatus()).isEqualTo(INACTIVE);
97-99: 댓글 검증도 동일하게 널 체크 추가- CommentJpaEntity deleteComment = commentJpaRepository.findById(comment.getCommentId()).orElse(null); - assertThat(deleteComment.getStatus()).isEqualTo(INACTIVE); + CommentJpaEntity deleteComment = commentJpaRepository.findById(comment.getCommentId()).orElse(null); + assertThat(deleteComment).isNotNull(); + assertThat(deleteComment.getStatus()).isEqualTo(INACTIVE);src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java (3)
109-111: NULL 안전성 보강- CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(commentId).orElse(null); - assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); + CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(commentId).orElse(null); + assertThat(commentJpaEntity).isNotNull(); + assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE);
130-132: NULL 안전성 보강(대댓글)- CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(replyId).orElse(null); - assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); + CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(replyId).orElse(null); + assertThat(commentJpaEntity).isNotNull(); + assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE);
153-155: NULL 안전성 보강(일반 케이스)- CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(commentId).orElse(null); - assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); + CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(commentId).orElse(null); + assertThat(commentJpaEntity).isNotNull(); + assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE);src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java (1)
50-57: update에서 save 불필요 (더티 체킹 활용)영속 엔티티 필드만 갱신하면 트랜잭션 종료 시 플러시됩니다.
save는 중복 호출이므로 제거 권장.userJpaEntity.updateIncludeAliasFrom(user); - userJpaRepository.save(userJpaEntity); + // 더티 체킹으로 반영됨src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java (2)
10-14: ID 단건 조회 메서드 전환 타당
findByPostId는 JPQL 경유로 필터가 적용되어 soft delete 시 비노출됩니다. 주석에 “하이버네이트 필터가 적용됨”을 좀 더 명시하면 혼동이 줄어듭니다.
10-14: 아이템까지 한 번에 필요한 경우를 위한 EntityGraph 메서드 추가 제안빈번히 아이템을 함께 조회한다면 N+1 방지를 위해 전용 메서드를 추가하세요.
public interface VoteJpaRepository extends JpaRepository<VoteJpaEntity, Long>, VoteQueryRepository { @@ Optional<VoteJpaEntity> findByPostId(Long postId); + + // 아이템 함께 로딩 (필요 시) + // @EntityGraph(attributePaths = {"voteItems"}) + // Optional<VoteJpaEntity> findWithItemsByPostId(Long postId); }src/main/java/konkuk/thip/feed/adapter/out/jpa/SavedFeedJpaEntity.java (1)
20-26: 중복 저장 방지 유니크 키 제안 (user_id, post_id)저장 피드 중복을 애플리케이션에서 제거하더라도 경쟁 상태를 막으려면 DB 레벨 유니크 제약이 필요합니다. (SavedBook에도 동일 제안)
예: unique constraint (user_id, post_id) 추가하는 마이그레이션을 함께 배포하세요. 존재 여부는 다음으로 점검할 수 있습니다.
#!/bin/bash # saved_feeds 유니크 인덱스/제약 존재 여부 점검 rg -nPi 'saved_feeds.*(unique|uq|idx).*user_id.*post_id|unique\s*\(user_id\s*,\s*post_id\)' -C2src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java (2)
15-15: 사용하지 않는 정적 import 제거ACTIVE 상수는 더 이상 사용하지 않습니다. 정적 임포트를 제거해 주세요.
-import static konkuk.thip.common.entity.StatusType.ACTIVE;
29-30: 상태 필터 의존으로 의미가 바뀜 — 기본 필터 ON 전제 확인명시적 status 조건을 제거하고 필터에 위임했습니다. 서비스 진입 AOP가 항상 필터를 활성화한다는 전제가 필요합니다. 관리자/테스트에서 @IncludeInactive/@unfiltered 적용 시 카운트 값이 달라지는지 테스트로 보강해 주세요.
원하면, 기본 ON/해제 시나리오(ADMIN 포함/제외)에 대한 카운트 검증 테스트 템플릿을 추가로 드리겠습니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java (2)
41-47: distinct와 HashSet 동시 사용은 중복입니다DB에서 distinct로 중복 제거를 이미 수행하므로 컬렉션 변환 시 굳이 HashSet으로 한 번 더 중복 제거할 필요가 없습니다. 반환 타입 유지가 필요하다면 Set.copyOf를 사용해 의도를 명확히 하거나, HashSet을 유지할 거라면 쿼리의 distinct는 제거하세요.
가능한 최소 수정 예:
- return new HashSet<>( - queryFactory - .select(roomParticipant.userJpaEntity.userId) - .distinct() - .from(roomParticipant) - .join(roomParticipant.roomJpaEntity, room) - .where(room.bookJpaEntity.bookId.eq(bookId)) - .fetch() - ); + return Set.copyOf( + queryFactory + .select(roomParticipant.userJpaEntity.userId) + .distinct() + .from(roomParticipant) + .join(roomParticipant.roomJpaEntity, room) + .where(room.bookJpaEntity.bookId.eq(bookId)) + .fetch() + );
91-104: 선택: 작성자 조인 명시로 의도/필터 적용을 분명히select 절에서 post.userJpaEntity.nickname을 참조하고 있으나 명시 조인이 없어 구현체에 따라 암묵 조인 방식/타입이 달라질 수 있습니다. 작성자 엔티티에도 상태 필터가 적용돼야 한다면 명시 inner join을 권장합니다.
- .from(postLike) - .join(postLike.postJpaEntity, post) - .join(postLike.userJpaEntity, user) + .from(postLike) + .join(postLike.postJpaEntity, post) + .join(post.userJpaEntity, QUserJpaEntity.userJpaEntity) // author 조인 명시 + .join(postLike.userJpaEntity, user)src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java (1)
27-31: exists 쿼리의 미세 최적화COUNT 기반 exists는 불필요한 카운트를 유발합니다. Spring Data 파생 메서드로 대체하면 구현 단순화 및 벤더 최적화가 용이합니다.
- @Query("SELECT CASE WHEN COUNT(rp) > 0 THEN true ELSE false END " + - "FROM RoomParticipantJpaEntity rp " + - "WHERE rp.userJpaEntity.userId = :userId " + - "AND rp.roomJpaEntity.roomId = :roomId") - boolean existsByUserIdAndRoomId(@Param("userId") Long userId, @Param("roomId") Long roomId); + boolean existsByUserJpaEntity_UserIdAndRoomJpaEntity_RoomId(Long userId, Long roomId);src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (5)
39-44: 주석과 구현 불일치: “모집중 + ACTIVE” → 실제로는 startDate 조건만필터로 ACTIVE를 대체했으므로 주석을 갱신하세요. 가독성/혼선을 줄입니다.
- /** 모집중 + ACTIVE 공통 where */ + /** 모집중 공통 where (상태 필터는 Hibernate Filter로 처리) */
171-187: LocalDate.now() 직접 호출 대신 기준 시각 일관화메서드 내에서 today를 로컬 변수로 고정하면 평가 시점 일관성과 테스트 용이성이 올라갑니다(Clock 주입 고려).
- .where( - room.category.eq(category) - .and(room.startDate.after(LocalDate.now())) + final LocalDate today = LocalDate.now(); + .where( + room.category.eq(category) + .and(room.startDate.after(today))장기적으로는 DateUtil/Clock을 주입 받아 테스트에서 고정 시간을 사용하도록 리팩터링을 추천합니다.
200-206: 미사용 변수 제거userRoomSub가 선언만 되고 사용되지 않습니다. 제거해 주세요.
- QRoomParticipantJpaEntity userRoomSub = new QRoomParticipantJpaEntity("userRoomSub");
381-385: ISBN 검색에서도 now() 직접 사용 지양위와 동일하게 today 로컬 변수를 사용해 일관성을 유지하세요.
- BooleanExpression baseCondition = room.bookJpaEntity.isbn.eq(isbn) - .and(room.startDate.after(LocalDate.now())); + final LocalDate today = LocalDate.now(); + BooleanExpression baseCondition = room.bookJpaEntity.isbn.eq(isbn) + .and(room.startDate.after(today));
441-473: 인덱스/성능 조언: 조인/정렬 컬럼에 대한 보조 인덱스 제안다음 컬럼 조합에 인덱스가 없으면 스캔 비용이 큽니다. 트래픽이 많은 리스트 뷰라면 고려해 주세요.
- room(start_date, room_id), room(end_date, room_id)
- room(category, start_date, room_id)
- room(is_public, start_date)
- room_participant(user_id, room_id) — exists/조인 서브쿼리 최적화
- post_like(user_id, created_at, post_like_id), comment(user_id, created_at, comment_id) — 키셋/정렬
src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)
31-37: findById → findBy{User,Room}Id 전환은 필터 일관성 확보에 적합합니다. 유니크/인덱스 보장 확인 필요.STATUS 필터 기본 활성화 시 INACTIVE 참조를 막는 의도가 맞다면 OK. 다만 userId/roomId 컬럼이 유니크이고 인덱스가 있는지 점검해 주세요(없는 경우 save 시 불필요한 풀스캔/락 경쟁이 발생할 수 있습니다).
src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java (2)
45-52: saveVote: findById → findBy{User,Room}Id 전환 타당. 유니크/인덱스 확인 권장User/Room의 비즈니스 키 기반 조회는 필터 일관성 측면에서 적합합니다. 해당 컬럼 유니크·인덱스 존재 여부를 확인해 주세요.
139-149: delete: 하위 리소스 hard delete + 상위 soft delete 조합 의도 확인voteParticipant/voteItem을 deleteAllByVoteId로 물리 삭제하고 Vote는 soft delete 합니다. 감사/복구 요구가 있다면 하위도 soft delete가 일관됩니다. 현재가 의도라면 OK입니다.
src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java (1)
22-29: ParamDef 타입과 필터 파라미터 일치
현재StatusFilterAspect에서StatusType.ACTIVE.name()등으로 문자열을 전달하므로@ParamDef(type = String.class)를 그대로 유지하는 것이 맞습니다.
Enum 객체(List<StatusType>)를 직접 넘기려면 아래처럼 변경하세요.- @FilterDef( - name = "statusFilter", - parameters = @ParamDef(name = "statuses", type = String.class) - ) + @FilterDef( + name = "statusFilter", + parameters = @ParamDef(name = "statuses", type = konkuk.thip.common.entity.StatusType.class) + )그리고 Aspect에서
List.of(StatusType.ACTIVE, StatusType.INACTIVE)같은 식으로StatusType객체를 전달하도록 수정해야 바인딩 불일치가 없습니다.
필요 시org.hibernate.type.descriptor.java.EnumJavaType<StatusType>를 활용해 JavaType을 명시하는 방법도 고려해 보세요.src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java (2)
89-92: 카운트 결과 int 캐스팅 대신 안전한 변환 사용 권장오버플로 탐지를 위해 Math.toIntExact 사용을 권장합니다.
- return (int) feedJpaRepository.countAllFeedsByUserId(userId); + return Math.toIntExact(feedJpaRepository.countAllFeedsByUserId(userId));
94-97: Math.toIntExact 사용해 안전 캐스팅 적용
FeedQueryPersistenceAdapter.java:95–96에서(int) feedJpaRepository.countPublicFeedsByUserId(userId)대신return Math.toIntExact(feedJpaRepository.countPublicFeedsByUserId(userId));를 사용해 장기(long) → 정수(int) 변환 시 오버플로우를 방지하세요.
FeedJpaRepository.countPublicFeedsByUserId는 JPQL(@query)로 구현되어 있어 nativeQuery가 아니므로 필터가 자동 적용됩니다.src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java (2)
13-17: findByCommentId 추가로 필터 적용 보장 OKsoft-delete 대상 단건 조회에 적합합니다. 레포지토리 전반의 findById 사용처도 동일 패턴으로 통일하는 것을 권장합니다.
19-20: 상태 상수의 문자열 하드코딩 대신 enum 상수 사용 권장타입 안정성과 리팩토링 안전성을 위해 JPQL에서 enum 정규명을 사용하세요.
-@Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId = :postId") +@Query("UPDATE CommentJpaEntity c SET c.status = konkuk.thip.common.entity.StatusType.INACTIVE WHERE c.postJpaEntity.postId = :postId")src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java (2)
117-120: N+1(깊이 반복 조회) 비용 주의트리 깊이에 비례해 쿼리 횟수가 증가합니다. 일반 분포에서 허용 가능하나, 최악 케이스 방지를 위해 최대 반복 깊이 상한/총 조회 개수 상한을 두거나, 루트 다건 API에서는 다수 루트를 합산 처리하는 현재 방식(Set)을 우선 적용하는 운영 가이드를 권장합니다. 인덱스(댓글: post_id, parent_id, created_at 복합)도 고려해 주세요.
80-91: 프로젝션의 INACTIVE 플래그 계산 중복자식 조회에선 이미 comment.status.eq(ACTIVE)로 제한되어 있어 INACTIVE 플래그가 항상 false가 됩니다. DTO 스키마 요구 때문이 아니라면 제거로 비용을 줄일 수 있습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (2)
136-138: 주석과 실제 조건 불일치주석에는 “ACTIVE 인 feed …”라고 되어 있으나, 상태 조건은 필터에 위임했습니다. 주석을 최신화해 혼선을 줄여 주세요.
- // ACTIVE 인 feed & (내가 작성한 글 or 다른 유저가 작성한 공개글) & cursorCondition + // (내가 작성한 글 or 다른 유저가 작성한 공개글) & cursorCondition (상태는 Hibernate 필터에 위임)
346-349: 불린 비교 스타일 및 시그니처 정리
feed.isPublic.eq(true)대신feed.isPublic.isTrue()가 일관되고 가독성이 높습니다.- 파라미터
userId가 현재 사용되지 않습니다. 제거하거나, 자기 글 제외 조건을 재도입할지 결정해 주세요.- return feed.bookJpaEntity.isbn.eq(isbn) -// .and(feed.userJpaEntity.userId.ne(userId)) - .and(feed.isPublic.eq(true)); + return feed.bookJpaEntity.isbn.eq(isbn) + .and(feed.isPublic.isTrue());src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java (1)
69-71: save 호출 불필요(영속 컨텍스트 더티 체킹 활용 가능)조회된 엔티티에 대해 필드만 갱신하면 트랜잭션 커밋 시점에 자동 flush됩니다. 불필요한 save 호출 제거로 SQL 1회 절감 가능합니다.
- roomParticipantJpaRepository.save(roomParticipantJpaEntity); + // no-op: JPA dirty checkingsrc/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java (1)
24-24: count 쿼리의 상태 필터 의존성 확인 및 인덱스 제안
- 해당 메서드가 호출되는 트랜잭션에서 필터가 항상 활성인지 확인해 주세요. off 컨텍스트가 있다면 비활성 레코드까지 카운팅됩니다.
- 성능을 위해 (user_id, room_id, created_at) 복합 인덱스를 권장합니다.
src/test/java/konkuk/thip/common/util/StatusFilterTest.java (1)
40-44: 테스트 데이터 정리 순서를 개선하세요.외래 키 제약조건으로 인한 삭제 실패를 방지하기 위해 의존 관계 순서대로 정리하는 것이 좋습니다.
@AfterEach public void tearDown() { savedBookJpaRepository.deleteAllInBatch(); - bookJpaRepository.deleteAllInBatch(); userJpaRepository.deleteAllInBatch(); + bookJpaRepository.deleteAllInBatch(); }
src/main/java/konkuk/thip/book/adapter/out/jpa/SavedBookJpaEntity.java
Outdated
Show resolved
Hide resolved
| /** | ||
| * nullable = true : 최상위 댓글인 경우 null | ||
| */ | ||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "parent_id") | ||
| private CommentJpaEntity parent; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상위 댓글 필터링 시 ObjectNotFound 위험 — 매핑에 NotFound/nullable/optional 명시 필요
Hibernate Filter로 부모가 INACTIVE인 경우 지연로딩 시 부모 조회가 필터에 걸려 예외가 날 수 있습니다. @NotFound(IGNORE)와 명시적 nullable/optional로 방어하세요.
- /**
- * nullable = true : 최상위 댓글인 경우 null
- */
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "parent_id")
+ /**
+ * nullable = true : 최상위 댓글인 경우 null
+ * 부모가 필터로 제외될 수 있으므로 NotFound IGNORE로 방어
+ */
+ @NotFound(action = NotFoundAction.IGNORE)
+ @ManyToOne(fetch = FetchType.LAZY, optional = true)
+ @JoinColumn(name = "parent_id", nullable = true)
private CommentJpaEntity parent;아래 import 추가가 필요합니다.
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java
around lines 54 to 59, the lazy parent relation can throw ObjectNotFound when
the parent is filtered out by a Hibernate filter; to fix this mark the
association as optional/nullable and ignore missing targets: add
@NotFound(NotFoundAction.IGNORE) on the parent field, set @ManyToOne(optional =
true, fetch = FetchType.LAZY) and @JoinColumn(name = "parent_id", nullable =
true), and add the required imports (org.hibernate.annotations.NotFound and
org.hibernate.annotations.NotFoundAction).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 프로젝트는 헥사고날 아키텍처를 따르고 있어서 jpa entity와 도메인 entity를 분리해 사용하고 있으므로, Jpa entity 필드의 지연로딩이 발생하는 유스케이스는 없어.
(매번 jpa entity -> domain entity 로 매핑한 후, 어플리케이션 레어에서는 도메인 엔티티만 바라보고 있음.
추가로 comment domain entity 에서는 parent의 Long Id 값 만을 참조하고 있는데, parent가 없거나 inactive 라면 이 값은 null로 매핑됨)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seongjunnoh, 완전히 맞는 말씀입니다! 헥사고날 아키텍처에서 JPA entity와 domain entity를 명확히 분리하고 계시는군요.
JPA entity가 항상 domain entity로 매핑된 후 어플리케이션 레이어에서는 도메인 엔티티만 사용하시니, 제가 우려했던 지연로딩 시 ObjectNotFound 예외 상황은 발생할 수 없습니다. 특히 comment domain entity에서 parent를 Long ID로만 참조하고 계시니, parent가 inactive이거나 없는 경우에도 단순히 null로 매핑되어 안전하게 처리되겠네요.
아키텍처 패턴을 고려하지 못한 제안이었습니다. 명확한 설명 감사합니다!
✏️ Learnings added
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#287
File: src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java:54-59
Timestamp: 2025-09-01T08:35:56.409Z
Learning: THIP 프로젝트는 헥사고날 아키텍처를 사용하여 JPA entity와 domain entity를 분리한다. JPA entity는 어댑터 계층에서 도메인 entity로 매핑되고, 어플리케이션 계층에서는 도메인 entity만 사용한다. 따라서 JPA entity 필드의 지연로딩이 발생하는 유스케이스는 없다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#287
File: src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java:100-105
Timestamp: 2025-09-01T07:10:50.427Z
Learning: 댓글 시스템에서 '삭제된 부모댓글이라도, 그 하위에 active 인 자식댓글이 존재하면 이를 보여줘야한다'는 비즈니스 요구사항이 있다. 따라서 findAllActiveChildCommentsByCreatedAtAsc 메서드는 부모 댓글의 status와 관계없이 모든 active 상태인 자식 댓글들을 조회하는 것이 올바른 동작이다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#285
File: src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java:1-7
Timestamp: 2025-08-31T05:25:14.835Z
Learning: seongjunnoh는 enum 의존성에 대해 유연한 접근을 선호하며, 도메인→어댑터 레이어 참조와 같은 아키텍처 layering 원칙보다 실용적인 구현을 우선시한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/common/exception/code/ErrorCode.java:210-211
Timestamp: 2025-08-24T09:40:48.595Z
Learning: seongjunnoh는 HTTP 상태 코드 선택에 대해 기술적 근거와 코드베이스 내 일관성을 중요하게 생각하며, 구체적인 사례 분석을 통한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.371Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/TestTokenController.java:0-0
Timestamp: 2025-08-24T09:33:52.982Z
Learning: seongjunnoh는 Spring의 ConditionalOnProperty 동작 원리를 정확히 이해하고 있으며, 보안 이슈에 대해서도 실질적인 위험성을 기준으로 판단하는 실용적 접근을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "user_id") | ||
| @JoinColumn(name = "user_id", nullable = false) | ||
| private UserJpaEntity userJpaEntity; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "comment_id") | ||
| @JoinColumn(name = "comment_id", nullable = false) | ||
| private CommentJpaEntity commentJpaEntity; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
연관관계 null 금지 강화는 👍; JPA/DB 정합성 일치와 필터 우회 이슈 보완 필요
- JPA 측면에서 nullable=false 선언은 좋습니다. flush 시점에서의 일관성을 위해
@ManyToOne(optional = false)도 함께 명시해 주세요. 그렇지 않으면 애플리케이션 레벨에서 null 할당이 허용되어 DB 제약에서만 실패할 수 있습니다. - 중복 좋아요 방지를 위해 (user_id, comment_id) 유니크 제약을 DB/DDL에 추가하는 것을 권장합니다. 애플리케이션 단 검증만으로는 경쟁 상황에서 레이스가 발생할 수 있습니다.
- Hibernate Filter 사용 맥락:
@ManyToOne지연 로딩은 PK 조회(SQL by id)로 이루어져 필터가 적용되지 않습니다. 즉, 활성 필터 하에서도like.getUserJpaEntity()접근 시 INACTIVE 유저/댓글이 로드될 수 있습니다. 조회 쿼리에서 명시적 JOIN 사용 혹은 연관관계에 @where 적용 등 보완이 필요합니다(정책에 맞는 쪽 선택).
다음 스크립트로 마이그레이션 파일 및 연관 엔티티 접근 사용처를 점검해 주세요.
적용 패치:
- @ManyToOne(fetch = FetchType.LAZY)
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private UserJpaEntity userJpaEntity;
- @ManyToOne(fetch = FetchType.LAZY)
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "comment_id", nullable = false)
private CommentJpaEntity commentJpaEntity;추가로 클래스 레벨 유니크 제약(선택):
-@Table(name = "comment_likes")
+@Table(
+ name = "comment_likes",
+ uniqueConstraints = @UniqueConstraint(
+ name = "uk_comment_like_user_comment",
+ columnNames = {"user_id","comment_id"}
+ )
+)🏁 Script executed:
#!/bin/bash
# 1) comment_likes 제약/마이그레이션 존재 여부 확인
rg -n --glob "src/main/resources/**" -i "comment_likes|comment_like" || true
# 2) 연관 접근 사용처 (필터 우회 가능성) 확인
rg -nP "\bget(UserJpaEntity|CommentJpaEntity)\s*\(" -C2Length of output: 34583
CommentLikeJpaEntity 연관관계 및 DB 제약 강화 필요
@ManyToOne(fetch = LAZY)에optional = false명시 (src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java)- DB 마이그레이션/DDL에
(user_id, comment_id)유니크 제약 반영 - Hibernate Filter 우회 방지: 연관관계에
@Where적용 또는 조회 시 명시적 JOIN 사용
수정 예시:
// 클래스 레벨
- @Table(name = "comment_likes")
+ @Table(
+ name = "comment_likes",
+ uniqueConstraints = @UniqueConstraint(
+ name = "uk_comment_like_user_comment",
+ columnNames = {"user_id","comment_id"}
+ )
+ )
// 필드 레벨
- @ManyToOne(fetch = FetchType.LAZY)
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private UserJpaEntity userJpaEntity;
- @ManyToOne(fetch = FetchType.LAZY)
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "comment_id", nullable = false)
private CommentJpaEntity commentJpaEntity;📝 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.
| @ManyToOne(fetch = FetchType.LAZY) | |
| @JoinColumn(name = "user_id") | |
| @JoinColumn(name = "user_id", nullable = false) | |
| private UserJpaEntity userJpaEntity; | |
| @ManyToOne(fetch = FetchType.LAZY) | |
| @JoinColumn(name = "comment_id") | |
| @JoinColumn(name = "comment_id", nullable = false) | |
| private CommentJpaEntity commentJpaEntity; | |
| // at the top of the entity class | |
| @Table( | |
| name = "comment_likes", | |
| uniqueConstraints = @UniqueConstraint( | |
| name = "uk_comment_like_user_comment", | |
| columnNames = {"user_id", "comment_id"} | |
| ) | |
| ) | |
| @Entity | |
| public class CommentLikeJpaEntity { | |
| // ... | |
| @ManyToOne(fetch = FetchType.LAZY, optional = false) | |
| @JoinColumn(name = "user_id", nullable = false) | |
| private UserJpaEntity userJpaEntity; | |
| @ManyToOne(fetch = FetchType.LAZY, optional = false) | |
| @JoinColumn(name = "comment_id", nullable = false) | |
| private CommentJpaEntity commentJpaEntity; | |
| // ... | |
| } |
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java
around lines 21 to 27, the ManyToOne associations need stricter constraints and
protection against Hibernate filter bypass: add optional = false to both
@ManyToOne annotations (in addition to the existing nullable = false on
@JoinColumn), update your DB migration/DDL to add a UNIQUE constraint on
(user_id, comment_id) to prevent duplicate likes, and harden lazy-association
filtering by either applying an appropriate @Where clause on the
association/entity or ensuring all reads use explicit JOINs (and update any
repository/JPQL queries accordingly).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
db unique 제약조건은 아직 고려하지 않고 있어.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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!
| .join(comment.userJpaEntity, commentCreator) | ||
| .where( | ||
| comment.parent.commentId.in(parentIds), // parentIds 하위의 모든 자식 댓글 조회 | ||
| comment.status.eq(StatusType.ACTIVE) // 자식 댓글은 ACTIVE인 것만 조회 | ||
| comment.status.eq(ACTIVE), // 자식 댓글은 ACTIVE인 것만 조회 | ||
| commentCreator.status.eq(ACTIVE) // 자식 댓글 작성자 ACTIVE | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
자식 댓글 조회 시 부모 상태 필터 미적용 — findChildCommentId와 불일치
여기서는 자식 댓글과 작성자만 ACTIVE를 강제하지만, 아래 findChildCommentId에서는 부모도 ACTIVE를 요구합니다. “삭제된 부모 아래 자식 노출 정책”을 일관화해 주세요.
가능한 두 옵션:
- 부모가 INACTIVE면 자식도 제외(현행 findChildCommentId에 맞춤):
- .where(
- comment.parent.commentId.in(parentIds),
- comment.status.eq(ACTIVE),
- commentCreator.status.eq(ACTIVE)
- )
+ .where(
+ comment.parent.commentId.in(parentIds),
+ parentComment.status.eq(ACTIVE),
+ comment.status.eq(ACTIVE),
+ commentCreator.status.eq(ACTIVE)
+ )- 삭제된 부모 아래 자식도 허용(루프들과 일치하도록 단건 조회를 완화):
- parentComment.status.eq(ACTIVE),
comment.status.eq(ACTIVE),의도에 맞는 방향으로 선택 부탁드립니다.
🏁 Script executed:
#!/bin/bash
# findChildCommentId 메서드의 쿼리에서 부모 상태 필터 적용 여부 확인
rg -n "findChildCommentId" -C5 srcLength of output: 3525
🏁 Script executed:
#!/bin/bash
# findChildCommentId 메서드의 부모 상태 필터 적용 여부 확인 (join 및 where 절)
sed -n '200,260p' src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.javaLength of output: 1686
자식 댓글 조회 시 부모 상태(ACTIVE) 필터 누락 — 일관성 유지 필요
findAllActiveChildCommentsByCreatedAtAsc에서는 부모 상태 필터가 빠져 있어, findChildCommentId의 동작과 불일치합니다. 삭제된(비활성) 부모 아래 자식이 노출되지 않도록 아래 중 하나를 적용하세요.
.where(
- comment.parent.commentId.in(parentIds),
+ comment.parent.commentId.in(parentIds),
+ parentComment.status.eq(ACTIVE), // 부모도 ACTIVE인 경우만 조회
comment.status.eq(ACTIVE),
commentCreator.status.eq(ACTIVE)
)📝 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.
| .join(comment.userJpaEntity, commentCreator) | |
| .where( | |
| comment.parent.commentId.in(parentIds), // parentIds 하위의 모든 자식 댓글 조회 | |
| comment.status.eq(StatusType.ACTIVE) // 자식 댓글은 ACTIVE인 것만 조회 | |
| comment.status.eq(ACTIVE), // 자식 댓글은 ACTIVE인 것만 조회 | |
| commentCreator.status.eq(ACTIVE) // 자식 댓글 작성자 ACTIVE | |
| ) | |
| .join(comment.userJpaEntity, commentCreator) | |
| .where( | |
| comment.parent.commentId.in(parentIds), // parentIds 하위의 모든 자식 댓글 조회 | |
| parentComment.status.eq(ACTIVE), // 부모도 ACTIVE인 경우만 조회 | |
| comment.status.eq(ACTIVE), // 자식 댓글은 ACTIVE인 것만 조회 | |
| commentCreator.status.eq(ACTIVE) // 자식 댓글 작성자 ACTIVE | |
| ) |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java
around lines 100 to 105, the query for finding child comments is missing a
filter that ensures the parent comment is ACTIVE, causing inconsistency with
findChildCommentId; update the JPQL/QueryDSL predicate to include a check for
the parent’s status (e.g., comment.parent.status.eq(ACTIVE)) or join the parent
entity and assert its status is ACTIVE so children under deleted/inactive
parents are excluded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
위 메서드는 부모 댓글의 status와 관계없이 모든 active 상태인 자식 댓글들을 조회하는 메서드야.
'삭제된 부모댓글이라도, 그 하위에 active 인 자식댓글이 존재하면 이를 보여줘야한다' 라는 요구사항이 있어.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seongjunnoh 비즈니스 요구사항에 대한 명확한 설명 감사드립니다!
삭제된 부모 댓글 하위의 활성 자식 댓글도 표시해야 한다는 정책을 이해했습니다. 이 경우 현재 구현이 요구사항에 맞게 올바르게 작동하고 있네요. findChildCommentId와 다른 동작을 하는 것도 각각의 용도가 다르기 때문이군요.
향후 리뷰에서 이런 도메인별 정책을 고려하도록 하겠습니다.
✏️ Learnings added
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#287
File: src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java:100-105
Timestamp: 2025-09-01T07:10:50.427Z
Learning: 댓글 시스템에서 '삭제된 부모댓글이라도, 그 하위에 active 인 자식댓글이 존재하면 이를 보여줘야한다'는 비즈니스 요구사항이 있다. 따라서 findAllActiveChildCommentsByCreatedAtAsc 메서드는 부모 댓글의 status와 관계없이 모든 active 상태인 자식 댓글들을 조회하는 것이 올바른 동작이다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#285
File: src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java:1-7
Timestamp: 2025-08-31T05:25:14.835Z
Learning: seongjunnoh는 enum 의존성에 대해 유연한 접근을 선호하며, 도메인→어댑터 레이어 참조와 같은 아키텍처 layering 원칙보다 실용적인 구현을 우선시한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.371Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/common/exception/code/ErrorCode.java:210-211
Timestamp: 2025-08-24T09:40:48.595Z
Learning: seongjunnoh는 HTTP 상태 코드 선택에 대해 기술적 근거와 코드베이스 내 일관성을 중요하게 생각하며, 구체적인 사례 분석을 통한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/TestTokenController.java:0-0
Timestamp: 2025-08-24T09:33:52.982Z
Learning: seongjunnoh는 Spring의 ConditionalOnProperty 동작 원리를 정확히 이해하고 있으며, 보안 이슈에 대해서도 실질적인 위험성을 기준으로 판단하는 실용적 접근을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
| @Transactional(readOnly = true) | ||
| @Unfiltered | ||
| public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
@unfiltered로 자식 댓글까지 비활성 포함되는 부작용 발생
메서드 단위 @unfiltered는 전체 트랜잭션에서 필터를 꺼서, 이름상 ‘findAllActiveChildCommentsOldestFirst’까지 INACTIVE를 포함시킬 위험이 큽니다. 요구사항은 “루트는 삭제 포함 조회, 자식은 ACTIVE만”으로 보입니다.
서비스에서 @unfiltered를 제거하고, “루트 댓글 조회”를 수행하는 어댑터/포트 메서드에만 @unfiltered를 부여해 범위를 축소하세요.
@Override
- @Transactional(readOnly = true)
- @Unfiltered
+ @Transactional(readOnly = true)
public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query) {추가로, 루트 조회용 포트/리포지토리 메서드(예: findLatestRootCommentsWithDeleted)에 @unfiltered를 선언하거나, 어댑터 내부에서 일시적으로 필터 on/off를 제어하는 구현을 분리하는 것을 권장합니다(같은 빈 내 self-invocation 주의).
📝 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.
| @Transactional(readOnly = true) | |
| @Unfiltered | |
| public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query) { | |
| @Override | |
| @Transactional(readOnly = true) | |
| public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query) { | |
| // … | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java
around lines 30-32, remove the class/method-level @Unfiltered so the whole
transaction no longer disables the filter (which currently causes child comments
to include INACTIVE), and instead apply @Unfiltered narrowly to the adapter/port
method that loads root comments (e.g., the repository method like
findLatestRootCommentsWithDeleted or the adapter method that queries roots).
Update the service to call a dedicated port/repository method annotated with
@Unfiltered (or have the adapter temporarily enable/disable the filter around
the root-query) so roots are fetched including deleted while child-comment
queries remain filtered to ACTIVE; if you toggle filters inside the same bean,
avoid self-invocation by moving toggle logic to a separate component or the
repository/adapter implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
댓글 조회 서비스 메서드의 하위 동작에서는 status filter를 끄고, 대신 하위 영속성 메서드에 명시적으로 요구사항에 맞는 status 조건을 붙이는 식으로 구현했으므로 문제될 건 없다고 생각해.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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!
| .where(user.nickname.like(pattern) | ||
| .and(user.userId.ne(userId)) | ||
| .and(user.status.eq(StatusType.ACTIVE))) | ||
| .and(user.userId.ne(userId))) | ||
| .orderBy(priority.desc(), user.nickname.asc()) | ||
| .limit(size) | ||
| .fetch(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
userId null 처리 누락 가능성(ne(null) → IS NOT NULL)
QueryDSL에서 ne(null)은 “IS NOT NULL”로 치환됩니다. 비로그인 검색 등 userId가 null일 수 있다면 불필요한 조건이 되며 옵티마이저에 따라 성능만 저하시킬 수 있습니다. null 시 해당 조건 자체를 생략하세요.
- .where(user.nickname.like(pattern)
- .and(user.userId.ne(userId)))
+ .where(user.nickname.like(pattern)
+ .and(userId != null ? user.userId.ne(userId) : null))📝 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.
| .where(user.nickname.like(pattern) | |
| .and(user.userId.ne(userId)) | |
| .and(user.status.eq(StatusType.ACTIVE))) | |
| .and(user.userId.ne(userId))) | |
| .orderBy(priority.desc(), user.nickname.asc()) | |
| .limit(size) | |
| .fetch(); | |
| .where(user.nickname.like(pattern) | |
| .and(userId != null ? user.userId.ne(userId) : null)) | |
| .orderBy(priority.desc(), user.nickname.asc()) | |
| .limit(size) | |
| .fetch(); |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java
around lines 71 to 75, the QueryDSL predicate unconditionally uses
user.userId.ne(userId) which becomes IS NOT NULL when userId is null; update the
query construction to skip adding the userId != userId predicate when the
provided userId is null (e.g. build predicates conditionally using a
BooleanBuilder or conditional where-call), so that when userId == null the
condition is omitted entirely and only the nickname/priority/limit clauses
remain.
src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java
Show resolved
Hide resolved
| @Transactional(readOnly = true) | ||
| public boolean isNicknameUnique(String nickname) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
DB 제약 검증 필요(전역 유니크 vs 부분 유니크)
현재 스키마/마이그레이션에 닉네임 고유 제약이 무엇인지 확인해 주세요. 서비스 레벨 판단과 DB 제약이 어긋나면 운영 중 충돌이 납니다.
다음 스크립트로 엔티티/마이그레이션 내 제약을 점검해 주세요:
🏁 Script executed:
#!/bin/bash
# 닉네임 관련 유니크 제약/인덱스 탐색
rg -nP --glob '!**/build/**' '(unique|UNIQUE).*(nickname)'
rg -nP --glob '!**/build/**' '@Column\([^)]*nickname[^)]*unique\s*=\s*true'
rg -nP --glob '!**/build/**' 'CREATE\s+UNIQUE\s+INDEX|ADD\s+CONSTRAINT' -C2
# UserJpaEntity 필드 정의 확인
rg -nP --glob '!**/build/**' 'class\s+UserJpaEntity\b' -n -C5
rg -nP --glob '!**/build/**' '\bnickname\b' -n -C3Length of output: 105238
DB 레벨 닉네임 고유 제약 추가 필요
현재 UserJpaEntity.nickname에 @Column(unique=true)가 없고, 별도의 UNIQUE INDEX나 CONSTRAINT도 정의되어 있지 않습니다.
– users.nickname 컬럼에 UNIQUE 제약 또는 인덱스를 추가하는 마이그레이션(SQL/Java 기반)을 반드시 작성하고 적용하세요.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java
around lines 16-17, add a DB-level uniqueness constraint for users.nickname:
update the JPA entity UserJpaEntity to mark the nickname column unique (e.g.,
@Column(unique = true)) and create/apply a migration (SQL or Flyway/Liquibase)
that adds a UNIQUE constraint or UNIQUE INDEX on the users.nickname column;
ensure the migration handles existing duplicate rows (cleanup or fail with a
clear error), and run the migration as part of deployment so the database
enforces nickname uniqueness.
| @AfterEach | ||
| void tearDown() { | ||
| savedBookJpaRepository.deleteAll(); | ||
| savedBookJpaRepository.deleteAllInBatch(); | ||
| feedJpaRepository.deleteAllInBatch(); | ||
| roomParticipantJpaRepository.deleteAllInBatch(); | ||
| roomJpaRepository.deleteAll(); | ||
| bookJpaRepository.deleteAll(); | ||
| userJpaRepository.deleteAll(); | ||
| roomJpaRepository.deleteAllInBatch(); | ||
| bookJpaRepository.deleteAllInBatch(); | ||
| userJpaRepository.deleteAllInBatch(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
테스트 정리 성능/정확성 개선: deleteAllInBatch 일관 사용 권장
tearDown에서 deleteAllInBatch로 전환은 좋습니다. 동일 맥락에서 테스트 본문 내의 삭제 호출도 일관되게 배치 삭제를 사용하면
- 성능: 개별 로드/삭제 대비 DB round-trip 감소
- 정확성: 활성 필터가 켜져 있을 경우 deleteAll()이 ACTIVE만 삭제하는 반면, 배치 삭제는 INACTIVE 잔존을 방지
효과가 있습니다.
다음 호출들도 배치 삭제로 통일을 고려해 주세요(참고 라인):
- Line 148: roomJpaRepository.deleteAll() → deleteAllInBatch()
- Line 188: savedBookJpaRepository.deleteAll() → deleteAllInBatch()
🤖 Prompt for AI Agents
In src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java
around lines 114-121 and referenced lines 148 and 188, the test cleanup uses
mixed deleteAll and deleteAllInBatch; update the calls at line 148
(roomJpaRepository.deleteAll()) and line 188
(savedBookJpaRepository.deleteAll()) to use deleteAllInBatch() so all
teardown/cleanup uses batch deletes consistently; ensure the method names match
the repository API and run the tests to confirm no compilation or behavioral
issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굿굿
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@buzz0331 감사합니다! 일관된 배치 삭제 사용으로 테스트 성능과 정확성이 개선될 것입니다. 🚀
| VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(vote.getPostId()).orElse(null); | ||
| assertThat(voteJpaEntity.getStatus()).isEqualTo(INACTIVE); | ||
|
|
||
| // 2) 댓글 삭제 soft delete | ||
| assertThat(commentJpaRepository.findByCommentIdAndStatus(comment.getCommentId(),INACTIVE)).isPresent(); | ||
| CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(comment.getCommentId()).orElse(null); | ||
| assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
orElse(null)로 인한 NPE 위험 — 명시적 실패로 교체하세요
삭제 후 존재 검증 단계에서 Optional을 null로 풀어 NPE가 발생할 수 있습니다. 테스트 실패 원인을 명확히 하도록 orElseThrow(AssertionError)로 바꾸는 편이 안전합니다.
- VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(vote.getPostId()).orElse(null);
- assertThat(voteJpaEntity.getStatus()).isEqualTo(INACTIVE);
+ VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(vote.getPostId())
+ .orElseThrow(() -> new AssertionError("Soft delete 후 vote 레코드가 존재해야 합니다."));
+ assertThat(voteJpaEntity.getStatus()).isEqualTo(INACTIVE);
- CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(comment.getCommentId()).orElse(null);
- assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE);
+ CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(comment.getCommentId())
+ .orElseThrow(() -> new AssertionError("Soft delete 후 comment 레코드가 존재해야 합니다."));
+ assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE);📝 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.
| VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(vote.getPostId()).orElse(null); | |
| assertThat(voteJpaEntity.getStatus()).isEqualTo(INACTIVE); | |
| // 2) 댓글 삭제 soft delete | |
| assertThat(commentJpaRepository.findByCommentIdAndStatus(comment.getCommentId(),INACTIVE)).isPresent(); | |
| CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(comment.getCommentId()).orElse(null); | |
| assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); | |
| VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(vote.getPostId()) | |
| .orElseThrow(() -> new AssertionError("Soft delete 후 vote 레코드가 존재해야 합니다.")); | |
| assertThat(voteJpaEntity.getStatus()).isEqualTo(INACTIVE); | |
| // 2) 댓글 삭제 soft delete | |
| CommentJpaEntity commentJpaEntity = commentJpaRepository.findById(comment.getCommentId()) | |
| .orElseThrow(() -> new AssertionError("Soft delete 후 comment 레코드가 존재해야 합니다.")); | |
| assertThat(commentJpaEntity.getStatus()).isEqualTo(INACTIVE); |
buzz0331
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
어쩌다보니 대대적인 공사가 이루어졌네요ㅠㅠ 말로 들었을 땐 긴가민가 했는데 코드로 확인해보니 확실히 편리할 것 같습니다!! 정말 고생하셨습니다~~ 💯
src/main/java/konkuk/thip/book/adapter/out/jpa/SavedBookJpaEntity.java
Outdated
Show resolved
Hide resolved
| @@ -1,4 +1,4 @@ | |||
| package konkuk.thip.common.annotation; | |||
| package konkuk.thip.common.annotation.application; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굿 패키지 분리 좋습니다
| private static final String PCUT_INCLUDE_INACTIVE = | ||
| "@annotation(" + ANN_INCLUDE_INACTIVE + ")"; | ||
| private static final String PCUT_UNFILTERED = | ||
| "@annotation(" + ANN_UNFILTERED + ")"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
기본 필터 (PCUT_TX_DEFAULT)의 경우 @Transactional을 포함하고 있어서 트랜잭션 경계 내에서만 동작할 것 같은데 @IncludeInactive와 @Unfiltered는 @Transactional 조건이 없어서 트랜잭션 밖에서도 실행될 수 있을 것 같은데 혹시 의도하신건가요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
누락된 부분입니다! @IncludeInactive와 @unfiltered 도 트랜잭션 내부에서 실행되도록 코드 수정했습니다!
| @FilterDef( | ||
| name = "statusFilter", | ||
| parameters = @ParamDef(name = "statuses", type = String.class) | ||
| ) | ||
| @Filter( | ||
| name = "statusFilter", | ||
| condition = "status in (:statuses)" | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오호 하이버네이트에서 이런 어노테이션도 제공하는군요 굿
src/main/java/konkuk/thip/feed/adapter/out/jpa/SavedFeedJpaEntity.java
Outdated
Show resolved
Hide resolved
| private void enableFilterWith(Session s, List<String> statuses) { | ||
| s.enableFilter(FILTER_NAME).setParameterList(PARAM_STATUSES, statuses); | ||
| log.debug("statusFilter -> ENABLED [statuses={}]", statuses); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
status 조건을 리스트로 받아서 in 절로 where 조건을 구성하셨군요! 굿굿 확장성 면에서 좋을 것 같습니다
|
|
||
| private static final int MAX_RECENT_SEARCHES = 5; | ||
|
|
||
| @Override |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?? 이건 머죠? 오버라이드가 안되어 있었나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵넵 어노테이션이 명시되어 있지 않았어서 추가했습니다! 근데 없어도 오버라이드 되는것으로 인식되는거 같더라고요
src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java
Show resolved
Hide resolved
| @AfterEach | ||
| void tearDown() { | ||
| savedBookJpaRepository.deleteAll(); | ||
| savedBookJpaRepository.deleteAllInBatch(); | ||
| feedJpaRepository.deleteAllInBatch(); | ||
| roomParticipantJpaRepository.deleteAllInBatch(); | ||
| roomJpaRepository.deleteAll(); | ||
| bookJpaRepository.deleteAll(); | ||
| userJpaRepository.deleteAll(); | ||
| roomJpaRepository.deleteAllInBatch(); | ||
| bookJpaRepository.deleteAllInBatch(); | ||
| userJpaRepository.deleteAllInBatch(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굿굿
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굿굿 좋습니다! Unfiltered 어노테이션에 대한 테스트도 간단하게나마 추가되면 좋을 것 같습니다!
hd0rable
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
너무 수고하셨습니다ㅠㅠ 파일 변경수 ㅎㄷㄷ하네요,,👍🏻👍🏻
| @Test | ||
| @DisplayName("spring data jpa의 기본 findById 메서드는 PK를 기준으로만 조회하므로 status 필터링이 적용되지 않는다.") | ||
| @Transactional // filter를 활성화 하기 위한 트랜잭션 어노테이션 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
꼼꼼 테스트 코드 너무좋네요 굿!!
| /** | ||
| * nullable = true : 최상위 댓글인 경우 null | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
| @AutoConfigureMockMvc(addFilters = false) | ||
| @ActiveProfiles("test") | ||
| @DisplayName("[통합] 방 상세보기 api 통합 테스트") | ||
| @DisplayName("[통합] 책 상세보기 api 통합 테스트") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
헣 👍🏻 👍🏻
- filter는 스프링 빈이 트랜잭션을 시작할때 AOP가 이를 가로채서 세션을 획득함으로써 활성화 된다 - 하지만 스프링 테스트 메서드는 스프링 빈이 아니므로, TestConfig.TestService를 스프링 빈으로 만듦으로써 filter 활성화 테스트를 진행하였다 - 삭제된 주석 내용처럼 테스트 메서드에 트랜잭션을 붙여서 filter가 활성화 된 것이 아니라, 스프링 빈으로 등록한 TestService의 메서드를 호출함으로써 filter가 활성화 된 것 이다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (16)
src/test/java/konkuk/thip/common/persistence/JpaRepositoryMethodTest.java (8)
42-56: ID 생성 전략 명시로 테스트 안정성 향상 제안H2/로컬 환경에 따라 @GeneratedValue 기본 전략(AUTO→IDENTITY/SEQUENCE)이 달라질 수 있어, persist 시점의 INSERT 여부가 바뀔 수 있습니다. 테스트 안정성을 위해 전략을 명시하세요.
아래처럼 GenerationType.IDENTITY를 명시하면 주석/의도와 일관성이 좋아집니다.
import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; @@ - @GeneratedValue + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long userId;
58-60: private 중첩 Repository의 접근성 고려현재 private interface여도 JDK 동적 프록시가 동일 패키지에 프록시 클래스를 정의해 동작하긴 합니다. 다만 JDK 17/21, JPMS, AOT 환경 등에서 접근성 이슈 가능성이 있으니 public으로 두는 편이 더 안전합니다.
- private interface TestUserRepository extends JpaRepository<TestUser, Long> { + public interface TestUserRepository extends JpaRepository<TestUser, Long> { Optional<TestUser> findByUserId(Long userId); }
62-79: 1차 캐시 검증에 한 줄 보강 (가독성)동일성(assertSame) 외에 영속성 컨텍스트 포함 여부도 함께 표현하면 의도가 더 분명해집니다.
//then - assertSame(testUser, found); // 동일 인스턴스임을 확인 -> 1차 캐시에 저장된 엔티티를 조회했으므로 + assertSame(testUser, found); // 동일 인스턴스 + assertThat(em.contains(found)).isTrue(); // 1차 캐시 포함 확인
81-104: auto flush 발생을 결과로 증명하도록 보강현재 설명은 로그 의존입니다. 쿼리 전 flush가 실제로 반영되었는지 em.clear() 후 재조회로 검증하면 테스트가 자기완결적입니다.
//then - assertSame(testUser, found); // 동일 인스턴스임을 확인 -> DB select 쿼리가 나가지만, 동일성 해석을 거쳐 1차 캐시의 인스턴스를 반환 + assertSame(testUser, found); // 동일 인스턴스 (Identity Resolution) + + // auto flush로 더티 체인지가 DB에 반영되었는지 확인 + em.clear(); + TestUser reloaded = em.find(TestUser.class, id); + assertThat(reloaded.getNickname()).isEqualTo("김희용");추가로, auto flush의 “쿼리 결과 영향”을 더 직접적으로 드러내려면 finder를 userId 대신 nickname 기반으로 하나 더 두는 것도 방법입니다(선택).
106-118: DisplayName/메서드명 표현을 Optional.empty 기준으로 정교화리포지토리 API는 Optional을 반환하므로 “null” 대신 “Optional.empty()”가 더 정확합니다. 메서드명도 일치시키면 가독성이 좋아집니다.
- @DisplayName("영속성 컨텍스트 내부에서 remove 처리된 엔티티를 findById 메서드로 조회할 경우, null을 반환한다.") - void findById_returns_null_if_entity_removed_in_first_level_cache() throws Exception { + @DisplayName("영속성 컨텍스트에서 remove된 엔티티는 findById 시 Optional.empty()를 반환한다.") + void findById_returns_empty_optional_if_entity_removed_in_first_level_cache() throws Exception {
64-64: 테스트 시그니처의 throws Exception 제거예외를 던질 필요가 없어 보입니다. 불필요한 throws는 제거해도 됩니다.
- void findById_use_first_level_cache_without_flush_or_query() throws Exception { + void findById_use_first_level_cache_without_flush_or_query() { @@ - void derivedQuery_auto_flush_if_needed_then_select_and_identity_resolution() throws Exception { + void derivedQuery_auto_flush_if_needed_then_select_and_identity_resolution() { @@ - void findById_returns_empty_optional_if_entity_removed_in_first_level_cache() throws Exception { + void findById_returns_empty_optional_if_entity_removed_in_first_level_cache() {Also applies to: 83-83, 108-108
67-67: 주석 정확도 개선(ID 전략 차이 반영)“A 아직 DB에 반영되지 않음, flush X”는 IDENTITY 전략에선 persist 시 insert가 발생할 수 있어 오해 소지가 있습니다. 테스트 의도(1차 캐시/auto flush)를 기준으로 문구를 다듬는 걸 제안합니다.
- em.persist(testUser); // 1차 캐시에 등록 (아직 DB에 반영되지 않음, flush X) + em.persist(testUser); // 1차 캐시에 등록 (ID 전략에 따라 persist 시 INSERT가 즉시 발생할 수 있음; 본 테스트의 핵심은 1차 캐시/auto flush 동작)(두 곳 동일 수정)
Also applies to: 86-86
21-22: 단일 Assertion 스타일로 통일(선택)AssertJ로 통일하면 가독성이 좋아집니다. 선택 사항입니다.
-import static org.junit.jupiter.api.Assertions.assertSame; @@ - assertSame(testUser, found); + assertThat(found).isSameAs(testUser); @@ - assertSame(testUser, found); + assertThat(found).isSameAs(testUser);Also applies to: 74-74, 100-100
src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java (8)
18-18: 불필요한 import 제거
org.springframework.transaction.annotation.Transactional는 사용되지 않습니다. 정리해 주세요.적용 diff:
-import org.springframework.transaction.annotation.Transactional;
47-52: 대량 저장 성능: saveAll로 라운드트립 축소반복
save()대신saveAll()을 사용하면 트랜잭션/DB 라운드트립을 줄여 테스트 시간을 단축할 수 있습니다.적용 diff:
- for (int i = 1; i <= count; i++) { - UserJpaEntity user = TestEntityFactory.createUser(Alias.WRITER, "activeUser" + i); - userJpaRepository.save(user); - } + List<UserJpaEntity> users = java.util.stream.IntStream.rangeClosed(1, count) + .mapToObj(i -> TestEntityFactory.createUser(Alias.WRITER, "activeUser" + i)) + .toList(); + userJpaRepository.saveAll(users);
54-64: 중복 SQL 추출 + 업데이트 성공 여부 검증직접 SQL 업데이트가 여러 곳에서 반복됩니다. 헬퍼로 추출하고 업데이트 건수 검증을 추가하면 테스트 신뢰도가 올라갑니다. (현재 클래스/메서드 레벨 @transactional이 없어서 1차 캐시 정합성 이슈는 없지만, 향후 추가 시를 대비해 안전망을 갖추는 효과도 있습니다.)
적용 diff(헬퍼 사용으로 치환):
- for (int i = 1; i <= count; i++) { - UserJpaEntity user = TestEntityFactory.createUser(Alias.WRITER, "inactiveUser" + i); - userJpaRepository.save(user); - - jdbcTemplate.update( - "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", - user.getUserId() - ); - } + for (int i = 1; i <= count; i++) { + UserJpaEntity user = TestEntityFactory.createUser(Alias.WRITER, "inactiveUser" + i); + userJpaRepository.save(user); + markInactive(user.getUserId()); + }헬퍼 추가(클래스 내 임의 위치):
private void markInactive(Long userId) { int updated = jdbcTemplate.update( "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", userId ); assertThat(updated).isEqualTo(1); }참고: 향후 이 테스트 클래스 또는 메서드에 @transactional이 붙게 될 경우,
markInactive()내에서@PersistenceContext EntityManager em; em.clear();를 호출하도록 보완하면 1차 캐시와 DB 간 상태 불일치로 인한 플래키함을 예방할 수 있습니다.
72-76: 중복 제거: 상태 업데이트 헬퍼 사용직접 SQL 호출 대신 위에서 제안한
markInactive()를 사용하면 중복이 줄고 업데이트 성공 여부도 검증됩니다.적용 diff:
- jdbcTemplate.update( - "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", - inactiveUser.getUserId() - ); + markInactive(inactiveUser.getUserId());
221-223: 중복 제거: 상태 업데이트 헬퍼 사용여기도 동일하게
markInactive()재사용을 권장합니다.적용 diff:
- jdbcTemplate.update( - "UPDATE users SET status = 'INACTIVE' WHERE user_id = ?", - inactiveUser.getUserId() - ); + markInactive(inactiveUser.getUserId());
66-85: PK 조회가 필터를 우회함을 명확히 입증 — 보강 아이디어현재 검증은 충분합니다. 추가로
EntityManager.find()도 필터를 적용하지 않는다는 점을 직접 보여주는 테스트가 있으면 회귀 시 원인 파악이 더 빨라집니다.추가 예시:
@PersistenceContext private EntityManager em; @Test void entity_manager_find_bypasses_filter() { UserJpaEntity active = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "activeUser")); UserJpaEntity inactive = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "inactiveUser")); markInactive(inactive.getUserId()); // EM의 PK 조회는 필터 우회 UserJpaEntity foundActive = em.find(UserJpaEntity.class, active.getUserId()); UserJpaEntity foundInactive = em.find(UserJpaEntity.class, inactive.getUserId()); assertThat(foundActive).isNotNull(); assertThat(foundInactive).isNotNull(); }
106-122: 유사 패턴 테스트의 중복 축소 가능JPA/JPQL/QueryDSL 케이스가 동일한 준비/검증 패턴을 공유합니다. 파라미터라이즈드 테스트로 중복을 줄이면 유지보수가 쉬워집니다. (아래 테스트들에도 동일 적용 가능)
간단 스케치:
@ParameterizedTest @MethodSource("providers") void find_active_only(Supplier<List<UserJpaEntity>> finder) { saveActiveUser(3); saveInactiveUser(2); assertThat(finder.get()).extracting(UserJpaEntity::getNickname) .containsExactlyInAnyOrder("activeUser1","activeUser2","activeUser3"); } static Stream<Supplier<List<UserJpaEntity>>> providers() { return Stream.of( () -> testUserQueryService.findAllActiveOnly(), () -> testUserJpqlService.findAllByJpql(), () -> testUserQuerydslService.findAllByQuerydsl() ); }
214-237: 조인 필터 전파: 루트가 SavedBook인 케이스도 추가 검증 권장현재는 루트=User 기준 검증입니다. 과거 러닝(AttendanceCheck 카운트 쿼리에서 status=ACTIVE 필요)과 유사하게, 루트=SavedBook → JOIN User에서도 필터가 의도대로 전파되는지 별도 테스트를 추가하면 안전합니다.
예시:
@Test @DisplayName("[join 추가 테스트] 루트=SavedBook + User JOIN 시 기본은 ACTIVE만, @IncludeInactive 시 모두 집계") void join_filter_propagation_on_savedBook_root() { var active = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "activeUser")); var inactive = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "inactiveUser")); markInactive(inactive.getUserId()); var book = bookJpaRepository.save(TestEntityFactory.createBook()); savedBookJpaRepository.save(TestEntityFactory.createSavedBook(active, book)); savedBookJpaRepository.save(TestEntityFactory.createSavedBook(inactive, book)); long defCount = testUserQuerydslService.countSaversByBookFromSavedBookRoot(book.getBookId()); long incCount = testUserQuerydslService.countSaversByBookFromSavedBookRootIncludingInactive(book.getBookId()); assertThat(defCount).isEqualTo(1); assertThat(incCount).isEqualTo(2); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/test/java/konkuk/thip/common/persistence/JpaRepositoryMethodTest.java(1 hunks)src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java:5-5
Timestamp: 2025-08-14T07:19:27.310Z
Learning: AttendanceCheck 엔티티는 BaseJpaEntity를 상속받아 status 필드(StatusType.ACTIVE/INACTIVE/EXPIRED)로 소프트 딜리트를 구현하므로, 카운트 쿼리에서 status = 'ACTIVE' 조건을 포함해야 삭제된 레코드를 제외할 수 있음.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#285
File: src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java:1-7
Timestamp: 2025-08-31T05:25:14.835Z
Learning: seongjunnoh는 enum 의존성에 대해 유연한 접근을 선호하며, 도메인→어댑터 레이어 참조와 같은 아키텍처 layering 원칙보다 실용적인 구현을 우선시한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.371Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/common/exception/code/ErrorCode.java:210-211
Timestamp: 2025-08-24T09:40:48.595Z
Learning: seongjunnoh는 HTTP 상태 코드 선택에 대해 기술적 근거와 코드베이스 내 일관성을 중요하게 생각하며, 구체적인 사례 분석을 통한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/TestTokenController.java:0-0
Timestamp: 2025-08-24T09:33:52.982Z
Learning: seongjunnoh는 Spring의 ConditionalOnProperty 동작 원리를 정확히 이해하고 있으며, 보안 이슈에 대해서도 실질적인 위험성을 기준으로 판단하는 실용적 접근을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
📚 Learning: 2025-08-14T07:19:27.310Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java:5-5
Timestamp: 2025-08-14T07:19:27.310Z
Learning: AttendanceCheck 엔티티는 BaseJpaEntity를 상속받아 status 필드(StatusType.ACTIVE/INACTIVE/EXPIRED)로 소프트 딜리트를 구현하므로, 카운트 쿼리에서 status = 'ACTIVE' 조건을 포함해야 삭제된 레코드를 제외할 수 있음.
Applied to files:
src/test/java/konkuk/thip/common/persistence/StatusFilterTest.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/test/java/konkuk/thip/common/persistence/JpaRepositoryMethodTest.java
🧬 Code graph analysis (1)
src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(30-394)
🔇 Additional comments (4)
src/test/java/konkuk/thip/common/persistence/JpaRepositoryMethodTest.java (1)
24-31: JPA 슬라이스 설정 구성 적합@DataJpaTest + considerNestedRepositories + basePackageClasses 조합으로 테스트 격리가 잘 되어 있습니다. 상태 필터/Aspect 등 메인 빈 유입 가능성도 낮아 목적에 부합합니다.
src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java (3)
25-37: 테스트 스코프 선정 적절 (AOP + 다양한 접근 경로 커버)@SpringBootTest로 AOP까지 포함해 필터 기본동작/토글을 end-to-end로 검증하는 구성이 좋습니다. Repository/JPQL/QueryDSL/조인 케이스까지 폭넓게 커버하고 있어 회귀 위험을 낮춥니다.
40-45: 데이터 정리 순서와 방식 적절FK 제약을 고려한 삭제 순서(saver → book → user)와
deleteAllInBatch()선택이 적절합니다. 서비스 레이어를 거치지 않으므로(=필터 AOP 비적용) 잔존 레코드가 남을 우려도 낮습니다.
86-105: 커스텀 ID 조회에 필터 적용 검증 적절PK 기반 기본 조회의 한계를 보완하는 커스텀 메서드 경로에 필터가 적용됨을 명확히 보여줍니다. 변경 의도와 테스트가 일치합니다.
- hibernate session 획득 시 트랜잭션 활성 여부 가드 추가 - @IncludeInactive, @unfiltered 포인트컷을 트랜잭션 범위 내에서만 동작하도록 제한
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (1)
366-372: ISBN별 모집중 방: 공개방 필터 누락 가능성탐색 성격의 API라면 비공개 방 노출을 방지해야 합니다. 또한 now()를 지역 변수로 고정하면 경계시각 변화에 안정적입니다.
- DateExpression<LocalDate> cursorExpr = room.startDate; // 커서 비교는 startDate(= 모집 마감일 - 1일) - BooleanExpression baseCondition = room.bookJpaEntity.isbn.eq(isbn) - .and(room.startDate.after(LocalDate.now())); // 모집 마감 시각 > 현재 시각 + final LocalDate today = LocalDate.now(); + DateExpression<LocalDate> cursorExpr = room.startDate; // 커서 비교는 startDate(= 모집 마감일 - 1일) + BooleanExpression baseCondition = room.bookJpaEntity.isbn.eq(isbn) + .and(room.startDate.after(today)) // 모집 마감 시각 > 현재 시각 + .and(room.isPublic.isTrue()); // 공개 방만
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java (1)
29-31: 이전 피드백 반영 확인—잘 정리되었습니다@manytoone(optional=false) + nullable=false로 런타임에서 즉시 검증되도록 맞춘 점 좋습니다. 이전 코멘트와 동일한 취지의 수정으로 보이며, 데이터 마이그레이션(NULL 존재 여부)만 최종 확인해 주세요.
🧹 Nitpick comments (8)
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)
30-31: 클라이언트 메시지에서 프레임워크 어노테이션 노출 최소화 제안.외부 응답 메시지에 '@transactional' 표기는 구현 세부 노출입니다. 대외 메시지는 중립 문구로, 내부 로깅에서만 상세 표현 권장.
- PERSISTENCE_TRANSACTION_REQUIRED(HttpStatus.INTERNAL_SERVER_ERROR, 50110, "@Transactional 컨텍스트가 필요합니다. 트랜잭션 범위 내에서만 사용할 수 있습니다."), + PERSISTENCE_TRANSACTION_REQUIRED(HttpStatus.INTERNAL_SERVER_ERROR, 50110, "트랜잭션 컨텍스트가 필요합니다. 트랜잭션 범위 내에서만 호출할 수 있습니다."),src/main/java/konkuk/thip/book/adapter/out/jpa/SavedBookJpaEntity.java (2)
21-23: 연관관계 필수화 OK + 중복 저장 방지(무결성) 제안optional=false 적용은 적절합니다. 추후 동일 유저가 동일 책을 중복 저장하는 것을 DB 레벨에서 막으려면 (user_id, book_id) 유니크 제약을 고려해 주세요(상태 컬럼과의 정책에 따라 전역/부분 고유 여부 결정).
Also applies to: 25-27
4-4: 미사용 import 정리 혹은 의도 반영NotNull import가 현재 미사용입니다. 사용 계획이 없다면 제거하고, Bean Validation을 도입할 의도라면 연관 필드에 명시적으로 부여해 일관성을 맞춰 주세요.
-import jakarta.validation.constraints.NotNull;src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java (1)
20-22: 팔로잉 관계 필수화 OK + 유니크/자기 팔로우 방지 제안연관 필수화는 적절합니다. 추가로
- 동일 (user_id, following_user_id) 중복 방지를 위한 유니크 제약을 권장합니다.
- 자기 자신 팔로우 방지는 애플리케이션/DB(체크 제약 가능 시)에서 명시적으로 막아 주세요.
DB 타입에 따라 부분 유니크(STATUS=ACTIVE만) 필요 여부를 정책적으로 결정해 주세요.
Also applies to: 24-26
src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java (1)
54-56: 주석과 매핑 속성의 명시적 일치 제안
주석대로 null 허용 의도를 드러내기 위해 optional=true, nullable=true를 명시하면 가독성이 좋아집니다(기본값이긴 하나 의도 고정에 유용).아래와 같이 명시를 고려해 주세요:
@ManyToOne(fetch = FetchType.LAZY, optional = true) @JoinColumn(name = "parent_id", nullable = true)src/main/java/konkuk/thip/post/adapter/out/jpa/PostLikeJpaEntity.java (1)
24-26: 중복 좋아요 방지 유니크 제약 확인/추가 제안
(post_id, user_id) 유니크 제약이 없으면 중복 좋아요가 저장될 수 있습니다. DB 제약 존재 여부 확인 후 미존재 시 추가를 권장합니다.클래스 레벨에 유니크 제약을 명시하는 방안:
@Entity @Table( name = "post_likes", uniqueConstraints = @UniqueConstraint(name = "uk_post_likes_post_user", columnNames = {"post_id","user_id"}) ) public class PostLikeJpaEntity extends BaseJpaEntity { ... }src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (2)
253-257: “내가 참여한 모집중 방”에서는 isPublic 제외 의도 명시 + 중복 조건 통일 제안여기는 내 방 목록이므로 비공개 포함이 맞다면 의도를 주석으로 남겨 혼선을 줄이세요. 또한 모집중 조건을 별도 프라이빗 메서드로 추출해 재사용하면 중복을 줄일 수 있습니다.
- BooleanExpression base = participant.userJpaEntity.userId.eq(userId) - .and(room.startDate.after(today)); // 유저가 참여한 방 && 모집중인 방 + BooleanExpression base = participant.userJpaEntity.userId.eq(userId) + .and(room.startDate.after(today)); // 유저가 참여한 방 && 모집중인 방 (의도적으로 isPublic 필터 제외)
396-401: userId null 가정 명시 + 필터 OFF 컨텍스트 의미 변화 주의
- userId가 null이면 not exists 판단이 왜곡될 수 있습니다. 계약을 명시하거나 방어 코드를 추가하세요.
- 관리자/테스트에서 Hibernate 필터를 끄면 INACTIVE 참여도 “참여한 방”으로 간주되어 추천에서 제외됩니다. 해당 메서드는 필터 ON 전제를 문서화하는 것이 안전합니다.
private BooleanExpression findDeadlinePopularRoomCondition(Category category, Long userId) { + Objects.requireNonNull(userId, "userId must not be null"); return room.category.eq(category) .and(room.startDate.after(LocalDate.now())) // 모집 마감 시각 > 현재 시각 .and(room.isPublic.isTrue()) // 공개 방만 조회 .and(userJoinedRoom(userId).not()); // 유저가 참여하지 않은 방만 조회 }파일 상단 import 추가:
+import java.util.Objects;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (22)
build.gradle(2 hunks)src/main/java/konkuk/thip/book/adapter/out/jpa/SavedBookJpaEntity.java(2 hunks)src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java(0 hunks)src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java(1 hunks)src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java(2 hunks)src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java(1 hunks)src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java(1 hunks)src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/jpa/SavedFeedJpaEntity.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/jpa/PostLikeJpaEntity.java(1 hunks)src/main/java/konkuk/thip/recentSearch/adapter/out/jpa/RecentSearchJpaEntity.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java(7 hunks)src/main/java/konkuk/thip/roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java(1 hunks)src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java(1 hunks)src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteParticipantJpaEntity.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java(1 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java
✅ Files skipped from review due to trivial changes (2)
- src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java
- src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java
🚧 Files skipped from review as they are similar to previous changes (9)
- src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java
- src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java
- build.gradle
- src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java
- src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentLikeJpaEntity.java
- src/main/java/konkuk/thip/room/adapter/out/jpa/RoomParticipantJpaEntity.java
- src/main/java/konkuk/thip/roompost/adapter/out/jpa/AttendanceCheckJpaEntity.java
- src/main/java/konkuk/thip/feed/adapter/out/jpa/SavedFeedJpaEntity.java
- src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteParticipantJpaEntity.java
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java:5-5
Timestamp: 2025-08-14T07:19:27.310Z
Learning: AttendanceCheck 엔티티는 BaseJpaEntity를 상속받아 status 필드(StatusType.ACTIVE/INACTIVE/EXPIRED)로 소프트 딜리트를 구현하므로, 카운트 쿼리에서 status = 'ACTIVE' 조건을 포함해야 삭제된 레코드를 제외할 수 있음.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#285
File: src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java:1-7
Timestamp: 2025-08-31T05:25:14.835Z
Learning: seongjunnoh는 enum 의존성에 대해 유연한 접근을 선호하며, 도메인→어댑터 레이어 참조와 같은 아키텍처 layering 원칙보다 실용적인 구현을 우선시한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.371Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/common/exception/code/ErrorCode.java:210-211
Timestamp: 2025-08-24T09:40:48.595Z
Learning: seongjunnoh는 HTTP 상태 코드 선택에 대해 기술적 근거와 코드베이스 내 일관성을 중요하게 생각하며, 구체적인 사례 분석을 통한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/TestTokenController.java:0-0
Timestamp: 2025-08-24T09:33:52.982Z
Learning: seongjunnoh는 Spring의 ConditionalOnProperty 동작 원리를 정확히 이해하고 있으며, 보안 이슈에 대해서도 실질적인 위험성을 기준으로 판단하는 실용적 접근을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
📚 Learning: 2025-08-20T08:01:11.243Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#266
File: src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java:35-35
Timestamp: 2025-08-20T08:01:11.243Z
Learning: seongjunnoh는 DB 제약조건(nullable = false)이 이미 보장하는 상황에서 DTO 생성자의 추가적인 null 체크를 불필요한 방어코드로 보며, 실용적인 구현을 선호한다. 기술적 완벽성보다 실제 도메인 모델의 제약조건을 신뢰하는 접근을 취한다.
Applied to files:
src/main/java/konkuk/thip/book/adapter/out/jpa/SavedBookJpaEntity.java
📚 Learning: 2025-09-01T08:35:56.409Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#287
File: src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java:54-59
Timestamp: 2025-09-01T08:35:56.409Z
Learning: THIP 프로젝트는 헥사고날 아키텍처를 사용하여 JPA entity와 domain entity를 분리한다. JPA entity는 어댑터 계층에서 도메인 entity로 매핑되고, 어플리케이션 계층에서는 도메인 entity만 사용한다. 따라서 JPA entity 필드의 지연로딩이 발생하는 유스케이스는 없다.
Applied to files:
src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.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 (12)
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (2)
30-31: 에러 코드 추가 적절 (범위/이름/위치 모두 일관).
- 50xxx(인프라/서버) 구간과 정렬 유지, 이름도 도메인 명확.
- 상태 필터 AOP에서 트랜잭션 요구 상황을 표현하기에 적합합니다.
30-31: 검증 완료: 50110 중복 없음, StatusFilterAspect에서 정상 사용 중이며 InvalidStateException → BusinessException 핸들러로 매핑되어 있어 추가 조치 불필요합니다.src/main/java/konkuk/thip/notification/adapter/out/jpa/NotificationJpaEntity.java (1)
29-31: optional=false 적용 LGTM + 필터 상호작용 확인 필요JPA 제약과 DB 제약 일치시킨 점 좋습니다. 다만 Hibernate Filter가 기본 활성화되는 구조에서, 연관 대상(User)이 INACTIVE일 경우 LAZY 로딩 시 조회 0건 → EntityNotFoundException 가능성이 있습니다. 알림 조회 시 사용자 연관을 접근하는 경로에서 기대 동작(예: 숨김/예외/관리자 모드에서만 노출)이 테스트로 보장되는지 확인 부탁드립니다.
src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java (1)
58-60: optional=false 적용으로 조인 의미 명확화—OKFK가 nullable=false이므로 optional=false 추가로 일관성 및 쿼리 최적화(INNER JOIN 유도)가 기대됩니다. 마이그레이션 시 book_id IS NULL 데이터가 없는지 한 번만 점검해 주세요.
src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java (3)
42-45: post 연관 필수화(optional=false) 일관성 OK — DB 제약/인덱스 확인 바랍니다
nullable=false와의 일치 좋습니다. 마이그레이션에 NOT NULL 및 FK 인덱스(조회/조인 빈도 높음 시) 반영되었는지 확인 부탁드립니다.
50-52: user 연관 필수화 — 상태 필터와의 상호작용 주의
status 필터 활성 시 INACTIVE 사용자와의 조인/지연로딩이 제외될 수 있습니다. 본 프로젝트의 “엔티티→도메인 매핑 후 도메인만 사용” 흐름(러닝 참고)이라면 영향 미미하나, fetch join 쿼리가 있다면 null 연관 가능성 검토를 요청드립니다.
77-77: 포맷팅 변경만 — 문제 없습니다src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java (1)
42-44: fetch join 사용 없음 확인 및 추가 검토 요청
userJpaEntity에 대해 코드 전반에서JOIN FETCH나@EntityGraph설정이 보이지 않습니다.- 다만, 동적 JPQL 문자열 또는 Criteria API 방식으로 fetch join을 사용하는 사례가 있을 수 있으니 직접 검색·확인 필요합니다.
optional=false+ Hibernate 필터 조합 시, INACTIVE 상태의 사용자 접근 시 프록시 초기화 오류가 발생할 수 있으므로(Optional=false → Optional=true 검토 권장) 설계 검토 후 대응 방안 결정 바랍니다.src/main/java/konkuk/thip/post/adapter/out/jpa/PostLikeJpaEntity.java (1)
20-22: post 연관 필수화 — 적절합니다
FK 무결성 강화 방향 동의합니다.src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (3)
269-276: 진행중 조건(시작 ≤ 오늘 ≤ 종료) 및 정렬 기준 적절조건 정의와 커서/정렬이 일치합니다.
286-309: 진행+모집 통합 로직 적절playing/recruiting 분기별 커서와 우선순위 Case 활용이 명확합니다.
316-326: 만료 조건 및 역정렬 커서 처리 적절before(today) + desc 정렬, fetchMyRooms의 descending 커서 조건과 일관적입니다.
| /** 모집중 + ACTIVE 공통 where */ | ||
| private BooleanBuilder recruitingActiveWhere(LocalDate today) { | ||
| BooleanBuilder where = new BooleanBuilder(); | ||
| where.and(room.startDate.after(today)) | ||
| .and(room.status.eq(ACTIVE)); | ||
| where.and(room.startDate.after(today)); | ||
| return where; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
모집중 where에 공개방 필터 포함 + 주석 정리 필요
현재 모집중 검색 계열에서 비공개 방이 노출될 수 있습니다. 공개 목록(탐색/추천)이라면 isPublic 조건을 공통 where에 포함하는 편이 안전합니다. 주석의 ACTIVE 표현도 필터 전략으로 대체되었으니 정리하면 좋습니다.
추천 인덱스: room(start_date, room_id), room(is_public, start_date, room_id).
- /** 모집중 + ACTIVE 공통 where */
+ /** 모집중 공통 where (상태 필터는 Hibernate Filter에 위임) */
private BooleanBuilder recruitingActiveWhere(LocalDate today) {
BooleanBuilder where = new BooleanBuilder();
- where.and(room.startDate.after(today));
+ where.and(room.startDate.after(today));
+ // 공개 방만 노출
+ where.and(room.isPublic.isTrue());
return where;
}📝 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.
| /** 모집중 + ACTIVE 공통 where */ | |
| private BooleanBuilder recruitingActiveWhere(LocalDate today) { | |
| BooleanBuilder where = new BooleanBuilder(); | |
| where.and(room.startDate.after(today)) | |
| .and(room.status.eq(ACTIVE)); | |
| where.and(room.startDate.after(today)); | |
| return where; | |
| /** 모집중 공통 where (상태 필터는 Hibernate Filter에 위임) */ | |
| private BooleanBuilder recruitingActiveWhere(LocalDate today) { | |
| BooleanBuilder where = new BooleanBuilder(); | |
| where.and(room.startDate.after(today)); | |
| // 공개 방만 노출 | |
| where.and(room.isPublic.isTrue()); | |
| return where; | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java
around lines 37-41, the recruitingActiveWhere method currently only filters by
startDate and may return private rooms; update this common where to also filter
for public rooms (add an isPublic == true predicate) and replace the outdated
"ACTIVE" comment with a concise description like "공개 방 + 모집중 필터 (startDate >
today, isPublic = true)"; ensure the added predicate is combined with the
existing BooleanBuilder and keep the method focused on shared search constraints
(note suggested DB indexes: room(start_date, room_id) and room(is_public,
start_date, room_id)).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
방 검색 시에는 비공개 방을 포함해서 노출하는 것이 요구사항이야.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seongjunnoh 알겠습니다! 요구사항을 명확히 해주셔서 감사합니다. 방 검색에서는 비공개 방도 포함해서 보여주는 것이 맞군요.
제안했던 isPublic 필터는 잘못된 가정이었습니다. 코드를 다시 보니 컨텍스트에 따라 공개방 필터를 적용하는 곳(findDeadlinePopularRoomCondition, findOtherRecruitingRoomsByCategoryOrderByStartDateAsc)과 그렇지 않은 곳이 구분되어 있네요.
그러면 주석만 정리하는 것으로 충분할 것 같습니다:
/** 모집중 공통 where (상태 필터는 Hibernate Filter에 위임) */
private BooleanBuilder recruitingActiveWhere(LocalDate today) {
BooleanBuilder where = new BooleanBuilder();
where.and(room.startDate.after(today));
return where;
}✏️ Learnings added
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#287
File: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java:37-41
Timestamp: 2025-09-01T10:29:31.339Z
Learning: seongjunnoh 프로젝트에서 방 검색 시에는 공개방과 비공개방을 모두 포함하여 노출하는 것이 요구사항이다. isPublic 필터는 추천/인기방 등 특정 컨텍스트에서만 적용된다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#285
File: src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java:1-7
Timestamp: 2025-08-31T05:25:14.835Z
Learning: seongjunnoh는 enum 의존성에 대해 유연한 접근을 선호하며, 도메인→어댑터 레이어 참조와 같은 아키텍처 layering 원칙보다 실용적인 구현을 우선시한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.371Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/common/exception/code/ErrorCode.java:210-211
Timestamp: 2025-08-24T09:40:48.595Z
Learning: seongjunnoh는 HTTP 상태 코드 선택에 대해 기술적 근거와 코드베이스 내 일관성을 중요하게 생각하며, 구체적인 사례 분석을 통한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/TestTokenController.java:0-0
Timestamp: 2025-08-24T09:33:52.982Z
Learning: seongjunnoh는 Spring의 ConditionalOnProperty 동작 원리를 정확히 이해하고 있으며, 보안 이슈에 대해서도 실질적인 위험성을 기준으로 판단하는 실용적 접근을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
- soft delete 된 유저를 포함해서 닉네임 중복 검증 수행하도록 filter 비활성화
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java (3)
45-46: 직접 SQL 대신 레포지토리 soft delete 사용 권장스키마 의존(테이블/컬럼명 변경 시 깨짐)과 엔티티 리스너/캐시 일관성 이슈를 피하기 위해, 테스트에서도 JPA 경로로 소프트 딜리트를 수행하는 편이 안전합니다.
-import org.springframework.jdbc.core.JdbcTemplate; +// JdbcTemplate 불필요 - @Autowired private JdbcTemplate jdbcTemplate; + // JdbcTemplate 필드 제거 - jdbcTemplate.update( - "UPDATE users SET status = ? WHERE user_id = ?", - StatusType.INACTIVE.name(), deleteUser.getUserId()); + userJpaRepository.deleteById(deleteUser.getUserId()); + // 필요 시 가시성 보장을 위해 flush() + // userJpaRepository.flush();Also applies to: 158-161
50-50: deleteAllInBatch 전환 좋습니다테스트 속도에 유리합니다. FK 제약 순서가 필요한 경우만 주의하면 됩니다.
152-178: 필터 상태를 테스트에서 명시화해 플래키니스 방지본 테스트는 INACTIVE도 중복으로 간주하는 비즈니스 규칙을 검증합니다. 향후 기본 필터 정책 변경에 의해 깨질 수 있으니, 서비스(or AOP) 단에서 필터 비활성화가 보장됨을 명시(예: 커스텀 어노테이션/토글)하거나 사전 조건을 assert로 확인하는 방식을 권장합니다.
예시:
- 사전확인: “해당 닉네임 존재 여부 쿼리”가 INACTIVE 포함으로 동작하는지 별도 호출/검증
- 또는 이 테스트 메서드/서비스에 “필터 OFF” 어노테이션(있다면) 부착
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java(1 hunks)src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java(2 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java:5-5
Timestamp: 2025-08-14T07:19:27.310Z
Learning: AttendanceCheck 엔티티는 BaseJpaEntity를 상속받아 status 필드(StatusType.ACTIVE/INACTIVE/EXPIRED)로 소프트 딜리트를 구현하므로, 카운트 쿼리에서 status = 'ACTIVE' 조건을 포함해야 삭제된 레코드를 제외할 수 있음.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#285
File: src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java:1-7
Timestamp: 2025-08-31T05:25:14.835Z
Learning: seongjunnoh는 enum 의존성에 대해 유연한 접근을 선호하며, 도메인→어댑터 레이어 참조와 같은 아키텍처 layering 원칙보다 실용적인 구현을 우선시한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.371Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/common/exception/code/ErrorCode.java:210-211
Timestamp: 2025-08-24T09:40:48.595Z
Learning: seongjunnoh는 HTTP 상태 코드 선택에 대해 기술적 근거와 코드베이스 내 일관성을 중요하게 생각하며, 구체적인 사례 분석을 통한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/TestTokenController.java:0-0
Timestamp: 2025-08-24T09:33:52.982Z
Learning: seongjunnoh는 Spring의 ConditionalOnProperty 동작 원리를 정확히 이해하고 있으며, 보안 이슈에 대해서도 실질적인 위험성을 기준으로 판단하는 실용적 접근을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
📚 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/user/adapter/out/persistence/repository/UserJpaRepository.java
🧬 Code graph analysis (1)
src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(30-394)
⏰ 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 (1)
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java (1)
10-14: findByUserId 도입 적절 — 필터가 적용되는 ID 단건 조회 경로 확보Derived Query로 JPQL이 생성되어 Hibernate Filter가 적용됩니다. 기존의 PK 직조회(find) 우회 위험을 줄여줍니다.
| public interface UserJpaRepository extends JpaRepository<UserJpaEntity, Long>, UserQueryRepository { | ||
|
|
||
| /** | ||
| * 소프트 딜리트 적용 대상 entity 단건 조회 메서드 | ||
| */ | ||
| Optional<UserJpaEntity> findByUserId(Long userId); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
JpaRepository.findById가 여전히 노출됨 — 필터 우회 가능성 차단 필요
JpaRepository에서 상속되는 findById는 EntityManager.find를 사용해 Hibernate Filter가 적용되지 않습니다. 무심코 호출되면 INACTIVE가 노출될 수 있습니다. 레포지토리에서 findById를 JPQL로 재정의해 필터를 강제 적용하는 것을 권장합니다.
아래 패치를 적용하면 상속 메서드를 JPQL로 대체해 필터 적용을 보장할 수 있습니다.
package konkuk.thip.user.adapter.out.persistence.repository;
import konkuk.thip.user.adapter.out.jpa.UserJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
import java.util.Optional;
public interface UserJpaRepository extends JpaRepository<UserJpaEntity, Long>, UserQueryRepository {
/**
* 소프트 딜리트 적용 대상 entity 단건 조회 메서드
*/
Optional<UserJpaEntity> findByUserId(Long userId);
+ /**
+ * 주의: Hibernate Filter를 강제 적용하기 위해 JPQL로 재정의합니다.
+ */
+ @Deprecated(forRemoval = false)
+ @Query("select u from UserJpaEntity u where u.userId = :userId")
+ Optional<UserJpaEntity> findById(@Param("userId") Long userId);레포지토리 전역에서 우발적 findById 사용을 점검해 주세요:
🏁 Script executed:
#!/bin/bash
# findById 호출부 점검
rg -nP --type=java -C2 '\.findById\s*\('
# EntityManager 직접 사용으로 필터 우회하는 코드 점검(선택)
rg -nP --type=java -C2 'EntityManager\s+\w+|\.find\s*\(\s*UserJpaEntity'Length of output: 78730
UserJpaRepository에 findById 재정의 추가
기본 EntityManager.find(JpaRepository.findById)는 Hibernate Filter를 적용하지 않아 soft-delete된 엔티티가 노출될 수 있습니다 (필터는 쿼리에는 적용되지만 직접 조회에는 적용되지 않음) (docs.jboss.org).
다음 메서드를 UserJpaRepository에 추가해 soft-delete 필터를 강제 적용하세요:
public interface UserJpaRepository extends JpaRepository<UserJpaEntity, Long>, UserQueryRepository {
Optional<UserJpaEntity> findByUserId(Long userId);
+ /** Hibernate Filter 강제 적용을 위한 JPQL 재정의 */
+ @Deprecated
+ @Query("select u from UserJpaEntity u where u.userId = :userId")
+ Optional<UserJpaEntity> findById(@Param("userId") Long userId);
}전역 .findById( 호출도 점검해 주세요:
rg -nP --type=java '\.findById\s*\('🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java
around lines 8-14, add a redefined findById method that enforces the Hibernate
soft-delete filter (i.e., declare Optional<UserJpaEntity> findById(Long id); in
this repository so Spring Data uses a query that respects filters instead of
EntityManager.find), and then run a project-wide search for raw .findById( calls
(eg. rg -nP --type=java '\.findById\s*\(') to identify and replace usages that
would bypass the filter with repository calls that apply soft-delete or use
repository methods that query with the soft-delete constraint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
그래서 soft delete 적용대상인 jpa entity 들을 로드하는 메서드를 findByUserId 와 같이 jpa repository에 정의해두고, 영속성 어댑터에서는 해당 메서드를 호출하도록 구성했어. 테스트 코드를 통해서 findByUserId() 메서드를 호출할 시 필터를 거치는 것을 확인했어.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seongjunnoh 좋은 접근 방식이네요! 말씀하신 해결책이 제가 제안한 findById 재정의보다 더 나은 방법일 수 있습니다.
도메인별 명시적 메서드들(findByUserId, findByRoomId 등)을 사용하면:
- 의도가 명확함: 개발자가 실수로 필터를 우회할 가능성이 낮아집니다
- 더 안전함: 필터가 적용되지 않는
findById가 여전히 필요한 경우를 위해 남겨둘 수 있습니다 - 도메인 중심적: 메서드 이름이 비즈니스 의미를 더 잘 반영합니다
AI 요약을 보니 모든 영속성 어댑터에서 체계적으로 이런 방식으로 변경하셨고, 테스트도 통과한 것 같네요. 이런 일관성 있는 리팩토링이 인상적입니다.
필터 동작을 확인하신 테스트 결과도 신뢰할 수 있을 것 같습니다. 잘 구현하셨네요! 👍
✏️ Learnings added
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#287
File: src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java:8-14
Timestamp: 2025-09-01T13:18:13.630Z
Learning: seongjunnoh는 JpaRepository의 findById 메서드 재정의보다는 도메인별 명시적 메서드(findByUserId, findByRoomId 등)를 정의하여 Hibernate Filter 적용을 보장하는 방식을 선호하며, 이를 통해 더 안전하고 의도가 명확한 코드 구조를 구축한다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
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 안전성을 위해 권장됩니다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#285
File: src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java:1-7
Timestamp: 2025-08-31T05:25:14.835Z
Learning: seongjunnoh는 enum 의존성에 대해 유연한 접근을 선호하며, 도메인→어댑터 레이어 참조와 같은 아키텍처 layering 원칙보다 실용적인 구현을 우선시한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.371Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/common/exception/code/ErrorCode.java:210-211
Timestamp: 2025-08-24T09:40:48.595Z
Learning: seongjunnoh는 HTTP 상태 코드 선택에 대해 기술적 근거와 코드베이스 내 일관성을 중요하게 생각하며, 구체적인 사례 분석을 통한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/TestTokenController.java:0-0
Timestamp: 2025-08-24T09:33:52.982Z
Learning: seongjunnoh는 Spring의 ConditionalOnProperty 동작 원리를 정확히 이해하고 있으며, 보안 이슈에 대해서도 실질적인 위험성을 기준으로 판단하는 실용적 접근을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
buzz0331
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굿굿 수고하셨습니다~
| @ManyToOne(fetch = FetchType.LAZY, optional = false) | ||
| @JoinColumn(name = "user_id", nullable = false) | ||
| private UserJpaEntity userJpaEntity; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @ManyToOne(fetch = FetchType.LAZY, optional = false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굿굿 반영 감삼다
| private static final String PCUT_INCLUDE_INACTIVE = | ||
| "@annotation(" + ANN_INCLUDE_INACTIVE + ")"; | ||
| "@annotation(" + ANN_INCLUDE_INACTIVE + ") && (" + "@annotation(" + ANN_TX + ") || @within(" + ANN_TX + ")" + ")"; | ||
|
|
||
| // @Unfiltered: 트랜잭션 컨텍스트가 보장된 경우에만 동작 | ||
| private static final String PCUT_UNFILTERED = | ||
| "@annotation(" + ANN_UNFILTERED + ")"; | ||
| "@annotation(" + ANN_UNFILTERED + ") && (" + "@annotation(" + ANN_TX + ") || @within(" + ANN_TX + ")" + ")"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
| private Session currentTxSession() { | ||
| if (!TransactionSynchronizationManager.isActualTransactionActive()) { | ||
| throw new InvalidStateException(PERSISTENCE_TRANSACTION_REQUIRED); | ||
| } | ||
| return session(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오호 트랜잭션 경계내에서 호출되는 것인지 검증하는 건가욥
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵 맞습니다! 이전 코드는 바로 em.unwrap(Session.class) 를 통해 바로 session을 획득하도록 했었는데, 코래의 리뷰에 따라 실제로 트랜잭션이 활성화 되었는지를 확인한 후, session을 획득할 수 있도록 수정하였습니다!
코드를 수정함으로써 필터와 관련된 모든 동작이 현재 스레드의 트랜잭션에 바인딩된 session 에만 적용되도록 보장할 수 있을것 같습니다!
#️⃣ 연관된 이슈
📝 작업 내용
status = active 인 데이터만을 조회하는 hibernate filter 를 도입하였습니다.
저희 프로젝트의 jpa entity는
위와 같이 구성되어 있습니다.
이때 soft delete 적용 대상인 entity 들은 삭제요청 후에도 DB에 여전히 존재하는 데이터이므로, 조회시에 status = active 인 entity 만을 조회하도록 부가적인 작업이 필요했습니다 (ex. 모든 조회관련 영속성 코드에 'where status = active' 명시)
하지만 위와 같은 방식은 비효율적이고, 누락될 가능성이 있다고 판단하여 아래와 같은 방법을 고안하였습니다.
이 중 상황에 따라 status = inactive 인 데이터를 추가적인 코드 수정없이 제어하기 위해 3번의 방식을 채택하여 구현하였습니다. (자세한 설명은 노션 문서화를 참고해주시면 감사하겠습니다)
ref : https://code-boki.tistory.com/297
주요 코드 설명
📸 스크린샷
💬 리뷰 요구사항
도입한 hibernate filter + aop 의 동작을 검증하기 위해 테스트 코드를 작성하였습니다! 테스트 코드를 먼저 참고해주시면 구현된 코드를 리뷰하시기 더 편할 것 같습니다!!
Jpa Repository 기본 제공 메서드인 'findById()' 메서드는 내부적으로 entityManager.find() 를 호출하는데, em.find() 는 단순 PK 조회이므로 hibernate filter / where 조건이 무시됩니다. (-> inactive 인 엔티티도 조회됨) (https://discourse.hibernate.org/t/find-by-id-and-filters/7632 -> 공식문서 qna+ stackoverflow 에서 그렇다고 하네요)
따라서 soft delete 대상인 entity 에 한해서 repository 에 entity 단건 조회용 메서드를 정의하고, 영속성 어댑터가 이 메서드를 호출하도록 수정하였습니다!!
repository 에 정의한 메서드는 jpql 기반 커스텀 메서드 이므로 jpql 실행 시 hibernate filter가 정상 작동하여 active 인 엔티티만 조회가능합니다. (테스트 코드로 검증한 부분이 있으니 참고해주시면 감사하겠습니다)
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit