Skip to content

Commit b811177

Browse files
committed
Rename: board 도메인 구조 개선
1 parent e8b23dc commit b811177

21 files changed

+61
-102
lines changed

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

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

3+
import com.back.domain.board.comment.controller.docs.CommentControllerDocs;
34
import com.back.domain.board.comment.dto.CommentListResponse;
45
import com.back.domain.board.comment.dto.CommentRequest;
56
import com.back.domain.board.comment.dto.CommentResponse;

src/main/java/com/back/domain/board/comment/controller/CommentLikeController.java

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

3+
import com.back.domain.board.comment.controller.docs.CommentLikeControllerDocs;
34
import com.back.domain.board.comment.dto.CommentLikeResponse;
45
import com.back.domain.board.comment.service.CommentLikeService;
56
import com.back.global.common.dto.RsData;

src/main/java/com/back/domain/board/comment/controller/CommentControllerDocs.java renamed to src/main/java/com/back/domain/board/comment/controller/docs/CommentControllerDocs.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.comment.controller;
1+
package com.back.domain.board.comment.controller.docs;
22

33
import com.back.domain.board.comment.dto.CommentListResponse;
44
import com.back.domain.board.comment.dto.CommentRequest;

src/main/java/com/back/domain/board/comment/controller/CommentLikeControllerDocs.java renamed to src/main/java/com/back/domain/board/comment/controller/docs/CommentLikeControllerDocs.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.comment.controller;
1+
package com.back.domain.board.comment.controller.docs;
22

33
import com.back.domain.board.comment.dto.CommentLikeResponse;
44
import com.back.global.common.dto.RsData;

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package com.back.domain.board.comment.repository;
22

33
import com.back.domain.board.comment.entity.CommentLike;
4+
import com.back.domain.board.comment.repository.custom.CommentLikeRepositoryCustom;
45
import org.springframework.data.jpa.repository.JpaRepository;
5-
import org.springframework.data.jpa.repository.Query;
6-
import org.springframework.data.repository.query.Param;
76
import org.springframework.stereotype.Repository;
87

9-
import java.util.Collection;
10-
import java.util.List;
118
import java.util.Optional;
129

1310
@Repository

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

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

33
import com.back.domain.board.comment.entity.Comment;
4+
import com.back.domain.board.comment.repository.custom.CommentRepositoryCustom;
45
import org.springframework.data.jpa.repository.JpaRepository;
56
import org.springframework.stereotype.Repository;
67

src/main/java/com/back/domain/board/comment/repository/CommentLikeRepositoryCustom.java renamed to src/main/java/com/back/domain/board/comment/repository/custom/CommentLikeRepositoryCustom.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.comment.repository;
1+
package com.back.domain.board.comment.repository.custom;
22

33
import java.util.Collection;
44
import java.util.List;

src/main/java/com/back/domain/board/comment/repository/CommentLikeRepositoryImpl.java renamed to src/main/java/com/back/domain/board/comment/repository/custom/CommentLikeRepositoryImpl.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.comment.repository;
1+
package com.back.domain.board.comment.repository.custom;
22

33
import com.back.domain.board.comment.entity.QCommentLike;
44
import com.querydsl.jpa.impl.JPAQueryFactory;

src/main/java/com/back/domain/board/comment/repository/CommentRepositoryCustom.java renamed to src/main/java/com/back/domain/board/comment/repository/custom/CommentRepositoryCustom.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.comment.repository;
1+
package com.back.domain.board.comment.repository.custom;
22

33
import com.back.domain.board.comment.dto.CommentListResponse;
44
import org.springframework.data.domain.Page;
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
package com.back.domain.board.comment.repository;
1+
package com.back.domain.board.comment.repository.custom;
22

33
import com.back.domain.board.comment.dto.CommentListResponse;
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,63 @@
1615
import com.querydsl.jpa.impl.JPAQueryFactory;
1716
import lombok.RequiredArgsConstructor;
1817
import org.springframework.data.domain.*;
18+
1919
import java.util.*;
2020
import java.util.stream.Collectors;
2121

2222
@RequiredArgsConstructor
2323
public class CommentRepositoryImpl implements CommentRepositoryCustom {
2424
private final JPAQueryFactory queryFactory;
2525

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

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

45-
// 2. 부모 댓글 조회 (페이징)
45+
// 2. 부모 댓글 조회 (페이징 적용)
4646
List<CommentListResponse> parents = fetchComments(
4747
comment.post.id.eq(postId).and(comment.parent.isNull()),
4848
orders,
4949
pageable.getOffset(),
5050
pageable.getPageSize()
5151
);
5252

53+
// 부모가 비어 있으면 즉시 빈 페이지 반환
5354
if (parents.isEmpty()) {
5455
return new PageImpl<>(parents, pageable, 0);
5556
}
5657

57-
// 3. 부모 ID 목록 수집
58+
// 3. 부모 ID 수집
5859
List<Long> parentIds = parents.stream()
5960
.map(CommentListResponse::getCommentId)
6061
.toList();
6162

62-
// 4. 자식 댓글 조회 (부모 ID 기준)
63+
// 4. 자식 댓글 조회 (부모 집합에 대한 전체 조회)
6364
List<CommentListResponse> children = fetchComments(
6465
comment.parent.id.in(parentIds),
65-
List.of(comment.createdAt.asc()),
66+
List.of(comment.createdAt.asc()), // 시간순 정렬
6667
null,
6768
null
6869
);
6970

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. 부모-자식 매핑
71+
// 5. 부모-자식 매핑
7872
mapChildrenToParents(parents, children);
7973

80-
// 8. 정렬 후처리 (통계 필드 기반)
81-
parents = sortInMemoryIfNeeded(parents, pageable);
82-
83-
// 9. 전체 부모 댓글 수 조회
74+
// 6. 전체 부모 댓글 수 조회
8475
Long total = queryFactory
8576
.select(comment.count())
8677
.from(comment)
@@ -95,7 +86,11 @@ public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pagea
9586
/**
9687
* 댓글 조회
9788
* - User / UserProfile join (N+1 방지)
98-
* - likeCount는 이후 주입
89+
*
90+
* @param condition where 조건
91+
* @param orders 정렬 조건
92+
* @param offset 페이징 offset (null이면 미적용)
93+
* @param limit 페이징 limit (null이면 미적용)
9994
*/
10095
private List<CommentListResponse> fetchComments(
10196
BooleanExpression condition,
@@ -126,42 +121,17 @@ private List<CommentListResponse> fetchComments(
126121
.where(condition)
127122
.orderBy(orders.toArray(new OrderSpecifier[0]));
128123

124+
// 페이징 적용
129125
if (offset != null && limit != null) {
130126
query.offset(offset).limit(limit);
131127
}
132128

133129
return query.fetch();
134130
}
135131

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-
162132
/**
163133
* 부모/자식 관계 매핑
164-
* - childMap을 parentId 기준으로 그룹화 후 children 필드에 set
134+
* - 자식 목록을 parentId 기준으로 그룹화 후, 각 부모 DTO의 children에 설정
165135
*/
166136
private void mapChildrenToParents(List<CommentListResponse> parents, List<CommentListResponse> children) {
167137
if (children.isEmpty()) return;
@@ -170,53 +140,37 @@ private void mapChildrenToParents(List<CommentListResponse> parents, List<Commen
170140
.collect(Collectors.groupingBy(CommentListResponse::getParentId));
171141

172142
parents.forEach(parent ->
173-
parent.setChildren(childMap.getOrDefault(parent.getCommentId(), List.of()))
143+
parent.setChildren(childMap.getOrDefault(parent.getCommentId(), Collections.emptyList()))
174144
);
175145
}
176146

177147
/**
178-
* 정렬 처리 (DB 정렬)
179-
* - createdAt, updatedAt 등 엔티티 필드
148+
* 정렬 조건 생성
149+
* - Pageable의 Sort 정보를 QueryDSL OrderSpecifier 목록으로 변환
180150
*/
181-
private List<OrderSpecifier<?>> buildOrderSpecifiers(Pageable pageable, QComment comment) {
151+
private List<OrderSpecifier<?>> buildOrderSpecifiers(Pageable pageable) {
152+
QComment comment = QComment.comment;
182153
PathBuilder<Comment> entityPath = new PathBuilder<>(Comment.class, comment.getMetadata());
183154
List<OrderSpecifier<?>> orders = new ArrayList<>();
184155

185156
for (Sort.Order order : pageable.getSort()) {
186-
String prop = order.getProperty();
157+
String property = order.getProperty();
187158

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

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-
}
169+
// 명시된 정렬이 없으면 기본 정렬(createdAt DESC) 적용
170+
if (orders.isEmpty()) {
171+
orders.add(new OrderSpecifier<>(Order.DESC, comment.createdAt));
218172
}
219173

220-
return results;
174+
return orders;
221175
}
222176
}

0 commit comments

Comments
 (0)