🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [42]일차
🚀 이전 회고와 관련되어서 "오프셋 기반 페이징"을 정리해보고자 한다.
Spring Data JDBC에 국한되지 않고 일반적인 SQL에 쓰이는 페이징 기법이어서
회고를 별도로 정리해야겠다는 생각이 들었다.
오프셋 기반 페이징
- Offset-based Pagination : SQL의 LIMIT과 OFFSET을 활용한 페이징 기법
- 큰 OFFSET 값이 들어가면 스캔해야 할 데이터가 많아짐 (인덱스가 없을 경우)
➡️페이지를 넘길수록 OFFSET 값이 커지면서속도가 느려짐 - 다양한 페이징 기법 예시
ex. 구글에서 페이지를 내리다보면 다음페이지를 눌러서 가는 것이 아닌 스크롤로 내림으로써
페이지를 넘기는 방식처럼 구현이되어있다.
➡️이러한 방식은 페이지에 해당되는 데이터만 꺼내는 방식으로
한번에 많은 데이터를 가져올 필요가 없으므로 성능향상에 있어 도움이 된다.
SELECT id, name, email FROM users LIMIT 10 OFFSET 20;
- LIMIT : 가져올 데이터 개수
- OFFSET : 앞의 데이터를 몇 개 건너뛸지
인터페이스 정의 (interface)
public interface CustomUserRepository {
Page<User> findAllUsersWithPagination(Pageable pageable);
// 페이지에 있는 정보를 기준으로 찾아준다.
}
인터페이스의 구현체 (class)
String query = "SELECT id, name, email FROM users LIMIT :limit OFFSET :offset";
- 페이지에 관련해서 MySQL은 limit을 제공한다.
- LIMIT :limit OFFSET :offset
:(콜론)이 들어가면 변수처럼 값이 바뀌어서 그곳에 들어감
(=LIMIT을 limit 변수로 바꿔서, OFFSET을 offset 변수로 바꿔서 들어간다는 의미) ➡️실제 값으로 치환될 변수
➡️MySQL의 페이징 처리를 위한 핵심적인 SQL 구문
LIMIT :limit → 가져올 데이터 개수 (한 페이지에서 몇 개 가져올 것인지)
OFFSET :offset → 건너뛸 데이터 개수 (어디서부터 가져올 것인지)
ex. 특정 페이지에서 데이터 가져오기
SELECT id, name FROM users LIMIT 10 OFFSET 10;
- 첫 번째 페이지는 LIMIT 10 OFFSET 0
- 두 번째 페이지는 LIMIT 10 OFFSET 10
- 세 번째 페이지는 LIMIT 10 OFFSET 20
❓? 와 : (콜론)의 차이점
Spring Data JDBC에서 페이징을 적용할 때
➡️위치 기반 바인딩(?) : 값의 순서가 중요하며, 값이 많아지면 관리가 어려움
➡️이름 기반 바인딩(:) : 가독성 좋음, 유지보수 쉬움
이 둘은 SQL 쿼리를 작성 시, 바인딩하는 방식이 다른 것이다.
?(물음표) 기법 | :(콜론) 기법 | |
설명 | 위치 기반 바인딩 : 순서에 따라 값이 대체 | 이름 기반 바인딩 : 변수명을 지정해서 값이 대체 |
예시 코드 | SELECT id, name FROM users LIMIT ? OFFSET ? |
SELECT id, name FROM users LIMIT :limit OFFSET :offset |
▶️실습 - Map 객체 만들기
@RequiredArgsConstructor
public class CustomUserRepositoryImpl implements CustomUserRepository{
private final NamedParameterJdbcTemplate jdbcTemplate;
@Override
public Page<User> findAllUsersWithPagination(Pageable pageable) {
// JdbcTemplate을 이용해서 실제 수행할 쿼리를 만듦
String query = "SELECT id, name, email FROM users LIMIT :limit OFFSET :offset";
List<User> users = jdbcTemplate.query(query, "test", "test", new BeanPropertyRowMapper<>(User.class));
return null;
}
}
- 정의되어있지 않다는 오류 발생
➡️원인 : 원하는 값을 넘겨보낼때 “test”, “test” 처럼 넘길 수 없고 map객체를 넘겨야한다.
➡️해결방법 : 저 인자에 넣을 부분에 Map객체를 미리 정의후 넣어주어야한다.
Map 객체 정의
// 키 String, 값에는 어떠한 값도 들어갈 수 있으므로 Object타입
Map<String, Object> params = new HashMap<>();
params.put("limit", pageable.getPageSize());
params.put("offset", pageable.getOffset());
- limit, offset이라는 키를 넣을때는 순서는 중요치 않아도 정확하게 매핑시켜주어야함
- 쿼리에 바인딩할 변수값을 params 맵에 저장
- pageable.getOffset() ➡️건너뛸 개수
- pageable.getPageSize() ➡️한 페이지에 가져올 개수
public Page<User> findAllUsersWithPagination(Pageable pageable)
- pageable 객체를 가져와서 그에 맞는 메소드를 사용할 수 있음
- limit : 페이지 사이즈에 대한 것으로 여기서는 "키"를 의미함
- Map 객체 정의 후 query()의 인자로 넣어주었지만 여전히 문제 발생
➡️해결방법 : JdbcTemplate가 아닌 JdbcTemplate의 자녀객체인 NamedParameterJdbcTemplate을 사용해야함
private final NamedParameterJdbcTemplate jdbcTemplate;
CustomUserRepository 클래스 - UserRepository의 구현체
@RequiredArgsConstructor
public class CustomUserRepositoryImpl implements CustomUserRepository{
private final NamedParameterJdbcTemplate jdbcTemplate;
@Override
public Page<User> findAllUsersWithPagination(Pageable pageable) {
// JdbcTemplate을 이용해서 실제 수행할 쿼리를 만듦
String query = "SELECT id, name, email FROM users LIMIT :limit OFFSET :offset";
// 키 String, 값에는 어떠한 값도 들어갈 수 있으므로 Object타입
Map<String, Object> params = new HashMap<>();
params.put("limit", pageable.getPageSize());
params.put("offset", pageable.getOffset());
List<User> users = jdbcTemplate.query(query, params, new BeanPropertyRowMapper<>(User.class));
String countAllDataQuery = "SELECT count(*) FROM users";
return PageableExecutionUtils.getPage(users, pageable,
() -> jdbcTemplate.queryForObject(countAllDataQuery, params, Long.class));
}
}
- @RequiredArgsConstructor
➡️final이 붙은 필드를자동으로 생성자 주입 - private final NamedParameterJdbcTemplate jdbcTemplate;
➡️ ? 대신 :변수명을 사용하는 Named Parameter 방식 사용 - public Page<User> findAllUsersWithPagination(Pageable pageable)
➡️Pageable을 사용하여 페이징 처리된 사용자 목록 조회
반환 타입은 Spring Data의 Page<User> - new BeanPropertyRowMapper<>(User.class)
➡️결과를 User 객체 리스트로 변환
- return 부분
페이지를 사용하려면 전체 데이터가 몇건 필요한지를 설정
ex. 만약 전체데이터가 100건일때
한 페이지에서 보여줄 데이터는 20개 위치 = 5페이지가 필요
이를 위해서 전체 데이터의 건수를 먼저 구해야함 - return PageableExecutionUtils.getPage(...)
➡️Object로 리턴될 것 - queryForObject(...)
➡️쿼리 수행 후 Object를 리턴
첫번째 인자 : countAllDataQuery를 가지고 수행
두번째 인자 : params를 통해 한페이지에 몇 건 씩 쓸지에 대한 정보를 가짐
세번째 인자 : 반환할 타입을 정의 (Long)
public interface UserRepository extends CrudRepository<User, Long>, CustomUserRepository {
List<User> findByName(String name);
}
- 이렇게 정의한 후 기존 UserRepository 인터페이스에 추가해줄 수 있다.
// 페이지 0번 페이지로 시작하는데 페이지 안에 담길 데이터의 사이즈는 3개로 설정
PageRequest pageRequest = PageRequest.of(0, 3);
Page<User> pageUsers = userRepository.findAllUsersWithPagination(pageRequest);
pageUsers.forEach(System.out::println);
- Page<User> pageUsers = userRepository.findAllUsersWithPagination(pageRequest);
➡️findAllUserWithPagination() 메소드의 반환값은 Page 타입 - >> 출력 : 유저 데이터의 가장 처음 3개 데이터가 출력될 것
ex. PageRequest.of(1, 5)
첫번째 페이지였던 3개의 데이터의 그 다음 데이터(=다음 페이지)부터 5개의 데이터가 출력될 것 - 페이지번호나 페이지 사이즈만 바꿔주어도 그에 알맞는 데이터를 알아서 가져다주는 것을 확인가능
정리하자면
➡️이 예제는 스프링 프레임워크가 전부 다 자동화해주는 것이 아니라 사용자가 직접 해서 만들수도 있다는 것
➡️Join이 많이 발생하는 복잡한 연산들은 사용자가 직접 구현해서 하는 것이 성능이 훨씬 좋다는 것
그 외 Spring Data에서 지원하는 다른 페이징 기법
- Keyset Pagination (커서 기반 페이징)
➡️OFFSET을 사용하지 않고 WHERE 조건을 활용해서 다음 페이지를 찾음
페이징 성능이 훨씬 빠름
ex. 실시간 데이터가 많은 서비스 (게시판, 피드, 로그 등) - Spring Data JPA의 Pageable 기반 페이징
➡️JPA가 내부적으로 LIMIT, OFFSET을 자동 적용
findAll(Pageable pageable) 같은 방식으로 간편하게 사용 가능
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_44일차_"페이징 처리" (0) | 2025.02.10 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_43일차_"친구목록 페이지" (1) | 2025.02.07 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_42일차_"Spring Data JDBC" (1) | 2025.02.06 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_41일차_"람다식 / 스트림 API" (0) | 2025.02.05 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_40일차_"Spring JDBC" (1) | 2025.02.04 |