Backend/SpringBoot

Paging

Dean83 2024. 10. 24. 16:13

다수의 데이터가 있을 경우, 전부를 보내주지 않고 Paging 하여 리턴 할 수 있도록 한다. 기본구조 4개중 Repository Service, Controller 세곳 모두 연관이 있다. 기본적으로 정렬과 연관이 있다. 정렬된 데이터를 리턴하기 때문이다. 

 

페이징 방식은 크게 두개로 나뉜다

페이지네이션 (offset - 시작위치 / limit - 사이즈)

  • 페이지 건너뛰어 탐색 가능
  • 앞 페이지에 데이터가 삽입 되었을 경우, 다음페이지 탐색시 중복 데이터가 노출된다.
  • 스프링부트에서 쉽게 사용할 수 있다.
  • Page 를 사용한다
    • 기본 Page로 비즈니스 로직을 사용할 수 없다면,  @Query 등으로 수동으로 쿼리문을 통해 Page와 조합하여 구현한다
  • Page 라는 자료형을 사용하고, count 쿼리도 같이 날린다. 전체 페이지 수를 계산해야 하기 떄문이다. DB에 부담될 수 있다.
    • @Query 어노테이션을 이용할 경우 따로 countQuery 쿼리도 포함해 줘야 한다.

무한 스크롤 (커서 기반)

  • 앞의 보여준 페이지(?) 에 데이터가 삽입 되어도 상관없이 이어서 보여줄 수 있다.
  • 커서가 중요하다. 커서는 정렬조건으로 주로 createdAt 혹은 ID가 많이 쓰인다.
  • 이 커서를 클라이언트로 부터 받기도 해야하고, 주기도 해야 한다. 이를 통해 정렬된 데이터 중 다음 데이터를 줄 위치를 파악한다.
  • Slice 를 이용한다.
    • 기본 Slice로 비즈니스 로직을 구현할 수 없을 경우 @Query 등으로 수동으로 쿼리문을 통해 Slice와 조합하여 구현해 주어야 한다. (where 조건을 추가 하는것이 중요)
  • 예 : 생성일 기준 최신순으로 보여줄때. 1번은 최초 탐색, 2번은 다음 데이터 탐색 쿼리문 (여기서 생성일이 커서가 된다) 
    • select * from comments order by created_at desc limit 10
    • select * from comments where created_at < 날짜 order by created_at desc limit 10
  • Slice 자료형을 사용하고, hasNext 맴버변수로 다음이 있는지 확인하기 때문에 count쿼리를 날리지 않는다.
  •  

 

일반 Offset 기반 Page

  •  Repository
    • Page 를 리턴하는 함수를 하나 생성한다.
    • 인자값으로는 Pageable을 받는다. 
public interface MemberRepository extends JpaRepository<Member, Long> {
    // 이름으로 검색하면서 페이징
    Page<Member> findByNameContaining(String name, Pageable pageable);
}
  • 만일, @Query 어노테이션을 이용하여 JPQL을 이용할 경우, countQuery 쿼리 또한 작성을 해 주어야 한다.
public interface MemberRepository extends JpaRepository<Member, Long> {

    @Query(
        value = "SELECT m FROM Member m WHERE m.name LIKE %:name%",
        countQuery = "SELECT COUNT(m) FROM Member m WHERE m.name LIKE %:name%"
    )
    Page<Member> findByNameContainingCustom(
            @Param("name") String name,
            Pageable pageable);
}
  • Service
    • Repository 에서 선언한 함수를 호출하여 값을 리턴한다
    • PageRequest 를 이용해 조건을 생성하여 조회 한다.
@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    public Page<MemberDTO> searchMembersByName(String keyword, int page, int size, String[] sort) {
        Sort.Direction direction = sort[1].equalsIgnoreCase("desc") 
                ? Sort.Direction.DESC 
                : Sort.Direction.ASC;
        Sort sortCondition = Sort.by(direction, sort[0]);

        Pageable pageable = PageRequest.of(page, size, sortCondition);
		
        Page page = memberRepository.findByNameContaining(keyword, pageable);
        return page.map(MemberDTO변환);
    }
}

 

  • PageRequest.of 에서 size 는 1페이지에 들어갈 데이터의 수 이다.

 

  • Controller
    • 인자값으로 페이지 번호를 받아야 한다
    • Service 함수를 통해 Page를 받아온다
@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    // 특정 이름 검색 (페이징)
    // ex) /members/search?keyword=홍&page=0&size=5&sort=id,asc
    @GetMapping("/search")
    public Page<MemberDTO> searchMembers(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id,asc") String[] sort) {

        return memberService.searchMembersByName(keyword, page, size, sort);
    }
}

 

 

 

무한스크롤을 위한 Slice 조회 예

  • Slice를 기본으로 이용하는 예제.
  • Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    Slice<Member> findByNameContaining(String name, Pageable pageable);
}

 

  • Service
@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;


    // 특정 이름 검색 (Slice 기반 무한 스크롤)
    public Slice<Member> searchMembersByName(String keyword, int page, int size, String[] sort) {
        Sort.Direction direction = sort[1].equalsIgnoreCase("desc") 
                ? Sort.Direction.DESC 
                : Sort.Direction.ASC;
        Sort sortCondition = Sort.by(direction, sort[0]);

        Pageable pageable = PageRequest.of(page, size, sortCondition);

        return memberRepository.findByNameContaining(keyword, pageable);
    }
}

 

  • Controller
@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    // 특정 이름 검색 (무한 스크롤)
    // ex) /members/search?keyword=홍&page=0&size=10&sort=id,asc
    @GetMapping("/search")
    public Slice<Member> searchMembers(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id,asc") String[] sort) {

        return memberService.searchMembersByName(keyword, page, size, sort);
    }
}
  • Page 내장 속성
    • 변수명.isEmpty
    • .totalPages
    • .size
      • 페이지당 보여줄 수
    • .number
      • 현재 페이지 번호
    • .hasPrevious
    • .hasNext 등이 있다.