본 포스트는 김영한 님의 실전! 스프링 데이터 JPA 강의를 토대로 작성하였습니다.
이전 포스트에 이어 스프링 데이터 JPA가 제공하는 다양한 기능들에 더 알아보자.
스프링 데이터 JPA는 다양한 반환 타입을 지원하는데, 대표적으로 다음과 같다.
List<Member> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional
이 외에도 좀 이따 알아볼 Page, Slice 등 다른 반환타입들을 지원하니 자세한 건 공식 문서를 참고하면 된다.
❗️ 참고
JPA에서는 단건 조회 시 결과 값이 없다면 getSingleResult에서 NoResultException 예외가 발생했지만, Spring Data JPA에서는 null을 반환한다.
또한 Collection 형태로 반환 받을 경우 결과 값이 없다고 하더라도 빈 Collection을 반환한다.
스프링 데이터 JPA에서는
org.springframework.data.domain.Sort
org.springframework.data.domain.Pageable
두 가지의 라이브러리로 페이징 기능을 지원한다. 경로에서 알 수 있듯이 springframework에 속하기 때문에 ORM 상관없이 사용할 수 있다.
코드로 사용 방법을 알아보자.
Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Sort sort);
반환형이 Page, Slice로 반환하면 되고, 특이한 점은 인자로 Pageable과 Sort라는 객체를 넘겨주면 된다.
먼저 Pageable부터 알아보자.
//인터페이스 함수 정의
public interface MemberRepository extends Repository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
//실제 사용
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC,"username"));
Page<Member> page = memberRepository.findByAge(10, pageRequest);
다음은 age가 10인 member를 페이징 해오는 코드이다.
PageRequest 라는 객체를 먼저 만드는데, 이 클래스가 아까 본 Pageable로 들어가는 것이다.
PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지, 두 번째 파라미터에는 조회할 데이터 수를 입력한다. (시작은 0부터이다.)
또한 세 번째 인자의 경우 있어도 되고 없어도 되지만, 'username' 을 기준으로 내림차순 정리하여 반환해달라는 의미이다.
Page 객체로 값을 넘겨받으면 다음과 같은 많은 기능이 제공된다.
List<Member> content = page.getContent(); //조회된 데이터
assertThat(content.size()).isEqualTo(3); //조회된 데이터 수
assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호
assertThat(page.isFirst()).isTrue(); //첫번째 항목인가?
assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
테스트 코드의 일부이지만 중요한 건 page. 뒤의 기능들을 보면 웬만한 필요한 기능들은 다 제공한다.
실제 조회 기능을 사용할 경우 API 통신을 할 때 Page 객체를 바로 반환하면 Json으로 파싱되어 넘어가게 되는데 다음과 같다.
@GetMapping("/api/list")
@Transactional
public Page<Member> list() {
Member member1 = new Member("member1", 10);
Member member2 = new Member("member2", 10);
Member member3 = new Member("member3", 10);
Member member4 = new Member("member4", 10);
Member member5 = new Member("member5", 10);
memberRepository.save(member1);
memberRepository.save(member2);
memberRepository.save(member3);
memberRepository.save(member4);
memberRepository.save(member5);
em.flush();
em.clear();
PageRequest pageRequest = PageRequest.of(0, 3);
Page<Member> page = memberRepository.findByAge(10, pageRequest);
return page;
}
응답 결과는
{
"content": [
{
"id": 1,
"username": "member1",
"age": 10,
"team": null
},
{
"id": 2,
"username": "member2",
"age": 10,
"team": null
},
{
"id": 3,
"username": "member3",
"age": 10,
"team": null
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 3,
"sort": {
"empty": true,
"unsorted": true,
"sorted": false
},
"offset": 0,
"paged": true,
"unpaged": false
},
"last": false,
"totalElements": 5,
"totalPages": 2,
"first": true,
"size": 3,
"number": 0,
"sort": {
"empty": true,
"unsorted": true,
"sorted": false
},
"numberOfElements": 3,
"empty": false
}
content 안에 객체 정보가 담기고 pagable 안에 페이징에 관련된 모든 정보가 담긴다.
하지만 엔티티를 직접 반환하는 것은 좋지 못한 방법이기 때문에 DTO로 변환해서 반환하는 것이 좋다. Pageable은 이 기능도 제공한다.
PageRequest pageRequest = PageRequest.of(0, 3);
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto(m.getUsername(), m.getAge()));
return dtoPage;
위의 데이터 저장 과정은 동일하며 map을 이용해 Member 객체를 Dto 객체로 생성하여 매핑한다. 이를 반환하면 위의 결과와 형식은 동일하지만 Dto에 담긴 값만 담아 전달할 수 있게 된다.
Page 인터페이스가 Slice 인터페이스를 상속하고 있다. 즉 위에서 본 isFirst, hasNext 등 대부분의 기능은 Slice에서 정의하고 있다. Page는 전체 데이터 개수를 알아야하기 때문에 count 쿼리를 함께 한다.
그러나 Slice는 전체 데이터 개수를 반환하는 함수가 없기 때문에 count 쿼리를 날리지 않으며, limit + 1 개의 데이터를 조회하여 가져온다. 즉 0부터 3개씩 가져오라 하면 Slice는 4개씩 가져온다.
❗️ 참고
카운트 쿼리를 분리할 수 있는 방법이 있다.@Query(value = "select m from Member m left join m.team t", countQuery = "select count(m.username) from Member m") Page<Member> findMemberAllCountBy(Pageable pageable);
다음과 같이 countQuery로 분리할 수 있는데, 분리하는 이유는 카운트 쿼리를 할 때 불 필요한 join을 막을 수 있기 때문이다.
즉, 기존 쿼리만 있다면 join이 들어가 있기 때문에 Page의 특성상 반드시 카운트 쿼리를 해야 하고 이 때 join도 같이 실행된다. 그리고 이는 성능 저하로 이어진다 (카운트는 무겁다!)
따라서 다음과 같이 분리하면 단순히 Member 테이블만 가지고 카운트를 수행한다.
📣 추가
스프링 부트가 3.XX로 넘어오면서 하이버네이트 6을 사용한다. 그리고 하이버네이트 6에서는 위의 join과정에서 Lazy일 경우 join을 하지 않는다. (기존에는 했었음) 따라서 join 뒤에 fetch를 붙여 team도 함께 쿼리해오도록 하면 원래와 동일한 경우를 재현할 수 있다.
뭐가 많다... 다음 포스트에서 스프링 데이터 JPA에 대해 마지막으로 정리를 해보자.