Skip to content

Commit 0379dfb

Browse files
committed
Ref: 내 북마크 게시글 목록 조회 Query DSL 기반 개선
1 parent d833021 commit 0379dfb

File tree

5 files changed

+154
-25
lines changed

5 files changed

+154
-25
lines changed

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

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.back.domain.board.post.dto;
22

33
import com.back.domain.board.common.dto.AuthorResponse;
4-
import com.back.domain.board.post.entity.Post;
54
import com.querydsl.core.annotations.QueryProjection;
65
import lombok.Getter;
76
import lombok.Setter;
@@ -50,21 +49,4 @@ public PostListResponse(Long postId,
5049
this.createdAt = createdAt;
5150
this.updatedAt = updatedAt;
5251
}
53-
54-
public static PostListResponse from(Post post) {
55-
return new PostListResponse(
56-
post.getId(),
57-
AuthorResponse.from(post.getUser()),
58-
post.getTitle(),
59-
post.getThumbnailUrl(),
60-
post.getCategories().stream()
61-
.map(CategoryResponse::from)
62-
.toList(),
63-
post.getLikeCount(),
64-
post.getBookmarkCount(),
65-
post.getCommentCount(),
66-
post.getCreatedAt(),
67-
post.getUpdatedAt()
68-
);
69-
}
7052
}

src/main/java/com/back/domain/board/post/repository/custom/PostRepositoryCustom.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
public interface PostRepositoryCustom {
1010
Page<PostListResponse> searchPosts(String keyword, String searchType, List<Long> categoryIds, Pageable pageable);
1111
Page<PostListResponse> findPostsByUserId(Long userId, Pageable pageable);
12+
Page<PostListResponse> findBookmarkedPostsByUserId(Long userId, Pageable pageable);
1213
}

src/main/java/com/back/domain/board/post/repository/custom/PostRepositoryImpl.java

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public Page<PostListResponse> searchPosts(String keyword, String searchType, Lis
7373
}
7474

7575
/**
76-
* 게시글 목록 조회
76+
* 특정 사용자의 게시글 목록 조회
7777
* - 총 쿼리 수: 3회
7878
* 1. 게시글 목록 조회 (User, UserProfile join)
7979
* 2. 카테고리 목록 조회 (IN 쿼리)
@@ -109,6 +109,67 @@ public Page<PostListResponse> findPostsByUserId(Long userId, Pageable pageable)
109109
return new PageImpl<>(posts, pageable, total);
110110
}
111111

112+
/**
113+
* 특정 사용자의 북마크 게시글 목록 조회
114+
* - 총 쿼리 수: 3회
115+
* 1. 북마크된 게시글 목록 조회 (Post, User, UserProfile join)
116+
* 2. 카테고리 목록 조회 (IN 쿼리)
117+
* 3. 전체 count 조회
118+
*
119+
* @param userId 사용자 ID
120+
* @param pageable 페이징 + 정렬 조건
121+
*/
122+
@Override
123+
public Page<PostListResponse> findBookmarkedPostsByUserId(Long userId, Pageable pageable) {
124+
QPost post = QPost.post;
125+
QPostBookmark bookmark = QPostBookmark.postBookmark;
126+
QUser user = QUser.user;
127+
QUserProfile profile = QUserProfile.userProfile;
128+
129+
// 1. 검색 조건 생성
130+
BooleanBuilder where = new BooleanBuilder(bookmark.user.id.eq(userId));
131+
132+
// 2. 정렬 조건 생성 (화이트리스트 기반)
133+
List<OrderSpecifier<?>> orders = buildOrderSpecifiers(pageable);
134+
135+
// 3. 북마크된 게시글 목록 조회 (Post, User, UserProfile join으로 N+1 방지)
136+
List<PostListResponse> posts = queryFactory
137+
.select(new QPostListResponse(
138+
post.id,
139+
new QAuthorResponse(post.user.id, post.user.userProfile.nickname, post.user.userProfile.profileImageUrl),
140+
post.title,
141+
post.thumbnailUrl,
142+
Expressions.constant(Collections.emptyList()), // categories는 별도 주입
143+
post.likeCount,
144+
post.bookmarkCount,
145+
post.commentCount,
146+
post.createdAt,
147+
post.updatedAt
148+
))
149+
.from(bookmark)
150+
.join(bookmark.post, post)
151+
.leftJoin(post.user, user)
152+
.leftJoin(user.userProfile, profile)
153+
.where(where)
154+
.orderBy(orders.toArray(new OrderSpecifier[0]))
155+
.offset(pageable.getOffset())
156+
.limit(pageable.getPageSize())
157+
.fetch();
158+
159+
// 결과가 없으면 즉시 빈 페이지 반환
160+
if (posts.isEmpty()) {
161+
return new PageImpl<>(posts, pageable, 0);
162+
}
163+
164+
// 4. 카테고리 목록 주입 (postIds 기반 IN 쿼리 1회)
165+
injectCategories(posts);
166+
167+
// 5. 전체 북마크 게시글 수 조회
168+
long total = countBookmarkedPosts(userId);
169+
170+
return new PageImpl<>(posts, pageable, total);
171+
}
172+
112173
// -------------------- 내부 메서드 --------------------
113174

114175
/**
@@ -287,4 +348,18 @@ private long countPosts(BooleanBuilder where) {
287348
.fetchOne();
288349
return total != null ? total : 0L;
289350
}
351+
352+
/**
353+
* 내 북마크 게시글 총 개수 조회
354+
* - 단순 count 쿼리 1회
355+
*/
356+
private long countBookmarkedPosts(Long userId) {
357+
QPostBookmark bookmark = QPostBookmark.postBookmark;
358+
Long total = queryFactory
359+
.select(bookmark.count())
360+
.from(bookmark)
361+
.where(bookmark.user.id.eq(userId))
362+
.fetchOne();
363+
return total != null ? total : 0L;
364+
}
290365
}

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.back.domain.board.comment.repository.CommentRepository;
55
import com.back.domain.board.common.dto.PageResponse;
66
import com.back.domain.board.post.dto.PostListResponse;
7-
import com.back.domain.board.post.repository.PostBookmarkRepository;
87
import com.back.domain.board.post.repository.PostRepository;
98
import com.back.domain.user.dto.ChangePasswordRequest;
109
import com.back.domain.user.dto.UpdateUserProfileRequest;
@@ -34,7 +33,6 @@ public class UserService {
3433
private final UserProfileRepository userProfileRepository;
3534
private final CommentRepository commentRepository;
3635
private final PostRepository postRepository;
37-
private final PostBookmarkRepository postBookmarkRepository;
3836
private final PasswordEncoder passwordEncoder;
3937

4038
/**
@@ -175,7 +173,6 @@ public PageResponse<MyCommentResponse> getMyComments(Long userId, Pageable pagea
175173
return PageResponse.from(page);
176174
}
177175

178-
// TODO: 내 북마크 목록 조회 N+1 발생 가능, 추후 리팩토링 필요
179176
/**
180177
* 내 북마크 게시글 목록 조회 서비스
181178
* 1. 사용자 조회 및 상태 검증
@@ -189,8 +186,7 @@ public PageResponse<PostListResponse> getMyBookmarks(Long userId, Pageable pagea
189186
User user = getValidUser(userId);
190187

191188
// 북마크된 게시글 조회
192-
Page<PostListResponse> page = postBookmarkRepository.findAllByUserId(user.getId(), pageable)
193-
.map(bookmark -> PostListResponse.from(bookmark.getPost()));
189+
Page<PostListResponse> page = postRepository.findBookmarkedPostsByUserId(user.getId(), pageable);
194190

195191
// 페이지 응답 반환
196192
return PageResponse.from(page);

src/test/java/com/back/domain/board/post/repository/custom/PostRepositoryImplTest.java

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
import com.back.domain.board.post.dto.PostListResponse;
44
import com.back.domain.board.post.entity.*;
55
import com.back.domain.board.post.enums.CategoryType;
6+
import com.back.domain.board.post.repository.PostBookmarkRepository;
67
import com.back.domain.board.post.repository.PostRepository;
78
import com.back.domain.board.post.repository.PostCategoryRepository;
89
import com.back.domain.user.entity.User;
910
import com.back.domain.user.entity.UserProfile;
1011
import com.back.domain.user.entity.UserStatus;
1112
import com.back.domain.user.repository.UserRepository;
1213
import com.back.global.config.QueryDslConfig;
14+
import jakarta.persistence.EntityManager;
15+
import jakarta.persistence.PersistenceContext;
16+
import org.junit.jupiter.api.AfterEach;
1317
import org.junit.jupiter.api.BeforeEach;
1418
import org.junit.jupiter.api.DisplayName;
1519
import org.junit.jupiter.api.Test;
@@ -34,6 +38,9 @@ class PostRepositoryImplTest {
3438
@Autowired
3539
private PostRepository postRepository;
3640

41+
@Autowired
42+
private PostBookmarkRepository postBookmarkRepository;
43+
3744
@Autowired
3845
private UserRepository userRepository;
3946

@@ -145,7 +152,7 @@ void searchPosts_empty() {
145152
assertThat(page.getContent()).isEmpty();
146153
}
147154

148-
// ====================== 게시글 목록 조회 테스트 ======================
155+
// ====================== 특정 사용자의 게시글 목록 조회 테스트 ======================
149156

150157
@Test
151158
@DisplayName("특정 사용자의 게시글 목록 페이징 조회")
@@ -229,4 +236,72 @@ void findPostsByUserId_noCategory() {
229236

230237
assertThat(target.getCategories()).isEmpty();
231238
}
239+
240+
// ====================== 특정 사용자의 북마크 게시글 목록 조회 테스트 ======================
241+
242+
@Test
243+
@DisplayName("특정 사용자의 북마크 게시글 목록 정상 조회")
244+
void findBookmarkedPostsByUserId_basic() {
245+
// given
246+
PostBookmark b1 = new PostBookmark(post1, user);
247+
PostBookmark b2 = new PostBookmark(post3, user);
248+
postBookmarkRepository.saveAll(List.of(b1, b2));
249+
250+
PageRequest pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdAt"));
251+
252+
// when
253+
Page<PostListResponse> page = postRepository.findBookmarkedPostsByUserId(user.getId(), pageable);
254+
255+
// then
256+
assertThat(page.getTotalElements()).isEqualTo(2);
257+
assertThat(page.getContent()).hasSize(2);
258+
259+
// 최신순 정렬 확인
260+
assertThat(page.getContent().get(0).getTitle()).isEqualTo("10대 대상 스터디");
261+
assertThat(page.getContent().get(1).getTitle()).isEqualTo("수학 공부 팁");
262+
263+
// 작성자 정보 확인
264+
PostListResponse first = page.getContent().getFirst();
265+
assertThat(first.getAuthor().nickname()).isEqualTo("작성자");
266+
267+
// 카테고리 목록 주입 검증
268+
assertThat(first.getCategories()).isNotEmpty();
269+
assertThat(first.getCategories())
270+
.extracting("name")
271+
.containsAnyOf("10대", "2인", "수학");
272+
}
273+
274+
@Test
275+
@DisplayName("사용자가 북마크한 게시글이 없으면 빈 페이지 반환")
276+
void findBookmarkedPostsByUserId_empty() {
277+
// given
278+
PageRequest pageable = PageRequest.of(0, 10);
279+
280+
// when
281+
Page<PostListResponse> page = postRepository.findBookmarkedPostsByUserId(user.getId(), pageable);
282+
283+
// then
284+
assertThat(page.getTotalElements()).isZero();
285+
assertThat(page.getContent()).isEmpty();
286+
}
287+
288+
@Test
289+
@DisplayName("정렬 조건(createdAt DESC)이 올바르게 적용")
290+
void findBookmarkedPostsByUserId_sorting() {
291+
// given
292+
PostBookmark b1 = new PostBookmark(post1, user);
293+
PostBookmark b2 = new PostBookmark(post2, user);
294+
PostBookmark b3 = new PostBookmark(post3, user);
295+
postBookmarkRepository.saveAll(List.of(b1, b2, b3));
296+
297+
PageRequest pageable = PageRequest.of(0, 2, Sort.by(Sort.Direction.DESC, "createdAt"));
298+
299+
// when
300+
Page<PostListResponse> page = postRepository.findBookmarkedPostsByUserId(user.getId(), pageable);
301+
302+
// then
303+
assertThat(page.getContent()).hasSize(2);
304+
assertThat(page.getContent().get(0).getTitle()).isEqualTo("10대 대상 스터디");
305+
assertThat(page.getContent().get(1).getTitle()).isEqualTo("과학 토론 모집");
306+
}
232307
}

0 commit comments

Comments
 (0)