스프링데이터 + JPA/스프링 데이터 JPA

23. 웹 확장 페이징과 정렬

sdafdq 2023. 11. 24. 11:28

스프링 데이터 JPA는 페이징과 정렬을 웹에서 편리하게 사용할 수 있도록 기능을 제공함.

 

@GetMapping("/members")
public Page<Member> list(Pageable pageable){
    return memberRepository.findAll(pageable);
}

이렇게 인자로 Pageable로 받으면, 

아마 argumentResolver에 어댑팅 되면서 그 때 스프링이 PageRequest였나 그걸 생성해서 값을 채운다음 저렇게 인자로 넣어 줄 거임.

 

그럼 그거 받아서 저 Pageable 인자로 받는 findAll은 PagingAndSortingRepository(JpaRepository가 상속받은 인터페이스 중 하나)에 있는거임

 

일단 그래서 요청해보면,

http://localhost:8080/members

지금 우리가 그냥 Page 객체로 준거라,

{
    "content": [
        {
            "createdDate": "2023-11-24T10:25:38.971901",
            "lastModifiedDate": "2023-11-24T10:25:38.971901",
            "createBy": "32a0324e-4c40-4749-89f8-82a41b398eff",
            "lastModifiedBy": "32a0324e-4c40-4749-89f8-82a41b398eff",
            "id": 1,
            "username": "user0",
            "age": 0,
            "team": null
        },
        {
            "createdDate": "2023-11-24T10:25:39.005888",
            "lastModifiedDate": "2023-11-24T10:25:39.005888",
            "createBy": "627cab00-3ea7-48c4-8da1-7cb579284519",
            "lastModifiedBy": "627cab00-3ea7-48c4-8da1-7cb579284519",
            "id": 2,
            "username": "user1",
            "age": 1,
            "team": null
        },
        .......

그냥 이런 식으로 객체가 json 형태로 바뀌어 져서 응답함.

 

저렇게 요청 시에 페이징 옵션을 따로 주지 않으면 기본이 0번째 페이지에 페이지당 사이즈는 20임.

 

http://localhost:8080/members?page=2
{
    "content": [
        {
            "createdDate": "2023-11-24T10:25:39.131801",
            "lastModifiedDate": "2023-11-24T10:25:39.131801",
            "createBy": "14b87fb7-32d3-486e-aee6-96b3c9204fd4",
            "lastModifiedBy": "14b87fb7-32d3-486e-aee6-96b3c9204fd4",
            "id": 41,
            "username": "user40",
            "age": 40,
            "team": null
        },
        {
            "createdDate": "2023-11-24T10:25:39.1338",
            "lastModifiedDate": "2023-11-24T10:25:39.1338",
            "createBy": "a5fc4356-5d01-469f-baab-f67696f5a4dc",
            "lastModifiedBy": "a5fc4356-5d01-469f-baab-f67696f5a4dc",
            "id": 42,
            "username": "user41",
            "age": 41,
            "team": null
        },
        ....

이렇게 2번째 페이지 부터

 

http://localhost:8080/members?page=2&size=3

이렇게 몇번째 페이지인지, 페이지 당 사이즈는 몇 인지

{
    "content": [
        {
            "createdDate": "2023-11-24T10:25:39.025861",
            "lastModifiedDate": "2023-11-24T10:25:39.025861",
            "createBy": "b13550d6-c3f1-4838-9b06-b210d5f9c645",
            "lastModifiedBy": "b13550d6-c3f1-4838-9b06-b210d5f9c645",
            "id": 7,
            "username": "user6",
            "age": 6,
            "team": null
        },
        {
            "createdDate": "2023-11-24T10:25:39.02986",
            "lastModifiedDate": "2023-11-24T10:25:39.02986",
            "createBy": "ebc5ee80-37a3-4929-be19-a97110bc0c4d",
            "lastModifiedBy": "ebc5ee80-37a3-4929-be19-a97110bc0c4d",
            "id": 8,
            "username": "user7",
            "age": 7,
            "team": null
        },
        {
            "createdDate": "2023-11-24T10:25:39.034856",
            "lastModifiedDate": "2023-11-24T10:25:39.034856",
            "createBy": "a8d8d6a3-3b2a-4bd5-adb8-134e1cb83e42",
            "lastModifiedBy": "a8d8d6a3-3b2a-4bd5-adb8-134e1cb83e42",
            "id": 9,
            "username": "user8",
            "age": 8,
            "team": null
        }
    ],
    "pageable": {
        "pageNumber": 2,
        "pageSize": 3,
        "sort": {
            "empty": true,
            "sorted": false,
            "unsorted": true
        },
        "offset": 6,
        "paged": true,
        "unpaged": false
    },
    "last": false,
    "totalElements": 100,
    "totalPages": 34,
    "size": 3,
    "number": 2,
    "sort": {
        "empty": true,
        "sorted": false,
        "unsorted": true
    },
    "first": false,
    "numberOfElements": 3,
    "empty": false
}

이렇게 나옴.

 

물론 저렇게 get방식 url 파라미터 말고도 post 방식으로도 가능 할거임.

 

 

 

다음은 정렬.

http://localhost:8080/members?page=0&size=3&sort=id,desc

sort = id인데, 내림차순. 내림차순의 0번째 페이지이므로 100번째부터(100개를 넣었음)

{
    "content": [
        {
            "createdDate": "2023-11-24T10:25:39.241737",
            "lastModifiedDate": "2023-11-24T10:25:39.241737",
            "createBy": "d9a4e9ec-66b0-472b-b657-907cf69e6526",
            "lastModifiedBy": "d9a4e9ec-66b0-472b-b657-907cf69e6526",
            "id": 100,
            "username": "user99",
            "age": 99,
            "team": null
        },
        {
            "createdDate": "2023-11-24T10:25:39.239739",
            "lastModifiedDate": "2023-11-24T10:25:39.239739",
            "createBy": "ada94eb1-5852-42dd-9448-d42ca95cd8d0",
            "lastModifiedBy": "ada94eb1-5852-42dd-9448-d42ca95cd8d0",
            "id": 99,
            "username": "user98",
            "age": 98,
            "team": null
        },
        {
        ....
http://localhost:8080/members?page=0&size=3&sort=id,desc&sort=username,desc

이렇게 여러 조건으로 sort

 

봤다시피 파라미터는

page : 몇번째 페이지

size : 한 페이지 당의 사이즈

sort : 정렬 조건 

 

 

그리고, 기본 설정은 size는 20임.

그리고 최대 사이즈는 2000 (한번에 가져올 수 있는 최고 데이터 수)

이게 기본임

 

이거 바꿀 수 도 있음. 

application.yml 에서

spring:
......
    web:
      pageable:
        default-page-size: 6
        max-page-size: 100

저 spring.web.pageable에서 설정하면 됨.

 

아니면 좀 기본 설정을 저런 컨트롤러마다? 인자로 들어오는 것 마다 개개별로 하고 싶으면

@GetMapping("/members")
public Page<Member> list(@PageableDefault(size = 5, sort = "username") Pageable pageable){
    return memberRepository.findAll(pageable);
}

이렇게

@PageableDefault(size=값, sort="기준열")

할 수 있음.

Pageable 앞에다가

 

@GetMapping("/members")
public Page<Member> list(@PageableDefault(size = 3,sort = "id", direction = Sort.Direction.DESC) Pageable pageable){
    return memberRepository.findAll(pageable);
}

 

실제로 잘 됨.

 

 

페이징 정보가 둘 이상일 때.

@Qualifier 쓰고 접두사로 구분함. 저게 접두사를 붙여주는 거임. 접두사_필드명 이런식으로 

Qualifier 직역은 특성을 전달하다, 특성을 부여하다 라고 한다.

이거는

public String list(
        @Qualifier("member") Pageable memberPageable,
        @Qualifier("order") Pageable orderPageable, ...
    )

이런 식으로 받는다고 한다.

저렇게 접두사를 붙여

http://localhost:8080/members?member_page=0&order_page=1

이런 식으로

 

 

return을 Page<Member>해서 사실 계속 엔티티를 직접 응답했는데, 좋지 않음.

Dto로 바꿔서 응답할거임.

 

@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable){
    Page<Member> page = memberRepository.findAll(pageable);
    Page<MemberDto> dto = page.map(member -> new MemberDto(member.getId(), member.getUsername(), "teamName"));
    return dto;
}

이렇게 Page에 있는 map 메소드를 이용하면, Page안에 content가 List<T> 이런 식으로 본격적으로 값을 가지고 있으니까, map 메소드를 이용하면 저 content를 바꿔줌.

저렇게 콜백으로 저 new MemberDto가 return 되게끔 하면, (저게 저 콜백 return 결과들 모아서 콜렉션으로 만들어 주는거임. return은 생략된거.)

 

{
    "content": [
        {
            "id": 1,
            "username": "user0",
            "teamName": "teamName"
        },
        {
            "id": 2,
            "username": "user1",
            "teamName": "teamName"
        },
        {
            "id": 3,
            "username": "user2",
            "teamName": "teamName"
        },
        {
            "id": 4,
            "username": "user3",
            "teamName": "teamName"
        },
        {
            "id": 5,
            "username": "user4",
            "teamName": "teamName"
        },
        {
            "id": 6,
            "username": "user5",
            "teamName": "teamName"
        }
    ],
    "pageable": {
        "pageNumber": 0,
        "pageSize": 6,
        "sort": {
            "empty": true,
            "sorted": false,
            "unsorted": true
        },
        "offset": 0,
        "paged": true,
        "unpaged": false
    },
    "last": false,
    "totalElements": 100,
    "totalPages": 17,
    "size": 6,
    "number": 0,
    "sort": {
        "empty": true,
        "sorted": false,
        "unsorted": true
    },
    "first": true,
    "numberOfElements": 6,
    "empty": false
}

이렇게 content 안이 Dto로 변환되어 잘 나옴.

6개 나온건 application.yml 글로벌 설정해서 사이즈를 페이지 사이즈를 6으로 설정해놔서. 0번째 사이즈 6개. 

또 default가 0번째 페이지 부터

 

 

 

그리고 Dto는 엔티티를 봐도 됨.

그러니까 생성자로 엔티티를 받아들여도 된다는 거.

 

물론 엔티티는 최대한 심플하고 깔끔하고 정확하게 핵심 비즈니스 메소드만. 화면에 관련된 거는 있으면 안됨

public MemberDto(Member member){
    this.id = member.getId()
    ....
}

이런 식으로

 

 

지금 보면 페이지 객체로 넘기니까

{
    "content": [
      	....
    ],
    "pageable": {
        "pageNumber": 0,
        "pageSize": 6,
        "sort": {
            "empty": true,
            "sorted": false,
            "unsorted": true
        },
        "offset": 0,
        "paged": true,
        "unpaged": false
    },
    "last": false,
    "totalElements": 100,
    "totalPages": 17,
    "size": 6,
    "number": 0,
    "sort": {
        "empty": true,
        "sorted": false,
        "unsorted": true
    },
    "first": true,
    "numberOfElements": 6,
    "empty": false
}

이렇게 페이지에 대한 정보들이 다 있음.

그냥 이거 갖다가 쓰면 됨.

 

 

 

그리고, 스프링 데이터의 페이징은 0부터 시작하는데, 이게 불만일 수도 있다.

서버 상에서 해결법은 직접 Pageable을 상속받아 PageRequest 역할을 하는 구현체를 만들고, 응답값도 Page말고 직접 만들어서 제공하는 거다.

또 하나 방법은

application.yml에

spring.data.web.pageable.one-indexed-parameters라는 옵션이 있는데,

그걸 true로 한다.

근데 이 방법은 그냥 web으로 갈 때 page파아미터를 -1처리 하는 것 뿐. 0으로 와도 -1은 할 수 없으니 0으로 처리하고, 1로 처리하면 0이다.

오히려 더 헷갈린다.

 

그냥 이거는 프론트 쪽에서 +1 처리하는 게 훨씬 낫다.