-
리스트 조회 시 페이징으로 특정 데이터만 조회프로젝트/법잘알 2024. 2. 23. 20:57
리스트를 조회할 때 fetch join을 사용했다 하더라도 시간이 많이 소요된다. 저번 포스팅에서 데이터를 임의로 30만개를 생성했다고 했는데, 30만개를 실제로 한 번에 get할 일은 거의 없다.
때문에 페이징을 진행하여 원하는 갯수만큼의 데이터를 가져오게 되는데 페이징을 할 때 계속 시간이 오래 걸리는 문제가 생겼다.
@Override public Page<Suggestion> findAllSuggestion(Category category, Boolean likeCount, Pageable pageable) { JPAQuery<Suggestion> query = queryFactory .selectFrom(suggestion) .leftJoin(suggestion.user, user).fetchJoin() // fetch join 제거 .leftJoin(suggestion.likeList, like).fetchJoin() // fetch join 제거 .where( categoryEq(category), likeCountEq(likeCount) ); List<Suggestion> content = query .orderBy(likeCount != null && likeCount ? suggestion.likeList.size().desc() : suggestion.createdDate.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); long total = query.fetchCount(); return new PageImpl<>(content, pageable, total); }
위의 코드를 수행할 때 페이지에는 5개의 suggestion만 조회되도록 요청했는데도 7초나 걸리는 것이었다. 그 이유는 fetchJoin에 있었다. 페이징으로 변경하기 전에 fetch join을 했었는데, 페이징은 원하는 페이지의 데이터만 가져오기 때문에 fetch join을 하면 필요 없는 데이터를 전부 가져오는 것이다.
fetch join을 제거했더니 2초가 걸렸다. 이전 포스팅에서 30만개의 데이터를 모두 조회할 때는 1분이 넘게 걸렸고, fetch join을 했더니 7초로 줄었지만 그래도 여전히 오래 걸렸다. 단순히 5개의 게시글 리스트를 조회하는데 2초나 걸리는건 매우 심각한 상황이기에 문제점을 찾았다.
문제점은 suggestion.likeList.size()에 있었다. suggestion의 likeList.size() 로직을 돌면서 likeList 테이블을 스캔해서 count 하는 방식이기 때문에 시간이 오래 걸렸다.
해결
@Entity @Getter @NoArgsConstructor public class Suggestion extends BaseEntity { @Id @GeneratedValue(strategy = IDENTITY) private Long id; private String title; private String content; @Enumerated(STRING) private Category category; @ManyToOne(fetch = LAZY) @JoinColumn(name = "user_id") private User user; @OneToMany(mappedBy = "suggestion", cascade = ALL) private List<Like> likeList = new ArrayList<>(); private Long likeCount; // 추가 @Builder public Suggestion(String title, String content, Category category, User user) { this.title = title; this.content = content; this.category = category; this.user = user; } public void addLike(Like like) { likeList.add(like); likeCount++; // 추가 } public void deleteLike(Like like) { likeList.remove(like); likeCount--; // 추가 } }
해결은 Suggestion 엔티티에 likeCount라는 컬럼을 추가한 뒤 addLike와 deleteLike를 수행할 때 likeCount의 수를 조절해주면 되는 것이었다.
public class SuggestionRepositoryImpl implements SuggestionRepositoryCustom { private final JPAQueryFactory queryFactory; public SuggestionRepositoryImpl(EntityManager em) { this.queryFactory = new JPAQueryFactory(em); } @Override public Page<Suggestion> findAllSuggestion(Category category, Boolean likeCount, Pageable pageable) { JPAQuery<Suggestion> query = queryFactory .selectFrom(suggestion) .leftJoin(suggestion.user, user) .where( categoryEq(category), likeCountEq(likeCount) ); List<Suggestion> content = query .orderBy(likeCount != null && likeCount ? suggestion.likeCount.desc() : suggestion.likeCount.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); Long total = query.fetchCount(); return new PageImpl<>(content, pageable, total); } private BooleanExpression categoryEq(Category category) { return category != null ? suggestion.category.eq(category) : null; } private BooleanExpression likeCountEq(Boolean likeCount) { if (likeCount != null && likeCount) { return suggestion.likeList.isNotEmpty(); } return null; } }
전체적인 쿼리도 위와 같이 깔끔하게 변경해주었다.
위의 사진같이 page 1번의 10개를 조회한 응답 시간이 65ms로 많이 줄어든 것을 확인했다.
지금까지는 개발을 완료하고 잘 되면 쿼리를 확인해서 N+1 문제가 발생하면 fetch join을 하는 것으로만 해결했는데, 이러면 실제 사용되지 못하는 서비스였다. 데이터를 30만개 넣어서 페이징해본 경험을 바탕으로 앞으로도 서비스를 개발할 때 실제 일어날 수 있는 상황을 대비해두고 테스트 해보며 개발해야겠다고 생각했다.
모든 상황에서 쿼리를 적게 날리기 위해 fetch join을 사용하지 않는다. 페이징을 할 때에는 fetch join을 사용 하지 않는게 훨씬 낫다는 것을 깨달았다...
(조금만 생각하면 되는 간단한 문제였는데 페이징 이전에 사용한 fetch join을 당연하게 생각하다가 2~3시간을 날렸다......)
'프로젝트 > 법잘알' 카테고리의 다른 글
조회 시 N+1 문제 해결로 쿼리 최적화 (0) 2024.02.23 영속성 컨텍스트 분리로 인한 비밀번호 수정 불가 (0) 2024.02.11 클린코드와 관심사의 분리 (0) 2024.02.04 ai 활용 - 텍스트 분석 라이브러리와 생성형 ai의 사용 (0) 2024.02.02 스프링 bean 순환참조 에러 (0) 2024.01.28