Skip to content

Commit 7120628

Browse files
Refactor: 게시판(Post/Comment) 레포지토리 및 패키지 구조 개선 (#166) (#172)
* Ref: PostRespositoryImpl 개선 * Ref: CommentRepositoryImpl 개선 * Ref: board 패키지 구조 개선 --------- Co-authored-by: loseminho <[email protected]>
1 parent f9423f4 commit 7120628

36 files changed

+502
-391
lines changed

src/main/java/com/back/domain/board/controller/CommentController.java renamed to src/main/java/com/back/domain/board/comment/controller/CommentController.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
package com.back.domain.board.controller;
1+
package com.back.domain.board.comment.controller;
22

3-
import com.back.domain.board.dto.CommentListResponse;
4-
import com.back.domain.board.dto.CommentRequest;
5-
import com.back.domain.board.dto.CommentResponse;
6-
import com.back.domain.board.dto.PageResponse;
7-
import com.back.domain.board.service.CommentService;
3+
import com.back.domain.board.comment.dto.CommentListResponse;
4+
import com.back.domain.board.comment.dto.CommentRequest;
5+
import com.back.domain.board.comment.dto.CommentResponse;
6+
import com.back.domain.board.common.dto.PageResponse;
7+
import com.back.domain.board.comment.service.CommentService;
88
import com.back.global.common.dto.RsData;
99
import com.back.global.security.user.CustomUserDetails;
1010
import jakarta.validation.Valid;

src/main/java/com/back/domain/board/controller/CommentControllerDocs.java renamed to src/main/java/com/back/domain/board/comment/controller/CommentControllerDocs.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
package com.back.domain.board.controller;
1+
package com.back.domain.board.comment.controller;
22

3-
import com.back.domain.board.dto.CommentListResponse;
4-
import com.back.domain.board.dto.CommentRequest;
5-
import com.back.domain.board.dto.CommentResponse;
6-
import com.back.domain.board.dto.PageResponse;
3+
import com.back.domain.board.comment.dto.CommentListResponse;
4+
import com.back.domain.board.comment.dto.CommentRequest;
5+
import com.back.domain.board.comment.dto.CommentResponse;
6+
import com.back.domain.board.common.dto.PageResponse;
77
import com.back.global.common.dto.RsData;
88
import com.back.global.security.user.CustomUserDetails;
99
import io.swagger.v3.oas.annotations.Operation;

src/main/java/com/back/domain/board/dto/CommentListResponse.java renamed to src/main/java/com/back/domain/board/comment/dto/CommentListResponse.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
package com.back.domain.board.dto;
1+
package com.back.domain.board.comment.dto;
22

3+
import com.back.domain.board.common.dto.AuthorResponse;
34
import com.querydsl.core.annotations.QueryProjection;
45
import lombok.Getter;
56
import lombok.Setter;

src/main/java/com/back/domain/board/dto/CommentRequest.java renamed to src/main/java/com/back/domain/board/comment/dto/CommentRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.back.domain.board.dto;
1+
package com.back.domain.board.comment.dto;
22

33
import jakarta.validation.constraints.NotBlank;
44

src/main/java/com/back/domain/board/dto/CommentResponse.java renamed to src/main/java/com/back/domain/board/comment/dto/CommentResponse.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
package com.back.domain.board.dto;
1+
package com.back.domain.board.comment.dto;
22

3-
import com.back.domain.board.entity.Comment;
3+
import com.back.domain.board.common.dto.AuthorResponse;
4+
import com.back.domain.board.comment.entity.Comment;
45

56
import java.time.LocalDateTime;
67

src/main/java/com/back/domain/board/entity/Comment.java renamed to src/main/java/com/back/domain/board/comment/entity/Comment.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
package com.back.domain.board.entity;
1+
package com.back.domain.board.comment.entity;
22

3+
import com.back.domain.board.post.entity.Post;
34
import com.back.domain.user.entity.User;
45
import com.back.global.entity.BaseEntity;
56
import jakarta.persistence.*;

src/main/java/com/back/domain/board/entity/CommentLike.java renamed to src/main/java/com/back/domain/board/comment/entity/CommentLike.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.back.domain.board.entity;
1+
package com.back.domain.board.comment.entity;
22

33
import com.back.domain.user.entity.User;
44
import com.back.global.entity.BaseEntity;

src/main/java/com/back/domain/board/repository/CommentRepository.java renamed to src/main/java/com/back/domain/board/comment/repository/CommentRepository.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
package com.back.domain.board.repository;
1+
package com.back.domain.board.comment.repository;
22

3-
import com.back.domain.board.entity.Comment;
3+
import com.back.domain.board.comment.entity.Comment;
44
import org.springframework.data.jpa.repository.JpaRepository;
55
import org.springframework.stereotype.Repository;
66

src/main/java/com/back/domain/board/repository/CommentRepositoryCustom.java renamed to src/main/java/com/back/domain/board/comment/repository/CommentRepositoryCustom.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
package com.back.domain.board.repository;
1+
package com.back.domain.board.comment.repository;
22

3-
import com.back.domain.board.dto.CommentListResponse;
3+
import com.back.domain.board.comment.dto.CommentListResponse;
44
import org.springframework.data.domain.Page;
55
import org.springframework.data.domain.Pageable;
66

Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
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;
99
import com.back.domain.user.entity.QUser;
1010
import com.back.domain.user.entity.QUserProfile;
1111
import com.querydsl.core.types.Order;
@@ -15,27 +15,34 @@
1515
import com.querydsl.core.types.dsl.PathBuilder;
1616
import com.querydsl.jpa.impl.JPAQueryFactory;
1717
import 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.*;
2319
import java.util.*;
2420
import java.util.stream.Collectors;
2521

2622
@RequiredArgsConstructor
2723
public 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

Comments
 (0)