Skip to content

Commit 76134fa

Browse files
Refactor: board 도메인 DTO 및 Repository 개선 (#212) (#238)
* Ref: DTO 개선 * Ref: Comment Repository 개선 * Test: Comment/Repository 테스트 작성 * Feat&Ref: 게시글 검색 조건 및 Repository 개선 * Test: Post/Respository 테스트 작성 --------- Co-authored-by: loseminho <[email protected]>
1 parent e0c45d7 commit 76134fa

File tree

17 files changed

+589
-243
lines changed

17 files changed

+589
-243
lines changed

src/main/java/com/back/domain/board/comment/dto/CommentLikeResponse.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
/**
66
* 댓글 좋아요 응답 DTO
77
*
8-
* @param commentId
9-
* @param likeCount
8+
* @param commentId 댓글 ID
9+
* @param likeCount 좋아요 수
1010
*/
1111
public record CommentLikeResponse(
1212
Long commentId,

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
@@ -3,7 +3,7 @@
33
import jakarta.validation.constraints.NotBlank;
44

55
/**
6-
* 댓글 작성 및 수정을 위한 요청 DTO
6+
* 댓글 작성/수정 요청 DTO
77
*
88
* @param content 댓글 내용
99
*/

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
/**
99
* 댓글 응답 DTO
1010
*
11-
* @param commentId 댓글 Id
12-
* @param postId 게시글 Id
11+
* @param commentId 댓글 ID
12+
* @param postId 게시글 ID
1313
* @param author 작성자 정보
1414
* @param content 댓글 내용
1515
* @param createdAt 댓글 생성 일시

src/main/java/com/back/domain/board/comment/dto/ReplyResponse.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
/**
99
* 대댓글 응답 DTO
1010
*
11-
* @param commentId 댓글 Id
12-
* @param postId 게시글 Id
13-
* @param parentId 부모 댓글 Id
11+
* @param commentId 댓글 ID
12+
* @param postId 게시글 ID
13+
* @param parentId 부모 댓글 ID
1414
* @param author 작성자 정보
1515
* @param content 댓글 내용
1616
* @param createdAt 댓글 생성 일시

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@
44
import com.querydsl.jpa.impl.JPAQueryFactory;
55
import lombok.RequiredArgsConstructor;
66
import org.springframework.stereotype.Repository;
7+
import org.springframework.transaction.annotation.Transactional;
78

89
import java.util.Collection;
910
import java.util.List;
1011

11-
1212
@Repository
1313
@RequiredArgsConstructor
1414
public class CommentLikeRepositoryImpl implements CommentLikeRepositoryCustom {
15-
1615
private final JPAQueryFactory queryFactory;
1716

17+
/**
18+
* 댓글 ID 목록 중 사용자가 좋아요한 댓글 ID 조회
19+
* - 총 쿼리 수: 1회
20+
*
21+
* @param userId 사용자 ID
22+
* @param commentIds 댓글 ID 목록
23+
*/
1824
@Override
1925
public List<Long> findLikedCommentIdsIn(Long userId, Collection<Long> commentIds) {
2026
QCommentLike commentLike = QCommentLike.commentLike;

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

Lines changed: 45 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.back.domain.board.comment.dto.QCommentListResponse;
55
import com.back.domain.board.comment.entity.Comment;
66
import com.back.domain.board.comment.entity.QComment;
7-
import com.back.domain.board.comment.entity.QCommentLike;
87
import com.back.domain.board.common.dto.QAuthorResponse;
98
import com.back.domain.user.entity.QUser;
109
import com.back.domain.user.entity.QUserProfile;
@@ -16,71 +15,65 @@
1615
import com.querydsl.jpa.impl.JPAQueryFactory;
1716
import lombok.RequiredArgsConstructor;
1817
import org.springframework.data.domain.*;
18+
import org.springframework.stereotype.Repository;
19+
1920
import java.util.*;
2021
import java.util.stream.Collectors;
2122

23+
@Repository
2224
@RequiredArgsConstructor
2325
public class CommentRepositoryImpl implements CommentRepositoryCustom {
2426
private final JPAQueryFactory queryFactory;
2527

26-
// TODO: Comment에 likeCount 필드 추가에 따른 로직 개선
28+
private static final Set<String> ALLOWED_SORT_FIELDS = Set.of("createdAt", "updatedAt", "likeCount");
29+
2730
/**
28-
* 게시글 ID로 댓글 목록 조회
29-
* - 부모 댓글 페이징 + 자식 댓글 전체 조회
30-
* - likeCount는 부모/자식 댓글을 한 번에 조회 후 주입
31-
* - likeCount 정렬은 메모리에서 처리
32-
* -쿼리 수: 4회 (부모조회 + 자식조회 + likeCount + count)
31+
* 특정 게시글의 댓글 목록 조회
32+
* - 총 쿼리 수: 3회
33+
* 1.부모 댓글 목록을 페이징/정렬 조건으로 조회
34+
* 2.부모 ID 목록으로 자식 댓글 전체 조회
35+
* 3.부모건수(count) 조회
3336
*
34-
* @param postId 게시글 Id
37+
* @param postId 게시글 Id
3538
* @param pageable 페이징 + 정렬 조건
3639
*/
3740
@Override
3841
public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pageable) {
3942
QComment comment = QComment.comment;
40-
QCommentLike commentLike = QCommentLike.commentLike;
4143

42-
// 1. 정렬 조건 생성 (엔티티 필드 기반)
43-
List<OrderSpecifier<?>> orders = buildOrderSpecifiers(pageable, comment);
44+
// 1. 정렬 조건 생성
45+
List<OrderSpecifier<?>> orders = buildOrderSpecifiers(pageable);
4446

45-
// 2. 부모 댓글 조회 (페이징)
47+
// 2. 부모 댓글 조회 (페이징 적용)
4648
List<CommentListResponse> parents = fetchComments(
4749
comment.post.id.eq(postId).and(comment.parent.isNull()),
4850
orders,
4951
pageable.getOffset(),
5052
pageable.getPageSize()
5153
);
5254

55+
// 부모가 비어 있으면 즉시 빈 페이지 반환
5356
if (parents.isEmpty()) {
5457
return new PageImpl<>(parents, pageable, 0);
5558
}
5659

57-
// 3. 부모 ID 목록 수집
60+
// 3. 부모 ID 수집
5861
List<Long> parentIds = parents.stream()
5962
.map(CommentListResponse::getCommentId)
6063
.toList();
6164

62-
// 4. 자식 댓글 조회 (부모 ID 기준)
65+
// 4. 자식 댓글 조회 (부모 집합에 대한 전체 조회)
6366
List<CommentListResponse> children = fetchComments(
6467
comment.parent.id.in(parentIds),
65-
List.of(comment.createdAt.asc()),
68+
List.of(comment.createdAt.asc()), // 시간순 정렬
6669
null,
6770
null
6871
);
6972

70-
// 5. 부모 + 자식 댓글 ID 합쳐 likeCount 조회 (쿼리 1회)
71-
Map<Long, Long> likeCountMap = fetchLikeCounts(parentIds, children);
72-
73-
// 6. likeCount 주입
74-
parents.forEach(p -> p.setLikeCount(likeCountMap.getOrDefault(p.getCommentId(), 0L)));
75-
children.forEach(c -> c.setLikeCount(likeCountMap.getOrDefault(c.getCommentId(), 0L)));
76-
77-
// 7. 부모-자식 매핑
73+
// 5. 부모-자식 매핑
7874
mapChildrenToParents(parents, children);
7975

80-
// 8. 정렬 후처리 (통계 필드 기반)
81-
parents = sortInMemoryIfNeeded(parents, pageable);
82-
83-
// 9. 전체 부모 댓글 수 조회
76+
// 6. 전체 부모 댓글 수 조회
8477
Long total = queryFactory
8578
.select(comment.count())
8679
.from(comment)
@@ -95,7 +88,11 @@ public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pagea
9588
/**
9689
* 댓글 조회
9790
* - User / UserProfile join (N+1 방지)
98-
* - likeCount는 이후 주입
91+
*
92+
* @param condition where 조건
93+
* @param orders 정렬 조건
94+
* @param offset 페이징 offset (null이면 미적용)
95+
* @param limit 페이징 limit (null이면 미적용)
9996
*/
10097
private List<CommentListResponse> fetchComments(
10198
BooleanExpression condition,
@@ -105,7 +102,7 @@ private List<CommentListResponse> fetchComments(
105102
) {
106103
QComment comment = QComment.comment;
107104
QUser user = QUser.user;
108-
QUserProfile profile = QUserProfile.userProfile;
105+
QUserProfile profile = QUserProfile .userProfile;
109106

110107
var query = queryFactory
111108
.select(new QCommentListResponse(
@@ -114,8 +111,8 @@ private List<CommentListResponse> fetchComments(
114111
comment.parent.id,
115112
new QAuthorResponse(user.id, profile.nickname, profile.profileImageUrl),
116113
comment.content,
117-
Expressions.constant(0L), // likeCount는 별도 주입
118-
Expressions.constant(false),
114+
comment.likeCount,
115+
Expressions.constant(false), // likedByMe는 별도 주입
119116
comment.createdAt,
120117
comment.updatedAt,
121118
Expressions.constant(Collections.emptyList()) // children은 별도 주입
@@ -126,42 +123,17 @@ private List<CommentListResponse> fetchComments(
126123
.where(condition)
127124
.orderBy(orders.toArray(new OrderSpecifier[0]));
128125

126+
// 페이징 적용
129127
if (offset != null && limit != null) {
130128
query.offset(offset).limit(limit);
131129
}
132130

133131
return query.fetch();
134132
}
135133

136-
/**
137-
* likeCount 일괄 조회
138-
* - IN 조건 기반 groupBy 쿼리 1회
139-
* - 부모/자식 댓글을 한 번에 조회
140-
*/
141-
private Map<Long, Long> fetchLikeCounts(List<Long> parentIds, List<CommentListResponse> children) {
142-
QCommentLike commentLike = QCommentLike.commentLike;
143-
144-
List<Long> allIds = new ArrayList<>(parentIds);
145-
allIds.addAll(children.stream().map(CommentListResponse::getCommentId).toList());
146-
147-
if (allIds.isEmpty()) return Map.of();
148-
149-
return queryFactory
150-
.select(commentLike.comment.id, commentLike.count())
151-
.from(commentLike)
152-
.where(commentLike.comment.id.in(allIds))
153-
.groupBy(commentLike.comment.id)
154-
.fetch()
155-
.stream()
156-
.collect(Collectors.toMap(
157-
tuple -> tuple.get(commentLike.comment.id),
158-
tuple -> tuple.get(commentLike.count())
159-
));
160-
}
161-
162134
/**
163135
* 부모/자식 관계 매핑
164-
* - childMap을 parentId 기준으로 그룹화 후 children 필드에 set
136+
* - 자식 목록을 parentId 기준으로 그룹화 후, 각 부모 DTO의 children에 설정
165137
*/
166138
private void mapChildrenToParents(List<CommentListResponse> parents, List<CommentListResponse> children) {
167139
if (children.isEmpty()) return;
@@ -170,53 +142,37 @@ private void mapChildrenToParents(List<CommentListResponse> parents, List<Commen
170142
.collect(Collectors.groupingBy(CommentListResponse::getParentId));
171143

172144
parents.forEach(parent ->
173-
parent.setChildren(childMap.getOrDefault(parent.getCommentId(), List.of()))
145+
parent.setChildren(childMap.getOrDefault(parent.getCommentId(), Collections.emptyList()))
174146
);
175147
}
176148

177149
/**
178-
* 정렬 처리 (DB 정렬)
179-
* - createdAt, updatedAt 등 엔티티 필드
150+
* 정렬 조건 생성
151+
* - Pageable의 Sort 정보를 QueryDSL OrderSpecifier 목록으로 변환
180152
*/
181-
private List<OrderSpecifier<?>> buildOrderSpecifiers(Pageable pageable, QComment comment) {
153+
private List<OrderSpecifier<?>> buildOrderSpecifiers(Pageable pageable) {
154+
QComment comment = QComment.comment;
182155
PathBuilder<Comment> entityPath = new PathBuilder<>(Comment.class, comment.getMetadata());
183156
List<OrderSpecifier<?>> orders = new ArrayList<>();
184157

185158
for (Sort.Order order : pageable.getSort()) {
186-
String prop = order.getProperty();
159+
String property = order.getProperty();
187160

188-
// 통계 필드는 메모리 정렬에서 처리
189-
if (prop.equals("likeCount")) {
161+
// 화이트리스트에 포함된 필드만 허용
162+
if (!ALLOWED_SORT_FIELDS.contains(property)) {
163+
// 허용되지 않은 정렬 키는 무시 (런타임 예외 대신 안전하게 스킵)
190164
continue;
191165
}
166+
192167
Order direction = order.isAscending() ? Order.ASC : Order.DESC;
193-
orders.add(new OrderSpecifier<>(direction, entityPath.getComparable(prop, Comparable.class)));
168+
orders.add(new OrderSpecifier<>(direction, entityPath.getComparable(property, Comparable.class)));
194169
}
195170

196-
return orders;
197-
}
198-
199-
/**
200-
* 통계 기반 정렬 처리 (메모리)
201-
* - likeCount 등 통계 필드
202-
* - 페이지 단위라 성능에 영향 없음
203-
*/
204-
private List<CommentListResponse> sortInMemoryIfNeeded(List<CommentListResponse> results, Pageable pageable) {
205-
if (results.isEmpty() || !pageable.getSort().isSorted()) return results;
206-
207-
for (Sort.Order order : pageable.getSort()) {
208-
Comparator<CommentListResponse> comparator = null;
209-
210-
if ("likeCount".equals(order.getProperty())) {
211-
comparator = Comparator.comparing(CommentListResponse::getLikeCount);
212-
}
213-
214-
if (comparator != null) {
215-
if (order.isDescending()) comparator = comparator.reversed();
216-
results.sort(comparator);
217-
}
171+
// 명시된 정렬이 없으면 기본 정렬(createdAt DESC) 적용
172+
if (orders.isEmpty()) {
173+
orders.add(new OrderSpecifier<>(Order.DESC, comment.createdAt));
218174
}
219175

220-
return results;
176+
return orders;
221177
}
222178
}

src/main/java/com/back/domain/board/post/controller/PostController.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import org.springframework.security.core.annotation.AuthenticationPrincipal;
2020
import org.springframework.web.bind.annotation.*;
2121

22+
import java.util.List;
23+
2224
@RestController
2325
@RequestMapping("/api/posts")
2426
@RequiredArgsConstructor
@@ -46,7 +48,7 @@ public ResponseEntity<RsData<PageResponse<PostListResponse>>> getPosts(
4648
@PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
4749
@RequestParam(required = false) String keyword,
4850
@RequestParam(required = false) String searchType,
49-
@RequestParam(required = false) Long categoryId
51+
@RequestParam(required = false) List<Long> categoryId
5052
) {
5153
PageResponse<PostListResponse> response = postService.getPosts(keyword, searchType, categoryId, pageable);
5254
return ResponseEntity

src/main/java/com/back/domain/board/post/controller/docs/PostControllerDocs.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import org.springframework.web.bind.annotation.RequestBody;
2222
import org.springframework.web.bind.annotation.RequestParam;
2323

24+
import java.util.List;
25+
2426
@Tag(name = "Post API", description = "게시글 관련 API")
2527
public interface PostControllerDocs {
2628

@@ -229,7 +231,7 @@ ResponseEntity<RsData<PageResponse<PostListResponse>>> getPosts(
229231
@PageableDefault(sort = "createdAt") Pageable pageable,
230232
@RequestParam(required = false) String keyword,
231233
@RequestParam(required = false) String searchType,
232-
@RequestParam(required = false) Long categoryId
234+
@RequestParam(required = false) List<Long> categoryId
233235
);
234236

235237

src/main/java/com/back/domain/board/post/dto/PostBookmarkResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
/**
66
* 게시글 북마크 응답 DTO
77
*
8-
* @param postId 게시글 id
8+
* @param postId 게시글 ID
99
* @param bookmarkCount 북마크 수
1010
*/
1111
public record PostBookmarkResponse(

src/main/java/com/back/domain/board/post/dto/PostLikeResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
/**
66
* 게시글 좋아요 응답 DTO
77
*
8-
* @param postId 게시글 id
8+
* @param postId 게시글 ID
99
* @param likeCount 좋아요 수
1010
*/
1111
public record PostLikeResponse(

0 commit comments

Comments
 (0)