"게시판 1페이지 조회는 0.01초 걸리는데, 10,000페이지 조회는 왜 3초가 걸릴까요?"
데이터베이스에서 수백만 건의 데이터를 한 번에 가져오는 것은 자살행위입니다. 그래서 우리는 데이터를 잘라서(Paging) 가져옵니다. 하지만 우리가 흔히 쓰는 오프셋 방식은 데이터가 많아질수록 치명적인 성능 저하를 일으킵니다. 이를 해결하기 위해 등장한 것이 커서(No-Offset) 방식입니다.
1. 방식 1: 오프셋 페이지네이션 (Offset Pagination) - "전통의 강자"
우리가 게시판 하단에서 흔히 보는 [1] [2] [3] ... [10] 형태의 UI를 구현할 때 사용하는 방식입니다. SQL의 OFFSET과 LIMIT 문법을 사용합니다.
특징
- 페이지 번호 이동: 사용자가 5페이지에서 100페이지로 바로 건너뛸 수 있습니다 (Random Access).
- 구현 용이성: SQL이 직관적이고 구현이 매우 쉽습니다.
코드 예시 (SQL & JPA)
SQL
-- 10,000번째 데이터부터 10개를 가져와라
SELECT * FROM orders
ORDER BY id DESC
LIMIT 10 OFFSET 10000;
Java
// Spring Data JPA
PageRequest pageRequest = PageRequest.of(1000, 10, Sort.by("id").descending());
orderRepository.findAll(pageRequest);
치명적 문제점 (Deep Pagination)
DB는 OFFSET 10000을 처리하기 위해 앞에 있는 10,000개를 읽은 뒤 버리는 작업을 수행합니다.
- 1페이지 조회: 10개 읽음 (빠름)
- 100만 페이지 조회: 1,000,010개 읽고, 1,000,000개 버림 (매우 느림)
- 데이터가 많아질수록 쿼리 속도가 $O(N)$으로 느려집니다.
2. 방식 2: 커서 페이지네이션 (Cursor / Keyset Pagination) - "성능 최적화"
페이스북, 인스타그램, 슬랙의 타임라인처럼 "무한 스크롤(Infinite Scroll)" 환경에서 표준으로 쓰이는 방식입니다. "몇 번째 페이지"라는 개념 대신 **"마지막으로 본 데이터의 다음 것"**을 가져옵니다.
특징
- No-Offset: OFFSET을 쓰지 않습니다. 대신 **인덱스가 걸린 Key(커서)**를 WHERE 절 조건으로 사용합니다.
- 빠른 속도: 몇 번째 페이지를 조회하든 항상 일정한 속도($O(1)$ 혹은 $O(\log N)$)를 보장합니다.
코드 예시 (SQL & QueryDSL)
SQL
-- "방금 본 마지막 주문 ID(cursor)가 9990라면, 그보다 작은 거 10개 줘"
SELECT * FROM orders
WHERE id < 9990
ORDER BY id DESC
LIMIT 10;
Java
// QueryDSL 예시
public List<Order> findOrders(Long lastOrderId, int size) {
return queryFactory
.selectFrom(order)
.where(
// 첫 페이지 요청(null)이면 조건 없음, 아니면 커서 조건 적용
lastOrderId != null ? order.id.lt(lastOrderId) : null
)
.orderBy(order.id.desc())
.limit(size)
.fetch();
}
장단점
- 장점: 데이터가 10억 건이 있어도 첫 페이지와 마지막 페이지의 조회 속도가 똑같이 빠릅니다. (인덱스를 타고 바로 찾아가기 때문)
- 단점:
- 임의 접근 불가: "100페이지로 바로 이동" 기능을 구현할 수 없습니다. 오직 "다음, 다음"만 가능합니다.
- 구현 복잡: 정렬 조건이 복잡해지거나(중복되는 정렬 기준 등), 유니크 키(Unique Key)가 없으면 구현이 까다롭습니다.
3. 실무 비교: 언제 무엇을 쓰는가?
| 구분 | Offset Pagination (전통적) | Cursor Pagination (현대적) |
| SQL 문법 | LIMIT 10 OFFSET 100 | WHERE id < cursor LIMIT 10 |
| UI 형태 | 번호 매기기 ([1] [2] [3]) | 무한 스크롤 / 더보기 버튼 |
| 성능 (대용량) | 뒤로 갈수록 매우 느려짐 | 항상 빠름 (Index Seek) |
| 데이터 일관성 | 조회 중 데이터 추가/삭제 시 중복/누락 발생 | 비교적 안전함 (커서 기준) |
| 추천 상황 | 관리자(Admin) 페이지, 게시판 | 모바일 앱 피드, 타임라인, 대용량 API |
결론
데이터가 적거나(수만 건 이하), 관리자 페이지처럼 **"특정 페이지로 점프"**가 반드시 필요하다면 오프셋 방식을 쓰세요. 가장 쉽고 직관적입니다.
하지만 모바일 앱 백엔드를 개발하거나, 데이터가 수백만 건 이상 쌓이는 대용량 서비스라면 반드시 커서(No-Offset) 방식을 도입해야 DB 부하를 막을 수 있습니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| ORM 패턴(ORM Pattern) 완벽 정리 (0) | 2025.12.07 |
|---|---|
| 사가 패턴(Saga Pattern) 완벽 정리: 코레오그래피(Choreography) vs 오케스트레이션(Orchestration) (0) | 2025.12.07 |
| 콜백 패턴(Callback Pattern) 총정리: 동기(Blocking) vs 비동기(Non-blocking) (0) | 2025.12.07 |
| 동시성 제어 패턴: 비관적 락(Pessimistic) vs 낙관적 락(Optimistic) (1) | 2025.12.07 |
| 의존성 주입(DI) 패턴 완벽 정리 (0) | 2025.12.07 |