Skip to content

Commit d833021

Browse files
committed
Ref: 내 댓글 목록 조회 Query DSL 기반 개선
1 parent b9e755c commit d833021

File tree

7 files changed

+175
-86
lines changed

7 files changed

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

3-
import com.back.domain.board.comment.entity.Comment;
3+
import com.querydsl.core.annotations.QueryProjection;
44

55
import java.time.LocalDateTime;
66

@@ -28,28 +28,6 @@ public record MyCommentResponse(
2828
LocalDateTime createdAt,
2929
LocalDateTime updatedAt
3030
) {
31-
public static MyCommentResponse from(Comment comment) {
32-
return new MyCommentResponse(
33-
comment.getId(),
34-
comment.getPost().getId(),
35-
comment.getPost().getTitle(),
36-
comment.getParent() != null
37-
? comment.getParent().getId()
38-
: null,
39-
comment.getParent() != null
40-
? truncate(comment.getParent().getContent())
41-
: null,
42-
comment.getContent(),
43-
comment.getLikeCount(),
44-
comment.getCreatedAt(),
45-
comment.getUpdatedAt()
46-
);
47-
}
48-
49-
private static String truncate(String content) {
50-
int length = 50;
51-
return (content == null || content.length() <= length)
52-
? content
53-
: content.substring(0, length) + "...";
54-
}
31+
@QueryProjection
32+
public MyCommentResponse {}
5533
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22

33
import com.back.domain.board.comment.entity.Comment;
44
import com.back.domain.board.comment.repository.custom.CommentRepositoryCustom;
5-
import org.springframework.data.domain.Page;
6-
import org.springframework.data.domain.Pageable;
75
import org.springframework.data.jpa.repository.JpaRepository;
86
import org.springframework.stereotype.Repository;
97

108
@Repository
119
public interface CommentRepository extends JpaRepository<Comment, Long>, CommentRepositoryCustom {
12-
Page<Comment> findAllByUserId(Long userId, Pageable pageable);
1310
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.back.domain.board.comment.repository.custom;
22

33
import com.back.domain.board.comment.dto.CommentListResponse;
4+
import com.back.domain.board.comment.dto.MyCommentResponse;
45
import org.springframework.data.domain.Page;
56
import org.springframework.data.domain.Pageable;
67

78
public interface CommentRepositoryCustom {
8-
Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pageable);
9+
Page<CommentListResponse> findCommentsByPostId(Long postId, Pageable pageable);
10+
Page<MyCommentResponse> findCommentsByUserId(Long postId, Pageable pageable);
911
}

src/main/java/com/back/domain/board/comment/repository/custom/CommentRepositoryImpl.java

Lines changed: 113 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package com.back.domain.board.comment.repository.custom;
22

33
import com.back.domain.board.comment.dto.CommentListResponse;
4+
import com.back.domain.board.comment.dto.MyCommentResponse;
45
import com.back.domain.board.comment.dto.QCommentListResponse;
6+
import com.back.domain.board.comment.dto.QMyCommentResponse;
57
import com.back.domain.board.comment.entity.Comment;
68
import com.back.domain.board.comment.entity.QComment;
79
import com.back.domain.board.common.dto.QAuthorResponse;
10+
import com.back.domain.board.post.entity.QPost;
811
import com.back.domain.user.entity.QUser;
912
import com.back.domain.user.entity.QUserProfile;
13+
import com.querydsl.core.BooleanBuilder;
1014
import com.querydsl.core.types.Order;
1115
import com.querydsl.core.types.OrderSpecifier;
1216
import com.querydsl.core.types.dsl.BooleanExpression;
@@ -30,23 +34,26 @@ public class CommentRepositoryImpl implements CommentRepositoryCustom {
3034
/**
3135
* 특정 게시글의 댓글 목록 조회
3236
* - 총 쿼리 수: 3회
33-
* 1.부모 댓글 목록을 페이징/정렬 조건으로 조회
34-
* 2.부모 ID 목록으로 자식 댓글 전체 조회
35-
* 3.부모 총 건수(count) 조회
37+
* 1.부모 댓글 목록 조회 (User, UserProfile join)
38+
* 2.자식 댓글 목록 조회 (User, UserProfile join)
39+
* 3.부모 전체 count 조회
3640
*
37-
* @param postId 게시글 Id
41+
* @param postId 게시글 ID
3842
* @param pageable 페이징 + 정렬 조건
3943
*/
4044
@Override
41-
public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pageable) {
45+
public Page<CommentListResponse> findCommentsByPostId(Long postId, Pageable pageable) {
4246
QComment comment = QComment.comment;
4347

44-
// 1. 정렬 조건 생성
48+
// 1. 검색 조건 생성
49+
BooleanExpression condition = comment.post.id.eq(postId).and(comment.parent.isNull());
50+
51+
// 2. 정렬 조건 생성
4552
List<OrderSpecifier<?>> orders = buildOrderSpecifiers(pageable);
4653

47-
// 2. 부모 댓글 조회 (페이징 적용)
54+
// 3. 부모 댓글 조회 (페이징 적용)
4855
List<CommentListResponse> parents = fetchComments(
49-
comment.post.id.eq(postId).and(comment.parent.isNull()),
56+
condition,
5057
orders,
5158
pageable.getOffset(),
5259
pageable.getPageSize()
@@ -57,42 +64,117 @@ public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pagea
5764
return new PageImpl<>(parents, pageable, 0);
5865
}
5966

60-
// 3. 부모 ID 수집
67+
// 4. 부모 ID 수집
6168
List<Long> parentIds = parents.stream()
6269
.map(CommentListResponse::getCommentId)
6370
.toList();
6471

65-
// 4. 자식 댓글 조회 (부모 집합에 대한 전체 조회)
72+
// 5. 자식 댓글 조회 (부모 집합에 대한 전체 조회)
6673
List<CommentListResponse> children = fetchComments(
6774
comment.parent.id.in(parentIds),
6875
List.of(comment.createdAt.asc()), // 시간순 정렬
6976
null,
7077
null
7178
);
7279

73-
// 5. 부모-자식 매핑
80+
// 6. 부모-자식 매핑
7481
mapChildrenToParents(parents, children);
7582

76-
// 6. 전체 부모 댓글 수 조회
77-
Long total = queryFactory
78-
.select(comment.count())
83+
// 7. 전체 부모 댓글 수 조회
84+
long total = countComments(condition);
85+
86+
return new PageImpl<>(parents, pageable, total);
87+
}
88+
89+
/**
90+
* 특정 사용자의 댓글 목록 조회
91+
* - 총 쿼리 수: 2회
92+
* 1. 댓글 목록 조회 (Comment, Post join)
93+
* 2. 전체 count 조회
94+
*
95+
* @param userId 사용자 ID
96+
* @param pageable 페이징 + 정렬 조건
97+
*/
98+
@Override
99+
public Page<MyCommentResponse> findCommentsByUserId(Long userId, Pageable pageable) {
100+
QComment comment = QComment.comment;
101+
QComment parent = new QComment("parent");
102+
QPost post = QPost.post;
103+
104+
// 1. 검색 조건 생성
105+
BooleanExpression condition = comment.user.id.eq(userId);
106+
107+
// 2. 정렬 조건 생성
108+
List<OrderSpecifier<?>> orders = buildOrderSpecifiers(pageable);
109+
110+
// 3. 댓글 목록 조회
111+
List<MyCommentResponse> comments = queryFactory
112+
.select(new QMyCommentResponse(
113+
comment.id,
114+
post.id,
115+
post.title,
116+
parent.id,
117+
parent.content.substring(0, 50),
118+
comment.content,
119+
comment.likeCount,
120+
comment.createdAt,
121+
comment.updatedAt
122+
))
79123
.from(comment)
80-
.where(comment.post.id.eq(postId).and(comment.parent.isNull()))
81-
.fetchOne();
124+
.leftJoin(comment.parent, parent)
125+
.leftJoin(comment.post, post)
126+
.where(condition)
127+
.orderBy(orders.toArray(new OrderSpecifier[0]))
128+
.offset(pageable.getOffset())
129+
.limit(pageable.getPageSize())
130+
.fetch();
131+
132+
// 결과가 없으면 즉시 빈 페이지 반환
133+
if (comments.isEmpty()) {
134+
return new PageImpl<>(comments, pageable, 0);
135+
}
136+
137+
// 4. 전체 댓글 수 조회
138+
long total = countComments(condition);
82139

83-
return new PageImpl<>(parents, pageable, total != null ? total : 0L);
140+
return new PageImpl<>(comments, pageable, total);
84141
}
85142

86143
// -------------------- 내부 메서드 --------------------
87144

145+
/**
146+
* 정렬 조건 생성
147+
* - Pageable의 Sort 정보를 QueryDSL OrderSpecifier 목록으로 변환
148+
*/
149+
private List<OrderSpecifier<?>> buildOrderSpecifiers(Pageable pageable) {
150+
QComment comment = QComment.comment;
151+
PathBuilder<Comment> entityPath = new PathBuilder<>(Comment.class, comment.getMetadata());
152+
List<OrderSpecifier<?>> orders = new ArrayList<>();
153+
154+
for (Sort.Order order : pageable.getSort()) {
155+
String property = order.getProperty();
156+
157+
// 화이트리스트에 포함된 필드만 허용
158+
if (!ALLOWED_SORT_FIELDS.contains(property)) {
159+
// 허용되지 않은 정렬 키는 무시 (런타임 예외 대신 안전하게 스킵)
160+
continue;
161+
}
162+
163+
Order direction = order.isAscending() ? Order.ASC : Order.DESC;
164+
orders.add(new OrderSpecifier<>(direction, entityPath.getComparable(property, Comparable.class)));
165+
}
166+
167+
// 명시된 정렬이 없으면 기본 정렬(createdAt DESC) 적용
168+
if (orders.isEmpty()) {
169+
orders.add(new OrderSpecifier<>(Order.DESC, comment.createdAt));
170+
}
171+
172+
return orders;
173+
}
174+
88175
/**
89176
* 댓글 조회
90177
* - User / UserProfile join (N+1 방지)
91-
*
92-
* @param condition where 조건
93-
* @param orders 정렬 조건
94-
* @param offset 페이징 offset (null이면 미적용)
95-
* @param limit 페이징 limit (null이면 미적용)
96178
*/
97179
private List<CommentListResponse> fetchComments(
98180
BooleanExpression condition,
@@ -147,32 +229,16 @@ private void mapChildrenToParents(List<CommentListResponse> parents, List<Commen
147229
}
148230

149231
/**
150-
* 정렬 조건 생성
151-
* - Pageable의 Sort 정보를 QueryDSL OrderSpecifier 목록으로 변환
232+
* 전체 댓글 개수 조회
233+
* - 단순 count 쿼리 1회
152234
*/
153-
private List<OrderSpecifier<?>> buildOrderSpecifiers(Pageable pageable) {
235+
private long countComments(BooleanExpression condition) {
154236
QComment comment = QComment.comment;
155-
PathBuilder<Comment> entityPath = new PathBuilder<>(Comment.class, comment.getMetadata());
156-
List<OrderSpecifier<?>> orders = new ArrayList<>();
157-
158-
for (Sort.Order order : pageable.getSort()) {
159-
String property = order.getProperty();
160-
161-
// 화이트리스트에 포함된 필드만 허용
162-
if (!ALLOWED_SORT_FIELDS.contains(property)) {
163-
// 허용되지 않은 정렬 키는 무시 (런타임 예외 대신 안전하게 스킵)
164-
continue;
165-
}
166-
167-
Order direction = order.isAscending() ? Order.ASC : Order.DESC;
168-
orders.add(new OrderSpecifier<>(direction, entityPath.getComparable(property, Comparable.class)));
169-
}
170-
171-
// 명시된 정렬이 없으면 기본 정렬(createdAt DESC) 적용
172-
if (orders.isEmpty()) {
173-
orders.add(new OrderSpecifier<>(Order.DESC, comment.createdAt));
174-
}
175-
176-
return orders;
237+
Long total = queryFactory
238+
.select(comment.count())
239+
.from(comment)
240+
.where(condition)
241+
.fetchOne();
242+
return total != null ? total : 0L;
177243
}
178244
}

src/main/java/com/back/domain/board/comment/service/CommentService.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.back.domain.board.comment.dto.CommentRequest;
55
import com.back.domain.board.comment.dto.CommentResponse;
66
import com.back.domain.board.comment.dto.ReplyResponse;
7-
import com.back.domain.board.comment.entity.CommentLike;
87
import com.back.domain.board.comment.repository.CommentLikeRepository;
98
import com.back.domain.board.common.dto.PageResponse;
109
import com.back.domain.board.comment.entity.Comment;
@@ -91,7 +90,7 @@ public PageResponse<CommentListResponse> getComments(Long postId, Pageable pagea
9190
.orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND));
9291

9392
// 댓글 목록 조회
94-
Page<CommentListResponse> comments = commentRepository.getCommentsByPostId(postId, pageable);
93+
Page<CommentListResponse> comments = commentRepository.findCommentsByPostId(postId, pageable);
9594

9695
return PageResponse.from(comments);
9796
}

src/main/java/com/back/domain/user/service/UserService.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ public PageResponse<PostListResponse> getMyPosts(Long userId, Pageable pageable)
156156
return PageResponse.from(page);
157157
}
158158

159-
// TODO: 내 댓글/북마크 목록 조회 N+1 발생 가능, 추후 리팩토링 필요
160159
/**
161160
* 내 댓글 목록 조회 서비스
162161
* 1. 사용자 조회 및 상태 검증
@@ -170,13 +169,13 @@ public PageResponse<MyCommentResponse> getMyComments(Long userId, Pageable pagea
170169
User user = getValidUser(userId);
171170

172171
// 댓글 목록 조회
173-
Page<MyCommentResponse> page = commentRepository.findAllByUserId(user.getId(), pageable)
174-
.map(MyCommentResponse::from);
172+
Page<MyCommentResponse> page = commentRepository.findCommentsByUserId(user.getId(), pageable);
175173

176174
// 페이지 응답 반환
177175
return PageResponse.from(page);
178176
}
179177

178+
// TODO: 내 북마크 목록 조회 N+1 발생 가능, 추후 리팩토링 필요
180179
/**
181180
* 내 북마크 게시글 목록 조회 서비스
182181
* 1. 사용자 조회 및 상태 검증

0 commit comments

Comments
 (0)