본문 바로가기
Side Project/Socket

List를 pageable과 PageImpl로 구현하기 - List Paging

by asdft 2024. 3. 15.

페이징을 쓰는 경우는 1번 case가 대부분이며 가장 이상적이다.

 

1. querydsl의 fetch()와 countQuery.size()로 가져오기. (가장 이상적)

아래 코드처럼 fetchResults( )를 하면, content를 위한 쿼리 1번 + total을 위한 쿼리 1번 총 2번의 쿼리를 날린다.

public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {

    //Contents 만을 위한 쿼리
    List<MemberTeamDto> content = queryFactory
        .select(new QMemberTeamDto(
            member.id,
            member.username,
            member.age,
            team.id,
            team.name))
        .from(member)
        .leftJoin(member.team, team)
        .where(
            usernameEq(condition.getUsername()),
            teamNameEq(condition.getTeamName()),
            ageGoe(condition.getAgeGoe()),
            ageLoe(condition.getAgeLoe())
        )
        .offset(pageable.getOffset())   //몇번부터 시작
        .limit(pageable.getPageSize())  //몇개를 가져올지
        .fetch();

    //Total Count를 위한 쿼리
    JPAQuery<Member> countQuery = queryFactory
        .select(member)
        .from(member)
        .leftJoin(member.team, team)
        .where(
            usernameEq(condition.getUsername()),
            teamNameEq(condition.getTeamName()),
            ageGoe(condition.getAgeGoe()),
            ageLoe(condition.getAgeLoe())
        );

    return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetch().size());

 

 

.fetchResults() 가 deprecated 됐다.

fetchCount, fetchResult는 둘다 querydsl 내부에서 count용 쿼리를 만들어서 실행해야 하는데, 이때 작성한 select 쿼리를 기반으로 count 쿼리를 만들어낸다. 그런데 이 기능이 select 구문을 단순히 count 처리하는 것으로 바꾸는 정도여서, 단순한 쿼리에서는 잘 동작하는데, 복잡한 쿼리 (다중그룹 쿼리) 에서는 잘 동작하지 않는다.

이럴때는 명확하게 카운트 쿼리를 별도로 작성하고,  fetch().size()를 사용해서 해결해야 한다.

 

하지만 컬렉션을 가져오기 위해서는 위처럼 하기가 힘들다.

 

결국 서버 딴에서 데이터를 가져오고 알아서 데이터를 걸러야한다.

 

2. 데이터들을 List에 저장 후, List를 PageImpl로 변환하기

검색 조건이 여러개 붙다 보면 서버 딴에서 데이터를 가공해야 할 때가 있다.

내가 구현한 해시태그 조회 API에서도 String타입의 skillName과 맵핑된 postId를 Querydsl에 파라미터로 넘겨 가져오고 있다.

 

postId의 경우 primary key로 중복값 없이 오직 한개밖에 존재하지 않는다. 

 

그래서 조건에 맞는 여러개의 postId를 차례로 조회 후, 이를 List에 저장해, 이 List를 기반으로 페이징을 진행해야 했다.

이 경우, 아래코드 처럼 List의 시작과 끝지점을 수동으로 설정해줘야 한다.

List의 이름이 postsByPostDto 이다.

  int start = (int) pageable.getOffset();	//(1)
  
  //동일표현 (start + pageable.getPageSize()) > postsByPostDto.size() ? postsByPostDto.size() : (start + pageable.getPageSize())
  int end = Math.min((start + pageable.getPageSize()), postsByPostDto.size());	//(2)


    return new PageImpl<>(postsByPostDto.subList(start, end), pageable, postsByPostDto.size());	//(3)

 

(1)   int start = (int) pageable.getOffset( );
  • start: 페이지의 시작 인덱스를 나타낸다. Pageable 객체의 offset 값을 사용하여 계산된다.
  • 아래  (참고)의 Pageable 인터페이스를 보면 알 수 있듯이 getOffset( )은 long타입으로 지정되어 있어 이를 int형으로 형변환 해주어야 한다.

 

(2)   int end = Math.min((start + pageable.getPageSize()), postsByPostDto.size()); 
  • end: 페이지의 끝 인덱스를 나타낸다. start에 페이지 크기(pageSize)를 더하고, 만약 그 결과가 전체 결과 목록의 크기를 넘어가면 전체 결과 목록의 크기를 사용한다. 이를 통해 마지막 페이지의 경우 마지막 인덱스를 설정할 수 있다.

 

(3)   PageImpl<>(postsByPostDto.subList(start, end), pageable, postsByPostDto.size()); 
  • 위에서 계산한 범위에 해당하는 결과를 잘라내어 새로운 PageImpl 객체를 생성한다. 이 객체는 페이징된 결과를 표현하며, 이전에 생성한 pageable 객체와 전체 결과 목록의 크기를 전달한다.

 

Offset 계산
offset은 페이지의 번호가 아니라, 데이터를 조회하기 시작할 row의 위치입니다.(0부터 시작)  
따라서 페이지의 사이즈가 5이고, 0번째 페이지를 조회하고 싶다면 offset은 0,  1번째 페이지를 조회하고 싶다면 5,
2번째 페이지를 조회하고 싶다면 10, 이런 식으로 반환되어야 하는 것입니다.

 


제대로 동작하는지 테스트 코드를 통해 알아보자

 

@Test
  void skillName과_관련된_게시물이_있으면_성공적으로_조회한다() {
    Skill skill = Skill.builder().id(1L).skillName("Java").build();
	
    String skillName = "Java";
    
    when(skillJpaRepository.findBySkillName(anyString())).thenReturn(Optional.of(skill));

    when(postSkillJpaRepository.findAllByPostQuery(anyLong())).thenReturn(postIdList);

    when(postJpaRepository.findPostByPostId(1L)).thenReturn(Optional.of(samplePostDtos().get(0)));
    when(postJpaRepository.findPostByPostId(2L)).thenReturn(Optional.of(samplePostDtos().get(1)));
    when(postJpaRepository.findPostByPostId(3L)).thenReturn(Optional.of(samplePostDtos().get(2)));
    when(postJpaRepository.findPostByPostId(4L)).thenReturn(Optional.of(samplePostDtos().get(3)));

    Page<Optional<PostDto>> postsUsingSkill
        = getAllPostsOfSkillService.getPostsUsingSkill(skillName, 1, 2);

    assertThat(postsUsingSkill.getContent()).extracting(postDto -> postDto.orElseThrow())
        .extracting("postId")
        .containsExactly(3L, 4L);
    assertThat(postsUsingSkill.getSize()).isEqualTo(2);
  }


  List<PostDto> samplePostDtos() {
    return List.of(
        new PostDto(1L, "title1", "content1", PostType.STUDY, PostMeeting.ONLINE,
            PostStatus.CREATED, 1L, "nickname", LocalDateTime.now()),
        new PostDto(2L, "title2", "content2", PostType.STUDY, PostMeeting.OFFLINE,
            PostStatus.CREATED, 1L, "nickname", LocalDateTime.now()),
        new PostDto(3L, "title3", "content3", PostType.STUDY, PostMeeting.OFFLINE,
            PostStatus.CREATED, 1L, "nickname", LocalDateTime.now()),
        new PostDto(4L, "title4", "content4", PostType.STUDY, PostMeeting.OFFLINE,
            PostStatus.CREATED, 1L, "nickname", LocalDateTime.now())
    );
  }

 

 

 

참고

[출처]

https://ttl-blog.tistory.com/841 [Shin._.Mallang:티스토리]