현재 진행 중인 프로젝트에는 게시판에 Pagination 기능이 들어간다.
나름 기본적으로 구현해야 할 기능 중 하나라고 생각한다.
Pagination이 필요한 이유는 다음과 같다.
- 만약 글이 100만건 정도 된다면?
- 한 번에 모두 보내는 것은 성능 저하를 초래한다.
- Client 입장에서 생각해본다면?
- 일정 수의 글 만큼 자르는 것이 보기 더 편할 것이다.
Pagination 로직이 Frontend에 있다면?
Backend는 Query를 통해 전체 글을 보내기만 하면 되고,
Frontend에 매번 전체 게시글을 불러온 뒤 구현해야 한다.
이는 위에서 언급했던 Pagination이 필요한 이유에 어긋난다.
글의 수가 많아질 수록 한 번에 보내는 데에 걸리는 시간이 길어지고,
Frontend에서 처리해야 할 시간도 점점 길어지게 될 것이다.
또한 Frontend에서 전체 데이터를 이미 호출 했으므로,
어쩌면 데이터가 유출될 위험이 있을지도 모르겠다.
결국은 품질 저하를 초래하는 방식임을 알 수 있다.
그럼 Backend에 구현한다면?
Frontend는 버튼만 만들어 요청을 보내기만 하면 된다.
단, 페이지를 넘길 때 마다 매번 요청이 들어와 Query가 날아간다.
이 방식이 물론 위의 Frontend 로직 방식보다는 성능이 빠를 것이다.
애초에 한 번에 보내야 할 글의 수가 현저히 줄어든다.
그리고 특별한 로직 없이 Query문 만으로도 해결이 가능하다.
하지만 내 고민은 이제부터가 시작이다.
사용자가 많아지면, Query가 날아가는 횟수가 굉장히 늘 것이다.
그럼 과연 Query 자체 성능에 있어서는 문제가 없을까?
수 많은 사용자가 조회를 할 때에는 Query 자체의 성능이 중요할 것이다.
그래서 아래로 Pagination의 종류에 대해서 포스팅하려고 한다.
Offset based pagination
먼저, Offset based pagination
이다.
꽤 많이 사용되는 방식이며, 이름 그대로 LIMIT, OFFSET
을 사용한다.
아래와 같은 Query를 통해 구현할 수 있다.
SELECT * FROM BOARD LIMIT 40 OFFSET 80
SELECT * FROM BOARD LIMIT 80, 40
위의 두 Query는 동일하게 80번 이후로 40개의 데이터를 불러온다.
실제로 SQL이 아닌 코드 내에서 적용할 땐, 아래와 같이 구현할 수 있다.
위의 사진은, 실제로 프로젝트에 사용된 JPQL로 구현된 Pagination 코드이다.
setFirstResult(), setMaxResult()
를 통해 LIMIT, OFFSET
을 설정할 수 있다.
Postman 을 통해 요청을 날려보면 아래와 같은 Query가 날아간다.
Query의 끝에 LIMIT
구문이 추가되어 있음을 확인할 수 있다.
하지만 이 Query에는 성능적인 면에서 치명적인 단점이 있다.
OFFSET
의 원리는 데이터를 불러온 뒤 삭제하는 것이다.
위의 예시를 들면, 120
개의 데이터를 불러온 뒤 80
개를 삭제하는 것이다.
데이터가 많아져 LIMIT 40 OFFSET 1,000,000
를 요청한다고 생각해보자.
그럼 우리는 1,000,040
개의 데이터를 불러온 뒤 삭제해야 하는 것이다.
결국 데이터가 많을 수록 시간은 느려지고, 부하를 받게 된다!
Cursor based pagination
위의 이유로 등장하게 된 것이 cursor based pagination
방식이다.
OFFSET
을 사용하지 않고, WHERE
절로 조건을 걸어 요청한다.
아래와 같이 Query를 요청하면, 아래의 표와 같이 결과가 나올 것이다.
SELECT *
FROM BOARD
WHERE ID < 1001
ORDER BY ID DESC LIMIT 5;
그럼 위의 Query에 이어서 아래와 같이 요청한다면?
SELECT *
FROM BOARD
WHERE ID < 996
ORDER BY ID DESC LIMIT 5;
위와 같은 표처럼 Query의 결과가 출력될 것이다.
Pagination을 한 효과와 동일하게, 최신순으로 5개씩 잘려 결과가 반환되었다.
이제 해당 Query를 일반화하면 아래와 같이 요청할 수 있다.
SELECT *
FROM BOARD
WHERE ID < {마지막으로 요청한 ID}
ORDER BY ID DESC LIMIT 5;
마지막으로 요청한 ID
를 계속 갱신하며 요청하는 것이다.
해당 방식은 OFFSET
과 다르게 데이터를 모두 불러오지 않는다.
또한 WHERE
절로 조건을 걸어, Index
도 태울 수 있다.
따라서 Offset based
에 비해 훨씬 효율적이다!
실제 성능 테스트
DB에 200개의 더미 데이터, 100만개의 더미 데이터를 넣고 요청을 보내 보았다.
아래의 결과들은 200개의 더미 데이터에 대한 비교이다.
Offset based pagination
Cursor based pagination
적은 데이터에서는 offset based pagination
이 더 빠름을 확인할 수 있다.
아래의 결과들은 100만개의 더미 데이터에 대한 비교이다.
아래의 사진에서 100만개의 데이터가 생성되었음을 확인할 수 있다.
Offset based pagination
Cursor based pagination
위의 두 Query 모두 1 ~ 40
번 데이터를 요청하였다.
그 말은, 최신 순으로 가장 오래된 데이터 40
개를 훑은 것이다.
이번에는 확실히 cursor based pagination
이 빠름을 확인할 수 있다.
즉, 데이터가 많아질 수록 cursor based pagination
이 유리하다!
그럼 이 방식을 코드에서 어떻게 구현할 수 있을까?
JPA Pageable
Spring JPA
에서는 Pageable
이라는 굉장히 유용한 객체를 제공한다.
Pageable
객체를 사용하지 않는 경우엔, 아래와 같이 구현될 것이다.
프론트엔드 단에서 가장 마지막에 받은 커서를 요청에 함께 보내야한다.
그 결과 아래와 같이 10개씩 최신순으로 불러옴을 볼 수 있다.
JPARepository
를 사용하는 경우엔, 훨씬 깔끔하게 코드 작성이 가능하다.
Pageable
객체는 JPARepository
에서 Pagination을 구현하도록 한다.
먼저, 아래와 같이 Repository
를 Interface
로 구현한다.
JPARepository
는 메서드의 이름을 SQL
로 매핑하는 ORM
이다.
위의 경우, 매우 간단하게 한 줄로 id
기준 오름차순으로 게시글을 가져올 수 있다.
아래와 같이 Postman 을 통해 /test?page=1&size=10
의 경로로 요청을 보내면?
그 아래의 사진과 같이 2개의 Query가 날아감을 확인할 수 있다.
또한 요청의 결과가 원하는 대로 999990 ~ 999981
번 게시글임을 확인할 수 있다.
이렇게 JPARepository
는 매우 간편한 Query를 지원한다.
하지만 복잡한 요청을 보내야 하는 경우, JPARepository
만으로는 처리할 수 없다.
JPQL, QueryDSL
과 같은 기능을 통해, 직접 Query를 작성해야 할 때도 있다.