1- package com .back .domain .board .repository ;
2-
3- import com .back .domain .board .dto .CommentListResponse ;
4- import com .back .domain .board .dto .QAuthorResponse ;
5- import com .back .domain .board .dto . QCommentListResponse ;
6- import com .back .domain .board .entity .Comment ;
7- import com .back .domain .board .entity .QComment ;
8- import com .back .domain .board .entity . QCommentLike ;
1+ package com .back .domain .board .comment . repository ;
2+
3+ import com .back .domain .board .comment . dto .CommentListResponse ;
4+ import com .back .domain .board .comment . dto .QCommentListResponse ;
5+ import com .back .domain .board .comment . entity . Comment ;
6+ import com .back .domain .board .comment . entity .QComment ;
7+ import com .back .domain .board .comment . entity .QCommentLike ;
8+ import com .back .domain .board .common . dto . QAuthorResponse ;
99import com .back .domain .user .entity .QUser ;
1010import com .back .domain .user .entity .QUserProfile ;
1111import com .querydsl .core .types .Order ;
1515import com .querydsl .core .types .dsl .PathBuilder ;
1616import com .querydsl .jpa .impl .JPAQueryFactory ;
1717import lombok .RequiredArgsConstructor ;
18- import org .springframework .data .domain .Page ;
19- import org .springframework .data .domain .PageImpl ;
20- import org .springframework .data .domain .Pageable ;
21- import org .springframework .data .domain .Sort ;
22-
18+ import org .springframework .data .domain .*;
2319import java .util .*;
2420import java .util .stream .Collectors ;
2521
2622@ RequiredArgsConstructor
2723public class CommentRepositoryImpl implements CommentRepositoryCustom {
24+
2825 private final JPAQueryFactory queryFactory ;
2926
27+ /**
28+ * 게시글 ID로 댓글 목록 조회
29+ * - 부모 댓글 페이징 + 자식 댓글 전체 조회
30+ * - likeCount는 부모/자식 댓글을 한 번에 조회 후 주입
31+ * - likeCount 정렬은 메모리에서 처리
32+ * - 총 쿼리 수: 4회 (부모조회 + 자식조회 + likeCount + count)
33+ *
34+ * @param postId 게시글 Id
35+ * @param pageable 페이징 + 정렬 조건
36+ */
3037 @ Override
3138 public Page <CommentListResponse > getCommentsByPostId (Long postId , Pageable pageable ) {
3239 QComment comment = QComment .comment ;
3340 QCommentLike commentLike = QCommentLike .commentLike ;
3441
35- // 정렬 조건
36- List <OrderSpecifier <?>> orders = buildOrderSpecifiers (pageable , comment , commentLike );
42+ // 1. 정렬 조건 생성 (엔티티 필드 기반)
43+ List <OrderSpecifier <?>> orders = buildOrderSpecifiers (pageable , comment );
3744
38- // 부모 댓글 조회
45+ // 2. 부모 댓글 조회 (페이징)
3946 List <CommentListResponse > parents = fetchComments (
4047 comment .post .id .eq (postId ).and (comment .parent .isNull ()),
4148 orders ,
@@ -47,48 +54,33 @@ public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pagea
4754 return new PageImpl <>(parents , pageable , 0 );
4855 }
4956
50- // 부모 id 수집
57+ // 3. 부모 ID 목록 수집
5158 List <Long > parentIds = parents .stream ()
5259 .map (CommentListResponse ::getCommentId )
5360 .toList ();
5461
55- // 자식 댓글 조회
62+ // 4. 자식 댓글 조회 (부모 ID 기준)
5663 List <CommentListResponse > children = fetchComments (
5764 comment .parent .id .in (parentIds ),
5865 List .of (comment .createdAt .asc ()),
5966 null ,
6067 null
6168 );
6269
63- // 부모 + 자식 id 합쳐서 likeCount 한 번에 조회
64- List <Long > allIds = new ArrayList <>(parentIds );
65- allIds .addAll (children .stream ().map (CommentListResponse ::getCommentId ).toList ());
66-
67- Map <Long , Long > likeCountMap = queryFactory
68- .select (commentLike .comment .id , commentLike .count ())
69- .from (commentLike )
70- .where (commentLike .comment .id .in (allIds ))
71- .groupBy (commentLike .comment .id )
72- .fetch ()
73- .stream ()
74- .collect (Collectors .toMap (
75- tuple -> tuple .get (commentLike .comment .id ),
76- tuple -> tuple .get (commentLike .count ())
77- ));
70+ // 5. 부모 + 자식 댓글 ID 합쳐 likeCount 조회 (쿼리 1회)
71+ Map <Long , Long > likeCountMap = fetchLikeCounts (parentIds , children );
7872
79- // likeCount 세팅
73+ // 6. likeCount 주입
8074 parents .forEach (p -> p .setLikeCount (likeCountMap .getOrDefault (p .getCommentId (), 0L )));
8175 children .forEach (c -> c .setLikeCount (likeCountMap .getOrDefault (c .getCommentId (), 0L )));
8276
83- // parentId → children 매핑
84- Map <Long , List <CommentListResponse >> childMap = children .stream ()
85- .collect (Collectors .groupingBy (CommentListResponse ::getParentId ));
77+ // 7. 부모-자식 매핑
78+ mapChildrenToParents (parents , children );
8679
87- parents .forEach (p ->
88- p .setChildren (childMap .getOrDefault (p .getCommentId (), List .of ()))
89- );
80+ // 8. 정렬 후처리 (통계 필드 기반)
81+ parents = sortInMemoryIfNeeded (parents , pageable );
9082
91- // 총 개수 ( 부모 댓글만 카운트)
83+ // 9. 전체 부모 댓글 수 조회
9284 Long total = queryFactory
9385 .select (comment .count ())
9486 .from (comment )
@@ -98,8 +90,12 @@ public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pagea
9890 return new PageImpl <>(parents , pageable , total != null ? total : 0L );
9991 }
10092
93+ // -------------------- 내부 메서드 --------------------
94+
10195 /**
102- * 공통 댓글 조회 메서드 (부모/자식 공통)
96+ * 댓글 조회
97+ * - User / UserProfile join (N+1 방지)
98+ * - likeCount는 이후 주입
10399 */
104100 private List <CommentListResponse > fetchComments (
105101 BooleanExpression condition ,
@@ -118,10 +114,10 @@ private List<CommentListResponse> fetchComments(
118114 comment .parent .id ,
119115 new QAuthorResponse (user .id , profile .nickname ),
120116 comment .content ,
121- Expressions .constant (0L ), // likeCount placeholder
117+ Expressions .constant (0L ), // likeCount는 별도 주입
122118 comment .createdAt ,
123119 comment .updatedAt ,
124- Expressions .constant (Collections .emptyList ())
120+ Expressions .constant (Collections .emptyList ()) // children은 별도 주입
125121 ))
126122 .from (comment )
127123 .leftJoin (comment .user , user )
@@ -137,22 +133,89 @@ private List<CommentListResponse> fetchComments(
137133 }
138134
139135 /**
140- * 정렬 조건 처리
136+ * likeCount 일괄 조회
137+ * - IN 조건 기반 groupBy 쿼리 1회
138+ * - 부모/자식 댓글을 한 번에 조회
141139 */
142- private List <OrderSpecifier <?>> buildOrderSpecifiers (Pageable pageable , QComment comment , QCommentLike commentLike ) {
140+ private Map <Long , Long > fetchLikeCounts (List <Long > parentIds , List <CommentListResponse > children ) {
141+ QCommentLike commentLike = QCommentLike .commentLike ;
142+
143+ List <Long > allIds = new ArrayList <>(parentIds );
144+ allIds .addAll (children .stream ().map (CommentListResponse ::getCommentId ).toList ());
145+
146+ if (allIds .isEmpty ()) return Map .of ();
147+
148+ return queryFactory
149+ .select (commentLike .comment .id , commentLike .count ())
150+ .from (commentLike )
151+ .where (commentLike .comment .id .in (allIds ))
152+ .groupBy (commentLike .comment .id )
153+ .fetch ()
154+ .stream ()
155+ .collect (Collectors .toMap (
156+ tuple -> tuple .get (commentLike .comment .id ),
157+ tuple -> tuple .get (commentLike .count ())
158+ ));
159+ }
160+
161+ /**
162+ * 부모/자식 관계 매핑
163+ * - childMap을 parentId 기준으로 그룹화 후 children 필드에 set
164+ */
165+ private void mapChildrenToParents (List <CommentListResponse > parents , List <CommentListResponse > children ) {
166+ if (children .isEmpty ()) return ;
167+
168+ Map <Long , List <CommentListResponse >> childMap = children .stream ()
169+ .collect (Collectors .groupingBy (CommentListResponse ::getParentId ));
170+
171+ parents .forEach (parent ->
172+ parent .setChildren (childMap .getOrDefault (parent .getCommentId (), List .of ()))
173+ );
174+ }
175+
176+ /**
177+ * 정렬 처리 (DB 정렬)
178+ * - createdAt, updatedAt 등 엔티티 필드
179+ */
180+ private List <OrderSpecifier <?>> buildOrderSpecifiers (Pageable pageable , QComment comment ) {
143181 PathBuilder <Comment > entityPath = new PathBuilder <>(Comment .class , comment .getMetadata ());
144182 List <OrderSpecifier <?>> orders = new ArrayList <>();
145183
146184 for (Sort .Order order : pageable .getSort ()) {
147- Order direction = order .isAscending () ? Order .ASC : Order .DESC ;
148185 String prop = order .getProperty ();
149186
150- switch (prop ) {
151- case "likeCount" -> orders .add (new OrderSpecifier <>(direction , commentLike .id .countDistinct ()));
152- default -> orders .add (new OrderSpecifier <>(direction ,
153- entityPath .getComparable (prop , Comparable .class )));
187+ // 통계 필드는 메모리 정렬에서 처리
188+ if (prop .equals ("likeCount" )) {
189+ continue ;
154190 }
191+ Order direction = order .isAscending () ? Order .ASC : Order .DESC ;
192+ orders .add (new OrderSpecifier <>(direction , entityPath .getComparable (prop , Comparable .class )));
155193 }
194+
156195 return orders ;
157196 }
197+
198+ /**
199+ * 통계 기반 정렬 처리 (메모리)
200+ * - likeCount 등 통계 필드
201+ * - 페이지 단위라 성능에 영향 없음
202+ */
203+ private List <CommentListResponse > sortInMemoryIfNeeded (List <CommentListResponse > results , Pageable pageable ) {
204+ if (results .isEmpty () || !pageable .getSort ().isSorted ()) return results ;
205+
206+ for (Sort .Order order : pageable .getSort ()) {
207+ Comparator <CommentListResponse > comparator = null ;
208+
209+ if ("likeCount" .equals (order .getProperty ())) {
210+ comparator = Comparator .comparing (CommentListResponse ::getLikeCount );
211+ }
212+
213+ if (comparator != null ) {
214+ if (order .isDescending ()) comparator = comparator .reversed ();
215+ results .sort (comparator );
216+ }
217+ }
218+
219+ return results ;
220+ }
158221}
0 commit comments