Spring-Jpa Best Practices step-12 - 페이징 API 만들기

Spring Data JPA를 활용한 페이징 API 만들기

Posted by Yun on 2019-01-21

해당 코드는 Github를 확인해주세요.

JPA를 이용해서 Paging API를 만들어 보도록 하겠습니다. 페이징 처리는 거의 모든 웹 개발에서 사용하고 있습니다. 그렇게 복잡하고 어려운 구현은 아니나 실제 쿼리로 작성할 때는 상당히 번거로운 작업이 됩니다. 또 데이터베이스마다 페이징 쿼리가 조금씩 다르다는 점도 복잡도를 높이는 요인 중 하나입니다.

Spring Data JPA에서는 이러한 문제를 아주 쉽게 해결할 수 있어 핵심 비즈니스 로직에 집중할 수 있게 해줍니다. 지금부터 예제를 설명하겠습니다.

기초 작업

이전에 작성한 AccountRepositoryJpaRepository를 상속하고 있습니다. JpaRepositoryPagingAndSortingRepository 클래스를 상속 받고 있습니다. 페이징 작업을 위한 작업은 AccountRepository를 작성한 순간 대부분 끝난 것입니다.

CurdRepository를 상속하는 것보다 하위 클래스인 JpaRepository를 상속 받아 Repository를 구현하는 것이 좋습니다.

페이징 처리하는 메서드도 간단합니다. 매게변수로 Pageable 받아 Page<T>으로 리턴해줍니다.

Sample Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@RestController
@RequestMapping("accounts")
public class AccountController {
@GetMapping
public Page<AccountDto.Res> getAccounts(final Pageable pageable) {
return accountService.findAll(pageable).map(AccountDto.Res::new);
}
}

@Service
@Transactional
@AllArgsConstructor
public class AccountService {
...
@Transactional(readOnly = true)
public Page<Account> findAll(Pageable pageable) {
return accountRepository.findAll(pageable);
}
}

정말 간단합니다. 컨롤어에서 Pageable 인터페이스를 받고 repository 메서드 findAll(pageable)로 넘기기만 하면됩니다.

요청

1
2
curl -X GET \
http://localhost:8080/accounts

응답

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
"content": [
{
"email": {
"value": "test001@test.com"
},
"password": {
"value": "$2a$10$tI3Y.nhgC.73LYCszoCaLu3nNEIM4QgeACiNseWlvr1zjrV5NCCs6",
"expirationDate": "+20120-01-20T00:00:00",
"failedCount": 0,
"ttl": 1209604,
"expiration": false
},
"fistName": "first",
"lastName": "last",
"address": {
"address1": "address1",
"address2": "address2",
"zip": "002"
}
}
...
],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"offset": 0,
"pageSize": 20,
"pageNumber": 0,
"paged": true,
"unpaged": false
},
"last": true, // 마지막 페이지 여부
"totalPages": 1, // 전체 페이지가 1개
"totalElements": 13, // 모든 요소는 13 개
"size": 20, // 한 페이지에서 보여줄 사이즈의 갯수, size를 제한하지 않으면 기본적으로 20으로 초기화 된다.
"number": 0,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"numberOfElements": 13,
"first": true, // 첫 패이지 여부
"empty": false // 리스트가 비어 있는지 여부
}

Response에 상세한 정보들을 내려주고 있습니다. 페이징 하단의 네비게이션을 작성할 때 유용할 정보들이 있습니다. 이렇게 Spring Data JPA를 이용하면 페이징 기능을 간편하게 만들 수 있습니다.

다양한 요청

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
{
"content": [
{
"email": {
"value": "test013@test.com"
},
"password": {
"value": "$2a$10$tI3Y.nhgC.73LYCszoCaLu3nNEIM4QgeACiNseWlvr1zjrV5NCCs6",
"expirationDate": "+20120-01-20T00:12:00",
"failedCount": 0,
"ttl": 1209604,
"expiration": false
},
"fistName": "first",
"lastName": "last",
"address": {
"address1": "address1",
"address2": "address2",
"zip": "002"
}
},
{
"email": {
"value": "test012@test.com"
},
"password": {
"value": "$2a$10$tI3Y.nhgC.73LYCszoCaLu3nNEIM4QgeACiNseWlvr1zjrV5NCCs6",
"expirationDate": "+20120-01-20T00:11:00",
"failedCount": 0,
"ttl": 1209604,
"expiration": false
},
"fistName": "first",
"lastName": "last",
"address": {
"address1": "address1",
"address2": "address2",
"zip": "002"
}
}
],
"pageable": {
"sort": {
"unsorted": false,
"sorted": true,
"empty": false
},
"offset": 0,
"pageSize": 2,
"pageNumber": 0,
"paged": true,
"unpaged": false
},
"last": false,
"totalPages": 7,
"totalElements": 13,
"size": 2,
"number": 0,
"sort": {
"unsorted": false,
"sorted": true,
"empty": false
},
"numberOfElements": 2,
"first": true,
"empty": false
}

Pageable은 다양한 요청 이용해서 기본적인 정렬 기능을 제공합니다. page는 실제 페이지를 의미하고 sizecontent의 size를 의미합니다. sort는 페이징을 처리 시 정렬을 값을 의미합니다. id,DESC는 id 기준으로 내림차순 정렬을 하겠다는 것입니다.

아래는 실제 JPA에서 작성한 Query 입니다. 쿼리문을 코드로 작성하기 때문에 실제로 어떤 쿼리가 동작하는지 반드시 확인하는 습관을 갖는 것이 좋습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
select
account0_.id as id1_0_,
account0_.address1 as address2_0_,
account0_.address2 as address3_0_,
account0_.zip as zip4_0_,
account0_.created_at as created_5_0_,
account0_.email as email6_0_,
account0_.first_name as first_na7_0_,
account0_.last_name as last_nam8_0_,
account0_.password_expiration_date as password9_0_,
account0_.password_failed_count as passwor10_0_,
account0_.password_ttl as passwor11_0_,
account0_.password as passwor12_0_,
account0_.update_at as update_13_0_
from
account account0_
order by
account0_.id desc limit ?

select
count(account0_.id) as col_0_0_
from
account account0_

개선

위의 Pageable의 개선할 점이 있습니다. 우선 size에 대한 limit이 없습니다. 위의 API에서 size값을 200000을 넘기면 실제 데이터베이스 쿼리문이 200000의 조회할 수 있습니다. 그 밖에 page가 0 부터 시작하는 것들도 개선하는 것이 필요해 보입니다.

PageRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class PageRequest {

private int page;
private int size;
private Sort.Direction direction;

public void setPage(int page) {
this.page = page <= 0 ? 1 : page;
}

public void setSize(int size) {
int DEFAULT_SIZE = 10;
int MAX_SIZE = 50;
this.size = size > MAX_SIZE ? DEFAULT_SIZE : size;
}

public void setDirection(Sort.Direction direction) {
this.direction = direction;
}
// getter

public org.springframework.data.domain.PageRequest of() {
return org.springframework.data.domain.PageRequest.of(page -1, size, direction, "createdAt");
}

Pageable을 대체하는 PageRequest 클래스를 작성합니다.

  • setPage(int page) 메서드를 통해서 0보다 작은 페이지를 요청했을 경우 1 페이지로 설정합니다.
  • setSize(int size) 메서드를 통해서 요청 사이즈 50 보다 크면 기본 사이즈인 10으로 바인딩 합니다.
  • of() 메서트를 통해서 PageRequest 객체를 응답해줍니다. 페이지는 0부터 시작하니 page -1 합니다. 본 예제에서는 sort는 createdAt 기준으로 진행합니다.

컨트롤러

1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping("accounts")
public class AccountController {
...

@GetMapping
public Page<AccountDto.Res> getAccounts(final PageRequest pageable) {
return accountService.findAll(pageable.of()).map(AccountDto.Res::new);
}
}

컨트롤러 영역은 간단합니다. Pageable -> PageRequest 교체하면 됩니다.

요청

page를 기본 사이즈를 50을 넘는 500을 설정했습니다. PageRequest이 정상 동작한다면 기본 사이즈 10으로 설정해서 페이징 처리가 진행됩니다. 아래는 응답 값입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"last": false,
"totalPages": 2,
"totalElements": 13,
"size": 10, // size 값이 10으로 정상 작동
"number": 0,
"numberOfElements": 10,
"first": true,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"empty": false
}

결론

Spring Data JPA는 페이징 처리는 데이터베이스마다 페이징 쿼리가 다른 부분들은 신경 쓰지 않고 더 쉽게 구현할 수 있는 장점이 있습니다. 또 Pageable을 통해서 페이징 구현이 가능하지만, 별도의 Request 객체를 두어서 관리하는 것이 안전하고 효과적입니다.