페이징 처리와 정렬은 전통적으로 SQL을 공부하는데 반드시 필요한 부분이다.
오라클은 '인라인 뷰(inline view)'를 알아야만 하고, MySQL은 limit를 알아야만 했다.
JPA는 내부적으로 이런 처리를 'Dialect'라는 존재를 이용해서 처리한다.
그래서 개발자들은 SQL이 아닌 API의 객체와 메서드를 사용하는 형태로 페이징 처리를 할 수 있게 된다.
Spring Data JPA에서 페이징 처리와 정렬은 특이하게도 findAll()이라는 메서드를 사용한다.
findAll()는 JpaRepository 인터페이스의 상위인 PagingAndSortRepository의 메서드로 파라미터로 전달되는 Pageable이라는 타입의 객체에 의해서 실행되는 쿼리를 결정하게 된다.
주의할 점은 리턴 타입을 Page<T> 타입으로 지정하는 경우에는 반드시 파라미터를 Pageable 타입을 이용해야 한다는 점.
Pageable 인터페이스
페이지 처리를 위한 가장 중요한 존재 : org.springframework.data.domain.Pageable 인터페이스
Pageable 인터페이스는 페이지 처리에 필요한 정보를 전달하는 용도의 타입으로, 인터페이스이기 때문에
실제 객체를 생성할 때는 구현체인 org.springframework.data.domain.PageRequest라는 클래스를 사용한다.
PageRequest 클래스의 생성자는 특이하게도 protected로 선언되어 new를 이용할 수 없다.
객체를 생성하기 위해서는 static한 of()를 이용해서 처리한다.
PageRequest 생성자를 보면 page, size, Sort라는 정보를 이용해서 객체를 생성한다.
이게 무슨 이야기인지 해석을 해보면
PageRequest는 Pageable 인터페이스를 상속받았기 때문에
Pageable pageable = PageRequest.of(int page, int size, Sort sort);
이런 관계가 가능하다는 것 같다.
원래였으면
Pageable pageable = new Pageable(); 이런 식으로도 구성할 수 있으나.
protected가 되어있기 때문에 new 를 사용할 수 없고 static한 of()를 사용해야 Pageable객체를 생성할 수 있다.
PageRequest.of에는 몇 가지의 형태가 존재한다.
- of(int page, int size) : 0부터 시작하는 페이지 번호와 개수(size), 정렬이 지정되지 않음
- of(int page, int size, Sort.Direction direction, String ...props) : 0부터 시작하는 페이지 번호와 개수, 정렬의 방향과 정렬 기준 필드들
- of(int page, int size, Sort sort) : 페이지 번호와 개수, 정렬 관련 정보
페이징 처리
@Test
public void testPageDefault(){
// 1페이지 10개
Pageable pageable = PageRequest.of(0,10);
Page<Memo> result = memoRepository.findAll(pageable);
System.out.println(result);
}
Page 객체로 리턴을 받는다는게 처음에 의아했다.
보통 Mybatis를 사용했을 때는 List<엔티티객체> result로 리턴을 받았기 때문이다.
Page 타입이 신기한 이유 -> 단순히 해당 목록만 가져오는게 아니라 실제 페이지 처리에 필요한
전체 데이터 개수를 가져오는 쿼리 역시 같이 처리하기 때문이다.
select
field1, field2
from
entity
limit ?;
select
count(entity.mno)
from
entity
첫 번째 쿼리에서는 MariaDB에서 페이징 처리에 사용하는 limit 구문이 사용되는 것을 볼 수 있다.
두 번째 쿼리에서는 count()를 이용해서 전체 개수를 처리하는 것을 볼 수 있다.
findAll()에 Pageable 타입의 파라미터를 전달하면 페이징 처리에 관련된 쿼리들을 실행하고, 이 결과들을 이용해서 리턴 타입으로 지정된 Page<엔티티 타입> 객체로 저장한다.
@Test
public void testPageDefault(){ // 1페이지 10개
Pageable pageable = PageRequest.of(0, 10);
Page<Memo> result = memoRepository.findAll(pageable);
System.out.println(result);
System.out.println("------------------------------------------------");
System.out.println("Total Pages: " + result.getTotalPages()); // 총 몇
System.out.println("Total Count: " + result.getTotalElements); // 전체 개수
System.out.println("Page Number: " + result.getNumber()); // 현재 페이지 번호 0부터 시작
System.out.println("Page Size: " + result.getSize()); // 페이지당 데이터 개수
System.out.println("has next page?: " + result.hasNext()); // 다음 페이지
System.out.println("first page?: " + result.isFirst()); // 시작 페이지(0) 여부
}
99개의 데이터가 존재하는 상황을 가정하면 결과는?
Page 1 of 10 containing org.zerock.ex2.entity.Memo instances
---------------------------------------------------------------------------
Total Pages: 10
Total Count: 99
Page Number: 0
Page Size: 10
has next page?: true
first page? true
10개씩 페이징 처리를 하기 때문에 전체 페이지 수는 10페이지이고,
99개의 데이터가 존재하는 등 필요한 정보를 가져오는 것을 볼 수 있다.
정렬 조건 추가하기
페이징 처리를 담당하는 PageRequest에는 정렬과 관련된 org.springframework.data.domain.Sort 타입을 파라미터로 전달할 수 있다.
@Test
public void testSort(){
Sort sort1 = Sort.by("mno").descending();
Pageable pageable = PageRequest.of(0, 10, sort1);
Page<Memo> result = memoRepository.findAll(pageable);
result.get().forEach(memo -> {
System.out.println(memo);
});
}
쿼리 메서드(Query Methods) 기능과 @Query
JpaRepository의 쿼리 메서드라는 기능과 JPQL(Java Persistence Query Langugage)라고 불리는 객체지향 쿼리.
예를 들어 특정한 범위 Memo 객체를 검색하거나, like 처리가 필요한 경우, 여러 검색 조건이 필요한 것에 대한 부분이다.
Spring Data JPA의 경우에는 이러한 처리를 위해서 다음과 같은 방법을 제공한다.
- 쿼리 메서드 : 메서드의 이름 자체가 쿼리의 구문으로 처리되는 기능
- @Query : SQL과 유사하게 엔티티 클래스의 정보를 이용해서 쿼리를 작성하는 기능
- Querydsl 등의 동적 쿼리 처리 기능
쿼리 메서드(Query Methods)
메서드 이름 자체가 질의(query)문이 되는 흥미로운 기능
주로 findBy나 getBy로 시작하고 이후에 필요한 필드 조건이나 And, Or와 같은 키워드를 이용해서 메서드의 이름 자체로 질의 조건을 만들어낸다.
쿼리 메서드의 관련 키워드는 주로 SQL에서 사용되는 키워드와 동일하게 작성되어 있다.
쿼리 메서드는 사용하는 키워드에 따라서 파라미터의 개수가 결정된다.
- select를 하는 작업이라면 List 타입이나 배열을 이용할 수 있다.
- 파라미터에 Pageable 타입을 넣는 경우에는 무조건 Page<E> 타입
예를 들어 Memo 객체의 mno 값이 70부터 80사이의 객체들을 구하고 mno의 역순으로 정렬하고 싶다면
다음과 같은 메서드를 MemoRepository 인터페이스에 추가하게 된다.
public interface MemoRepository extends JpaRepository<Memo, Long>{
List<Memo> findByMnoBetweenOrderByMnoDesc(Long from, Long to);
}
작성된 메서드의 이름을 보면 mno를 기준으로 해서 between 구문을 사용하고 order by가 적용될 것임을 알 수 있습니다.
테스트 코드
@Test
public void testQueryMethods(){
List<Memo> list = memoRepository.findByMnoBetweenOrderByMnoDesc(70L, 80L);
for(Memo memo : list){
System.out.println(memo);
}
}
select
memo0_.mno as mno1_0_
memo)_.memo_text as memo_tex2_0
from
memo memo0_
where
memo0_.mno between ? and ?
order by
memo0_.mno desc;
쿼리 메서드와 Pageable의 결합
만일 목록을 원하는 쿼리를 실행해야 한다면 대부분의 경우 앞의 예제와 같이 'OrderBy' 키워드 등을 사용해야 하기 때문에
메서드의 이름도 길어지고 혼동하기도 쉽다.
다행스럽게도 쿼리 메서드는 Pageable 파라미터를 같이 결합해서 사용할 수 있기 때문에 정렬에 관련된 부분은 Pageable로 처리해서 좀 더 간략한 메서드를 생성할 수 있다.
public interface MemoRepository extends JpaRepository<Memo, Long>{
List<Memo> findByMnoBetweenOrderByMnoDesc(Long from, Long to);
Page<Memo> findByMnoBetween(Long from, Long to, Pageable pageable);
}
정렬 조건은 Pageable을 통해서 조절할 수 있기 때문에 좀 더 간단한 형태의 메서드 선언이 가능해지는 것을 볼 수 있다.
Pageable 파라미터는 모든 쿼리 메서드에 적용할 수 있으므로 일반적인 경우에는 쿼리 메서드에 정렬 조건은 생략하고 만드는 경우가 많다.
@Test
public void testQueryMethodWithPageable(){
Pageable pageable = PageRequest.of(0, 10, Sort.by("mno").descending());
Page<Memo> result = memoRepository.findByMnoBetween(10L, 50L, pageable);
result.get().forEach(memo -> System.out.println(memo));
}
deleteBy 로 시작하는 삭제 처리
쿼리 메서드를 이용해서 'deleteBy'로 메서드의 이름을 시작하면 특정한 조건에 맞는 데이터를 삭제하는 것도 가능하다.
예를 들어 메모의 번호(mno)가 10보다 작은 데이터를 삭제한다면 다음과 같이 작성할 수 있다.
public interface MemoRepository extends JpaRepository<Memo, Long>{
...
void deleteMemoByMnoLessThan(Long num);
}
@Commit
@Transactional
@Test
public void testDeleteQueryMethods(){
memoRepository.deleteMemoByMnoLessThan(10L);
}
테스트 코드에는 @Transactional과 @Commit이라는 어노테이션을 사용한다.
deleteBy..인 경우 우선은 'select'문으로 해당 엔티티 객체들을 가져오는 작업과 각 엔티티를 삭제하는 작업이 같이 이루어지기 때문이다.
@Commit은 최종 결과를 커밋하기 위해서 사용한다. 이를 적용하지 않으면 테스트 코드의 deleteBy..는 기본적으로 롤백(Rollback) 처리되어서 결과가 반영되지 않는다.
그 이유는 SQL을 이용하듯이 한 번에 삭제가 이루어지는 것이 아니라 각각 엔티티에 대해 삭제 쿼리가 들어간다.
개발 시에는 deleteBy를 이용하는 방식보다는 @Query를 이용해서 위와 같은 비효적인 부분을 개선한다.
@Query 어노테이션
Spring Data JPA가 제공하는 쿼리 메서드는 검색과 같은 기능을 작성할 때 편리함을 제공하기는 한다.
그러나 조인이나 복잡한 조건을 처리해야 하는 경우 'And, Or' 등이 사용되면서 불편할 때가 많다.
때문에 일반적인 경우에는 간단한 처리만 쿼리 메서드를 이용하고 @Query를 이용하는 경우가 더 많다.
@Query의 경우는 메서드의 이름과 상관없이 메서드에 추가한 어노테이션을 통해서 원하는 처리가 가능하다.
@Query의 value는 JPQL(Java Persistence Query Language) 로 작성하는데 흔히 '객체지향 쿼리'라고 불리는 구문들이다.
@Query를 이용해 이런 작업을 할 수 있다.
- 필요한 데이터만 선별적으로 추출하는 기능이 가능
- 데이터베이스에 맞는 순수한 SQL(Native SQL)을 사용하는 기능
- insert, update, delete와 같은 select가 아닌 DML 등을 처리하는 기능(@Modifying과 함께 사용)
객체지향 쿼리는 테이블 대신에 엔티티 클래스를 이용하고, 테이블의 칼럼 대신에 클래스에 선언된 필드를 이용해서 작성한다.
JPQL은 SQL과 상당히 유사하기 때문에 간단한 기능을 제작하는 경우에는 추가적인 학습 없이도 적용이 가능하다.
예를 들어 'mno의 역순으로 정렬하라'는 기능을 @Query를 이용해서 제작하면 다음과 같은 형태가 된다.
@Query("select m from Memo m order by m.mno desc")
List<Memo> getListDesc();
-> JPQL은 데이터베이스의 테이블 대신에 엔티티 클래스와 멤버 변수를 이용해서 SQL과 비슷한 JPQL을 작성한다.
실제 SQL에서 사용되는 함수들도 JPQL에서 동일하게 사용된다.(avg(), count(), group by, order by) 등등
@Query의 파라미터 바인딩
@Query의 경우 SQL과 비슷하기 때문에 'where' 구문과 그에 맞는 파라미터를 처리할 때가 많다.
이런 방식으로 사용한다.
- '?1, ?2'와 1부터 시작하는 파라미터의 순서를 이용하는 방식
- ':xxx'와 같이 ':파라미터 이름'을 활용하는 방식
- '#{ }'과 같이 자바 빈 스타일을 이용하는 방식
':파라미터'를 이용하는 방식
@Transactional
@Modifying
@Query("update Memo m set m.memoText = :memoText where m.mno = :mno")
int updateMemoText(@Param("mno") Long mno, @Param("memoText") String memoText);
:#{ } 을 이용하는 방식
@Transactional
@Modifying
@Query("update Memo m set m.memoText = #{#param.memoText} where m.mno = #{#param.mno}")
int updateMemoText(@Param("param") Memo memo);
@Query와 페이징 처리
@Query를 이용하는 경우에도 Pageable 타입의 파라미터를 적용하면 페이징 처리와 정렬에 대한 부분을 작성하지 않을 수 있다.
리턴 타입을 Page<엔티티 타입>으로 지정하는 경우에는 count를 처리하는 쿼리를 적용할 수 있다.
@Query를 이용할 때는 별도의 countQuery라는 속성을 적용해 주고 Pageable 타입의 파라미터를 전달하면 된다.
@Query(value = "select m from Memo m where m.mno > :mno",
countQuery = "select count(m) from Memo m where m.mno > :mno")
Page<Memo> getListWithQuery(Long mno, Pageable pageable);
countQuery를 분리하는 이유!
위에서는 조인을 사용하지 않았지만 여러 left inner join, 혹은 left outer join을 사용해서 데이터를 받아올 때,
countQuery는 조인이 필요하지 않다. 그래서 총 게시글, 메모의 개수는 따로 countQuery로 분리하는 것이
더 효율적인 선택이라고 볼 수 있다.
Object[] 리턴
@Query 장점 중 하나는 쿼리 메서드의 경우 엔티티 타입의 데이터만 추출하지만,
@Query를 이용하는 경우에는 현재 필요한 데이터만을 Object[] 의 형태로 선별적으로 추출이 가능하다.
JPQL을 이용할 때, 경우에 따라서 JOIN이나 GROUP BY 등을 이용하는 경우가 종종 있는데, 이럴 때는 적당한 엔티티 타입이 존재하지 않은 경우가 많기 때문에 이런 상황에서 유용하게 Object[] 타입을 리턴 타입으로 지정할 수 있다.
예를 들어 mno와 memoText 그리고 현재 시간을 같이 얻어오고 싶다면 Memo 엔티티 클래스에는 시간 관련된 부분의 선언이 없기 때문에 추가적인 구문이 필요하다.
JPQL에서는 CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP와 같은 구문을 통해서 현재 데이터베이스의 시간을 구할 수 있다.
@Query(value = "select m.mno, m.memoText, CURRENT_DATE from Memo m where m.mno > :mno",
countQuery = "select count(m) from Memo m where m.mno > :mno)
Page<Object[]> getListWithQueryObject(Long mno, Pageable pageable);
Native SQL 처리
@Query의 강력한 기능은 데이터베이스 고유의 SQL 구문을 그대로 활용하는 것이다.
JPA 자체가 데이터베이스에 독립적으로 구현이 가능하다는 장점을 잃어버리기는 하지만 경우에 따라서는 복잡한 JOIN 구문 등을 처리하기 위해서 어쩔 수 없는 선택을 하는 경우에 사용한다.
@Query(value = "select * from memo where mno > 0", nativeQuery = true)
List<Object[]> getNativeResult();
'CS지식들 > 공부공부' 카테고리의 다른 글
동시성 문제 (3) (0) | 2022.12.19 |
---|---|
동시성 문제 (2) (0) | 2022.12.19 |
동시성 문제 (1) (0) | 2022.12.18 |
Mybatis와 스프링에서 페이징 처리 (0) | 2022.11.30 |
페이징 처리 (오라클) (0) | 2022.11.29 |