서비스를 운영하면서 게시판 페이징, 스크롤 페이징을 경험해보았고,
페이징을 심도있게 공부하면서 실질적인 성능과 유지보수에 관해 고민했던 내용을
백엔드 개발자의 입장으로서 두서없이 적어보려고 한다.
정답이 아니라 하나의 방법으로서...
0. 페이징을 하는 이유는? [why]
결론부터 말하면 페이징은 속도는 빠르게, 부하는 적게 하기 위해 지금 당장 필요한 데이터만 가져올 수 있도록 데이터를 분리하는 작업이다.
예를 들어, 10만 개의 게시물 데이터가 있다고 가정했을 때, 페이징 없이 10만 개의 데이터를 한 번에 가져온다 생각해보자.
먼저 DB에서 쿼리로 10만개의 데이터를 조회한다. 정렬과 필터조건, 조인 여부에 따라 시간은 천차만별이겠지만 행여 그러한 복잡한 로직이 없더라도 쿼리를 순식간에 실행해 10만 개의 데이터를 서버에 전달하면 이미 부하를 받는다. (서버 설정에 따라서는 아예 전달이 안 되어 에러가 날 수 있다) 이유는 데이터 묶음 자체가 거대하기에 서버 메모리에 문제가 발생하기 때문이다. 행여 Back 전달받아 API로 Front에 넘겼다고 하더라도 브라우져에서 10만 개의 데이터를 DOM으로 그리는 것 또 한 엄청난 부하를 발생시킨다.
10만 개의 게시물이 사람의 사용성 기준으로 한번에 필요한 경우도 없을 뿐더러 필요하더라도 부하를 위하여 페이징을 해야 한다.
1. 게시판 페이징은 무엇인가? [what]
게시판 페이징은 모든 개발자의 시험 관문 같은 기능이자, 모든 서비스들의 핵심 기능을 담당하는 중요한 기능이다. 아래는 티스토리의 게시물에 대한 게시판 페이징 처리가 된 화면이다.
1페이지는 https://uminoh.tistory.com/category/Googling?page=1 라는 url을 가지고
2페이지는 https://uminoh.tistory.com/category/Googling?page=2 라는 url을 가진다.
12개의 글을 10개씩 끊어 1페이지와 2페이지로 나누어 표현한다.
보통 OFFSET과 LIMIT을 활용해 쿼리를 작성하고 결과값을 받는다.
SELECT * FROM BOARD LIMIT 10 OFFSET 0; --1 PAGE
SELECT * FROM BOARD LIMIT 10 OFFSET 10; -- 2 PAGE
이렇게 구현하는 것 자체는 크게 어렵지 않다. 기본이니 만큼 여러 개의 데이터를 분리하는 작업은 어렵지 않다.
2. 백엔드에서 페이징이 가능한 조회 API를 만들때 고려할 점? [how]
처음 게시판 페이징을 만들때는 단순 기능에만 집중했고, 또 프론트와 백엔드 모두 내가 작업했기 때문에 아무 생각없이 프론트에서 LIMIT과 OFFSET을 직접 계산해서 API로 그 값들을 담아 호출했다. 그래서 백엔드에서는 그 자리 그대로 LIMIT과 OFFSET을 세팅해주어 DB 쿼리를 작성하면 되었다.
하지만, 실무에서는 프론트가 아닌 백엔드 API에서 LIMIT과 OFFSET을 계산하고 있었다. 이유는 모바일 프론트 개발에 있었다. 안드로이드, IOS 개발자도 같은 API를 활용해 개발해야 했다. 물론, 각자 프론트 개발자가 페이징 로직을 직접 구현해 전달해주어도 되지만, 모든 프론트의 개발 조건이 달라질 수도 있고, 행여 로직을 잘못 짜는 경우 모바일에서 직접 로직을 변경해야 하는데, 모바일은 스토어 배포 심사라는 프로세스가 있어 서버와 달리 즉시 배포가 불가능했다. 여러모로 로직은 백엔드에 두는 편이 좋았던 것이었다.
(페이징 기능을 계기로 백엔드에 할 수 있는 부분은 최대한 백엔드에서 처리해줘야 함을 느꼈다.)
3. 스크롤 페이징 (무한 스크롤) [what]
'게시판 페이징'만큼 대중적인 페이징에는 '스크롤 페이징'이 있다. 게시판 페이징보다는 난이도가 있다고 보는 편인 것 같다. 맨 처음 스크롤 페이징을 만들었을 때는 안드로이드 개발 때였다. 모바일은 스크롤 페이징을 하더라도 화면에 보이는 템플릿뷰는 최대한 유지하고 재활용하면서 데이터를 갈아끼는 형식으로 작동했는데 이유는 모바일 디바이스가 메모리가 적기 때문이었다. 그때만 해도 그냥 데이터를 몽땅 넣고 스크롤 하면서 아래의 데이터들이 불러오는 과정만 구현되면 꽤 멋지다고 생각했던 것 같다.
페이스북 타임라인과 같이 정말 끝이 보이지 않는 스크롤 페이징도 있지만, 리스트의 끝이 금방 보이는 곳에도 스크롤 페이징을 써야 할 때가 있다. 예를 들어, 티스토리의 이웃목록을 가져오는데 이웃이 1,011명이라고 가정해보자. 1,011명 정도면 되게 가볍게 API가 호출될 것 같다고 예상한다. 하지만, 정렬과 JOIN 등을 이유로 굉장히 느려졌다고 하면, 한 50개 정도만 보여주고 스크롤에 따라 페이징해야 한다. 여기서 2가지 과제가 있다. 스크롤 어느 지점에서 2번째 페이징을 부르기 시작할 것인가, 마지막 페이징인지를 어떻게 알 것인가? (사실 과제는 몇 가지 더 있지만 다음에 다룬다.)
4. 스크롤 어느 지점에서 2번째 페이징을 부르기 시작할 것인가? [how]
보통 2가지 방식으로 처리하는데, 맨 끝에 도달하면 호출하는 케이스, 어느 적정 스크롤 퍼센트에서 호출하는 케이스.
맨 끝에 도달하면 호출하는 케이스는 약간 자연스럽지 않다. 스크롤하다 잠깐 멈추고 데이터가 아래에 붙는다. 잠깐 멈춤은 내가 마우스휠로 쭉쭉 내리는 데에 방해요소가 된다.
어느 적정 스크롤 퍼센트(예를 들어, 60%)에서 호출하는 케이스는 꽤 자연스러운 스크롤을 만날 수 있다. (물론, API가 느리면 답이 없다) 위의 가정을 이어 티스토리 이웃목록 1,011명을 50개씩 나누어 스크롤 페이징을 한다고 가정해보자. 20번째 페이징이 끝났을 때 데이터는 11개가 남았다. 21번째 페이징을 통해 11개를 더 가져왔다. 그리고 이 시점, 스크롤에 따라 22번째 페이징을 해야 할까? 말아야 할까?
5. 페이징의 끝 [how]
보통 22번째 페이징을 한다. 그리고 데이터가 0개임을 확인하고, 앞으로 스크롤 페이징을 시도하지 않기로 한다. 하지만 분명 22번째 페이징 API를 호출한 것이고 이는 비용에 해당한다. 모든 페이징의 끝에 1번에 호출이 더 있다는 사실이 찜찜하다면 방법이 있다. 다시 위의 가정으로 20번째 페이징이 끝났을 때 데이터는 11개가 남았을 때 21번째 페이징 API에서 50 + 1개를 조회해본다. 51개가 있다면 다음 데이터는 있다고 볼 수 있고 그게 아니라면 다음 데이터는 없다. (물론, 여기서 51개의 조회값을 Response 값으로 내보내면 안 된다. 마지막 떼고 50개만 내보내야 함) 이렇게 하면 22번째 페이징을 할 필요는 없다. 이걸 프론트에서는 체크 불가능하고, 백엔드에서 체크해 Response 값으로 리턴해주어야한다.
6. NEXT_PAGING_YN [how]
매번 API Response 값에 'NEXT_PAGING_YN' (다음 페이징 유무) 값을 내려주면 프론트는 그 플래그 값을 활용해 스크롤을 제어하면 된다. API를 매번 50개 조회할 것을 51개씩 조회하는 비용과 1번 더 API를 호출되는 비용을 따지면 약간 애매한 부분은 있어 때에 따라 선택하면 될 것 같다. 오히려 좋은 건 서버에서 'NEXT_PAGING_YN' 이라는 플래그 값을 내려주는 부분이다. 분명 프론트에서 데이터가 하나도 안 내려오는 경우 스크롤이 더 없다고 판단해서 스크롤에 따른 API 호출을 막으면 되긴하나 이도 프론트마다 로직이 다 다를 수 있다. 실제 사례로 모바일에서 스크롤을 엄청나게 하다가 제대로 스크롤 API 호출이 막히지 않아 맨 끝점에서 무한 스크롤이 돌아 서버가 뻗은 경험이 있다. 프론트의 로직 중 백엔드에서 공통으로 로직을 관리할 수 있다면 그게 낫다고 느꼈다. 그래서 스크롤 페이징은 백엔드를 믿고 NEXT_PAGING_YN 플래그를 따라가라고 전달한다.
7. 마무리
나는 일단 디폴트로 모든 조회 API를 페이징이 가능하게 만든다. request 값으로 PAGE_NUMBER (페이지수)와 PAGE_PER_COUNT(페이지별 건수)를 받고 response 값으로 조회 목록과 NEXT_PAGING_YN(다음 페이징 유무) 값을 준다.
실제 서비스상에서는 대부분의 쿼리가 JOIN이나 서브쿼리가 많이 포함되어 복잡한 쿼리문이 되기 때문에 페이징은 무조건 성능상에 이 점이 있다. 그래서 페이징을 적극적으로 활용하는 것이 좋다고 본다!!!
'Deep Dive Series > Paging' 카테고리의 다른 글
[페이징 톺아보기 3] 두번째 페이징이 잦을때, IN절 페이징 (0) | 2021.08.07 |
---|---|
[페이징 톺아보기 2] 두 테이블의 유니온 페이징 (정석이 아니라 선택) (0) | 2021.08.02 |
[페이징 톺아보기 1] 가장 효율적인 커서 기반 페이징 (0) | 2021.08.02 |