계층별 DTO 명명 표준은 PagingResult에 totalCount를 두는 대가를 이렇게 적었다.
PagingResult 가 totalCount 를 필드로 가지면
매 목록 조회에 COUNT 쿼리를 한 번 치게 되어 offset 페이징을 계약으로 박는다.
「매 조회마다 한 번 더」가 페이징 방식 표준이 커서 페이징을 고른 이유이기도 하다. 그래서 정말 매번 나가는지 쟀다.
주장 — Page 조회는 목록마다 COUNT를 한 번 더 친다
Page 를 반환하는 Spring Data 조회는 목록 조회마다 COUNT 쿼리를 한 번 더 친다.
근거 종류는 1(기술 사실)이다.
반증 조건 — 한 경우라도 COUNT가 안 나가면 반증이다
- refuted — 두 환경 중 하나라도 세 경우 중 하나에서 COUNT 문이 나가지 않는다
- supported — 두 환경 모두 세 경우 전부 COUNT 문이 나간다
- inconclusive — 조회가 실패하거나 SQL을 한 문장도 관측하지 못한다
설계 — 페이지 위치를 셋으로 나누고 두 가지 방법으로 센다
행 25개를 넣고 세 경우를 각각 다른 트랜잭션으로 조회했다.
(가) size 50, page 0 첫 페이지인데 결과가 페이지 크기보다 적다
(나) size 10, page 2 마지막 페이지 (5행)
(다) size 10, page 0 중간 페이지
세는 방법을 둘로 뒀다. Hibernate 통계의 PreparedStatement 수와 StatementInspector가 본 SQL 문자열이다. 수만 세면 「무엇이 나갔는지」를 모르고, 문자열만 보면 「몇 번 나갔는지」를 놓친다. 첫 조회의 초기화 문장이 섞이지 않게 웜업을 한 번 돌린 뒤 셌다.
DB는 이 컴퓨터의 로컬 PostgreSQL 17이고 실험 전용 데이터베이스를 만들고 지웠다. 환경은 Spring Boot 3.5.16과 4.1.1이다.
결과 — 세 경우 중 둘에서 COUNT가 없었다
PROBE boot=3.5.16 (가) 문장1 COUNT없음 행25 총계25
(나) 문장1 COUNT없음 행5 총계25
(다) 문장2 COUNT나감 행10 총계25
PROBE boot=4.1.1 (같음)
판정은 refuted다. 두 세는 방법이 세 경우 모두 같은 수(1/1/2)를 냈다.
예상과 달랐던 점 — COUNT를 생략하고도 총계가 정확했다
COUNT가 안 나간 두 경우에도 getTotalElements()는 정확히 25를 돌려줬다.
(가) offset 0 + 받은 행 25 = 25
(나) offset 20 + 받은 행 5 = 25
총계가 산술로 복원되는 자리에서는 쿼리를 안 친다. 즉 「totalCount를 들고 다니면 매번 COUNT」는 참이 아니고, 「페이지 위치에 따라 친다」가 실제 모양이다.
이 실험은 어느 경우가 실제 트래픽에서 얼마나 자주 나오는지는 재지 않았다. 목록 첫 화면이 대부분이라면 COUNT는 거의 안 나가고, 깊은 페이지를 훑는 화면이라면 매번 나간다. 비용의 크기는 화면 사용 패턴에 달려 있다.
이 결과가 표준에 남기는 것 — 커서 페이징을 고른 근거가 약해진다
두 글이 걸린다. 계층별 DTO 명명 표준은 대가를 과장했고, 페이징 방식 표준은 그 대가를 근거의 하나로 썼다. 커서 페이징을 고른 이유가 통째로 무너지는 것은 아니다 — 깊은 offset이 앞 행을 읽고 버리는 문제는 이 실험이 건드리지 않았다.
규칙은 바로 고치지 않는다. 표준 글에 > 반증: 줄을 달고 kind: 모순 이슈에서 근거 문장을 어떻게 다시 적을지 정한다.
정리
- 첫 페이지가 통째로 들어오거나 마지막 페이지면 COUNT가 나가지 않는다. 두 Boot 버전이 같았다
- COUNT를 생략해도 총계는 정확했다. offset과 받은 행 수로 복원된다
- 중간 페이지에서는 나간다. 문장 2개가 관측됐다
- 재지 않은 것 — 실제 화면에서 세 경우의 비율, 깊은 offset의 비용
자신만의 철학을 만들어가는 중입니다.
댓글남기기