Spring Boot 데이터베이스 페이징 성능 전략
Spring Boot 환경에서 대용량 데이터를 효율적으로 다루기 위한 페이징 전략과 성능 고려사항을 정리한 기술적 설계
목차
개요
웹 애플리케이션에서 페이징은 응답 속도와 사용자 경험에 직접적인 영향을 준다. 특히 데이터가 커질수록 단순한 OFFSET 기반 페이징은 성능 저하를 유발한다. 이 글에서는 Spring Boot 환경을 기준으로 페이징 전략을 비교하고, JPA와 키셋(cursor) 페이징 구현 예, 운영 시 고려해야 할 성능 요인들을 정리한다.
페이징의 목적과 문제점
왜 페이징이 필요한가
대량의 레코드를 한 번에 반환하면 네트워크와 메모리 비용이 커진다. 따라서 필요한 만큼만 유저에게 전달하는 것이 핵심이다.
OFFSET 기반 페이징의 한계
- OFFSET이 커질수록 DB는 스킵할 행을 스캔하므로 비용 증가
- 동시성으로 인한 데이터 중복 또는 누락 가능성
- count 쿼리의 비용이 높아 전체 페이지 수 계산이 부담스러움
페이징 전략 비교
LIMIT / OFFSET
간단하고 구현이 쉽다. 작은 오프셋에서는 문제없다. 그러나 대형 데이터셋에서는 성능 저하가 심하다.
키셋(Cursor) 페이징
정렬 기준의 마지막 키를 기준으로 다음 페이지를 조회한다. DB는 인덱스를 사용해 빠르게 탐색할 수 있다. 대규모 목록 조회에 적합하다.
비교 요약
- OFFSET: 구현 용이성 높음, 큰 오프셋에서 비효율
- Cursor(키셋): 일관된 응답시간, 특정 정렬에 의존
- 복합 전략: 초기 페이지는 OFFSET, 심화 조회는 키셋 병용 가능
Spring Boot 적용 방법
JPA 기본 페이징
Spring Data JPA는 Pageable과 Page를 제공한다. 간단한 목록에서는 빠르게 적용 가능하다.
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findAll(Pageable pageable);
}
Controller에서는 Pageable을 받아 Page<User>를 반환한다. 다만 내부적으로 OFFSET을 사용하므로 대용량에서는 주의가 필요하다.
키셋 페이징 구현 예
키셋 페이징은 마지막 항목의 키를 전달받아 이후 데이터를 조회한다. 인덱스가 존재하면 성능이 좋다.
@Query("SELECT u FROM User u WHERE u.id < :lastId ORDER BY u.id DESC")
List<User> findByIdLessThanOrderByIdDesc(@Param("lastId") Long lastId, Pageable pageable);
// SQL 예시
SELECT * FROM users WHERE id < :lastId ORDER BY id DESC LIMIT :size;
프론트엔드는 각 페이지 응답에 마지막 ID를 포함한다. 다음 요청은 그 ID를 전달한다.
페이징 토큰 사용
마지막 키를 그대로 쓰면 안전하지 않은 경우가 있다. 이때 암호화된 페이징 토큰을 사용하면 무결성과 사용 편의성을 확보할 수 있다.
성능 고려사항
인덱스 설계
- 정렬과 검색에 사용되는 컬럼에 적절한 인덱스 적용
- 복합 정렬의 경우 복합 인덱스 검토
select 절 최적화
불필요한 컬럼 조회를 피한다. 필요한 컬럼만 DTO로 프로젝션하면 IO와 메모리 비용을 줄일 수 있다.
count 쿼리 대체 방안
전체 페이지 수가 비용이 클 때는 정확한 count 대신 '다음 페이지 존재 여부' 플래그로 대체한다. 또는 추정 카운트를 사용한다.
JPA 관련 주의
- fetch join 사용 시 페이징이 메모리로 로드되는 경우 주의
- 데이터 변환은 DB 조회 후 가능한 한 지연 처리
- N+1 문제는 EntityGraph나 join fetch, 또는 DTO 조회로 해결
모니터링과 테스트
실제 워크로드로 부하 테스트를 수행한다. EXPLAIN이나 실행계획을 확인해 병목을 찾는다. 지연 시간이 목표치를 벗어나면 쿼리와 인덱스를 재검토한다.
- 로드 테스트 도구로 다양한 오프셋과 키셋 시나리오 검증
- 프로파일링으로 GC, 메모리, 네트워크 병목 확인
결론
페이징 전략은 단일 해법이 없다. 소규모 데이터와 단순 페이지 네비게이션은 JPA의 Pageable로 충분하다. 반면 대규모 리스트나 깊은 페이지 접근이 잦은 서비스는 키셋(cursor) 페이징이 더 적합하다. 인덱스, 선택적 컬럼 조회, count 쿼리 대체, 그리고 적절한 모니터링이 함께해야 안정적 성능을 달성할 수 있다.