JPA를 사용하여 페이징 처리를 할 때 Slice와 Page 두 가지 방법이 있다.
Slice<Entity>
JPA와 QueryDSL을 사용하여 페이징 처리를 할 때 Slice<Entity>를 반환하는 방법은 전체 결과의 개수를 쿼리하지 않아도 되는 특별한 접근 방식이다.
이 방식은 페이징 처리 시 필요한 데이터의 일부만 로드하면서 다음 페이지가 존재하는지 여부만을 알려주는 방법이다. 즉, 사용자가 요청한 페이지와 그다음 페이지의 데이터만 조회하여 현재 페이지 다음에 데이터가 더 있는지 여부를 판단한다.
Slice<Entity>의 동작 방식
Slice<Entity>는 페이징 처리된 데이터 목록과 함께 다음 페이지의 존재 여부를 알려주는 방식으로, 전체 데이터의 수를 계산하지 않기 때문에 count 쿼리를 실행할 필요가 없다. 이는 특히 대용량 데이터를 다룰 때 성능상의 이점을 제공한다. 전체 개수를 구하는 count 쿼리는 때때로 전체 데이터를 스캔해야 하기 때문에 비용이 많이 들 수 있다.
사용 예제
예를 들어, 어떤 소셜 미디어 플랫폼의 글 목록을 페이지별로 보여주는 기능을 개발한다고 가정해 보자.
사용자는 글 목록을 무한 스크롤하는 방식으로 볼 수 있으며, 서버는 사용자가 현재 보고 있는 위치에서 다음 페이지에 해당하는 글 목록만을 제공하면 된다.
💡 무한 스크롤: 사용자가 페이지의 끝에 도달할 때마다 다음 페이지의 내용을 자동으로 불러오는 방식으로, 이러한 경우에는 전체 페이지 수나 전체 데이터 개수를 미리 알 필요가 없다. 따라서 `Slice`를 통해 다음 페이지의 존재 여부만 파악하면 충분하며, 이는 성능 최적화와 개발의 편리성 측면에서 큰 이점을 제공한다.
- JPA 사용
// 글 목록 조회를 위한 Repository 인터페이스에 정의된 메소드
Slice<Post> findPostsBy(Pageable pageable);
- QueryDSL 사용
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import java.util.List;
@Repository
public class PostRepositoryCustomImpl implements PostRepositoryCustom {
private final JPAQueryFactory queryFactory;
public PostRepositoryCustomImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public Slice<Post> findPostsBy(Pageable pageable) {
QPost post = QPost.post;
List<Post> content = queryFactory.selectFrom(post)
.orderBy(post.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1) // 다음 페이지 존재 여부 확인을 위해 +1
.fetch();
// 다음 페이지 존재 여부를 확인하고, 필요한 경우 마지막 항목 제거
boolean hasNext = false;
if (content.size() > pageable.getPageSize()) {
content.remove(content.size() - 1);
hasNext = true;
}
// SliceImpl을 사용하여 결과와 다음 페이지 존재 여부 반환
return new SliceImpl<>(content, pageable, hasNext);
}
}
이 메서드는Pageable 객체를 인자로 받아 현재 페이지와 페이지 크기에 따른 글 목록을 Slice<Post> 형태로 반환한다.
여기서 중요한 점은 이 메서드가 다음 페이지의 존재 여부만을 알려주며, 전체 글의 개수는 알려주지 않는다는 것이다.
Slice 객체의 필드와 반환 정보
Slice는 다음 페이지의 존재 여부만을 제공함으로써, 더 적은 비용으로 빠른 응답 시간을 가능하게 한다.
- getContent(): 현재 페이지에 해당하는 데이터 목록을 반환한다.
- hasNext(): 다음 페이지의 존재 여부만을 제공한다.
Slice 사용 이점
전체 데이터 개수를 구하는 count 쿼리 실행이 필요 없으므로, 특히 데이터 양이 많은 경우 응답 시간을 단축시킬 수 있다. 그리고 클라이언트 측에서는 다음 페이지가 있는지 없는지만을 확인하면 되므로, 구현이 단순해진다.
주의 사항
Slice는 다음 페이지의 존재 여부만을 제공하므로, 전체 페이지 수나 전체 데이터 개수가 필요한 경우에는 적합하지 않다. 그리고 사용자가 데이터의 마지막 페이지를 요청한 경우, 다음 페이지가 없다는 것을 알리기 위해 추가적인 로직이 필요할 수 있다.
Page<Entity>
Page<Entity>는 페이징 처리 시 Slice<Entity>와 함께 사용할 수 있는 또 다른 방법으로, 전체 페이지 수와 전체 데이터 개수를 포함하는 정보를 제공한다. 이 방법은 사용자가 페이지 번호를 선택하여 특정 페이지에 직접 접근할 수 있는 전통적인 페이징 인터페이스를 구현할 때 유용하다.
Page<Entity>의 동작 방식
Page<Entity> 객체는 페이징 처리된 데이터 목록뿐만 아니라, 전체 데이터 개수와 전체 페이지 수 등의 추가 정보를 제공한다. 이는 사용자에게 페이지네이션 컨트롤을 통해 특정 페이지로 직접 이동할 수 있는 기능을 제공하기 위해 필요한 정보다.
사용 예제
전통적인 웹 화면에서 사용자가 페이지 번호를 클릭하여 특정 페이지의 데이터를 조회할 수 있는 시나리오를 상상해 보자. 이 경우, 사용자 인터페이스에는 전체 페이지 수를 표시하고, 사용자가 원하는 페이지 번호를 선택할 수 있도록 해야 한다.
- JPA 사용
// 글 목록 페이지 조회를 위한 Repository 인터페이스에 정의된 메소드
Page<Post> findPostsByPage(Pageable pageable);
- QueryDSL 사용
import com.querydsl.core.types.Predicate;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import java.util.List;
@Repository
public class PostRepositoryCustomImpl implements PostRepositoryCustom {
private final JPAQueryFactory queryFactory;
public PostRepositoryCustomImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public Page<Post> findPostsByPage(Pageable pageable, Predicate condition) {
QPost post = QPost.post;
// 데이터 목록 조회
List<Post> content = queryFactory
.selectFrom(post)
.where(condition) // 조건 적용
.orderBy(post.createdAt.desc()) // 정렬 조건 적용
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// 전체 데이터 개수 계산 (orderBy 제외)
long total = queryFactory
.selectFrom(post)
.where(condition) // 동일한 조건 적용
.fetchCount();
// PageImpl을 사용하여 Page<Post> 객체 반환
return new PageImpl<>(content, pageable, total);
}
}
Page 객체의 필드와 반환 정보
- getContent(): 현재 페이지에 해당하는 데이터 목록
- getTotalElements(): 전체 데이터 개수
- getTotalPages(): 전체 페이지 수
- isFirst(), isLast(): 현재 페이지가 첫 페이지이거나 마지막 페이지인지 여부
- hasNext(), hasPrevious(): 다음 페이지나 이전 페이지의 존재 여부
Page 사용 이점
사용자에게 전체 페이지 수와 전체 데이터 개수를 포함한 상세 정보를 제공하여, 페이지네이션 인터페이스를 구현할 수 있다. 그리고 사용자가 원하는 페이지로 직접 이동할 수 있는 기능을 제공한다.
주의사항
Page를 사용할 때는 전체 데이터 개수를 계산하는 count 쿼리가 실행되므로, 대용량 데이터를 다룰 때 성능에 영향을 줄 수 있다.
🔥 결론
무한 스크롤 또는 다음 페이지 로드와 같은 기능에서는 Slice(Entity)의 사용이 선호된다. 전체 페이지 수나 전체 데이터 개수를 알 필요가 없으며, 다음 페이지가 존재하는지의 여부만 중요하다.
전통적인 페이지네이션이 필요한 경우, 즉 사용자가 직접 페이지를 선택할 수 있는 인터페이스가 있는 경우 Page(Entity)의 사용이 적합하다. 전체 데이터 개수와 전체 페이지 수를 알아야 하기 때문이다.
각각의 사용 사례에 따라 Slice 또는 Page를 선택함으로써, 애플리케이션의 성능과 사용자 경험을 최적화할 수 있다.
'Spring > Spring Data JPA' 카테고리의 다른 글
QueryDSL 사용할때 Custom Interface와 QueryRepository 단독 빈 등록 방식 비교 (테스트 어노테이션) (0) | 2024.04.01 |
---|---|
JPA에서 Entity 복합키 관리 (@Embeddable, @EmbeddedId) (0) | 2024.04.01 |
QueryDSL 사용 시 자동 업데이트 되지 않는 JPA Auditing 수정 날짜 문제 해결 방법 (1) | 2024.02.01 |
JPA와 Spring Data JPA의 차이 (1) | 2024.01.02 |
JPA N+1 문제 해결하기 (fetch join, entityGraph, batch size) (5) | 2023.12.28 |