Home 페이징과 정렬, 벌크성 수정 쿼리, EntityGraph, Lock
Post
Cancel

페이징과 정렬, 벌크성 수정 쿼리, EntityGraph, Lock

페이징, 정렬

비교를 위해 우선 JPA에서 페이징과 정렬을 사용하는 예제를 보자.

  • 검색 조건: 나이가 10살
  • 정렬 조건: 이름으로 내림차순
  • 페이징 조건: 첫 번째 페이지, 페이지 당 보여줄 데이터는 3건
1
2
3
4
5
6
7
8
9
10
11
12
13
public List<Member> findByPage(int age, int offset, int limit) {  
	return em.createQuery("select m from Member m where m.age = :age order by m.username desc")  
			.setParameter("age", age)  
			.setFirstResult(offset)  
			.setMaxResults(limit)  
			.getResultList();  
}  
  
public long totalCount(int age) {  
	return em.createQuery("select count(m) from Member m where m.age = :age", Long.class)  
			.setParameter("age", age)  
			.getSingleResult();  
}

스프링 데이터 JPA 페이징, 정렬

  • 페이징과 정렬 파라미터
    • org.springframework.data.domain.Sort : 정렬 기능
    • org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort가 포함되어 있음)
      • 페이징 + 정렬
  • 특별한 반환 타입
    • org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
    • org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능 (내부적으로 limit + 1 조회)
    • List : 추가 count 쿼리 없이 결과만 반환
1
2
3
//repository

Page<Member> findByAge(int age, Pageable pageable);
1
2
3
4
5
6
7
8
9
10
11
12
13
//사용

int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

Page<Member> page = memberRepository.findByAge(age, pageRequest);

List<Member> content = page.getContent(); //결과 객체들
long totalElements = page.getTotalElements(); //개수
page.getNumber(); //페이지 넘버
page.getTotalPages(); //페이지 개수
page.isFirst();
page.hasNext();
  • Page 인터페이스
1
2
3
4
5
public interface Page extends Slice { 
	int getTotalPages(); //전체 페이지 수 
	long getTotalElements(); //전체 데이터 수 
	Page map(Function<? super T, ? extends U> converter); //변환기 
}
  • Slice 인터페이스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Slice extends Streamable { 
	int getNumber(); //현재 페이지 
	int getSize(); //페이지 크기 
	int getNumberOfElements(); //현재 페이지에 나올 데이터 수 
	List getContent(); //조회된 데이터 
	boolean hasContent(); //조회된 데이터 존재 여부 
	Sort getSort(); //정렬 정보 
	boolean isFirst(); //현재 페이지가 첫 페이지 인지 여부 
	boolean isLast(); //현재 페이지가 마지막 페이지 인지 여부 
	boolean hasNext(); //다음 페이지 여부 
	boolean hasPrevious(); //이전 페이지 여부 
	Pageable getPageable(); //페이지 요청 정보 
	Pageable nextPageable(); //다음 페이지 객체 
	Pageable previousPageable();//이전 페이지 객체 
	Slice map(Function<? super T, ? extends U> converter); //변환기 
}

count 쿼리의 분리

1
2
3
@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);

count를 할 때는 join을 할 필요가 없기때문에 이러한 기능이 생겼다. 만약 분리가 되지 않으면 join으로 인해 예상치 못한 성능 저하가 나타날 수 있다.

단, 하이버네이트 6 이상부터는 의미없는 left join을 자동으로 최적화해주기 때문에 이러한 문제가 없다.

실무 팁

API를 사용할 때 엔티티 그대로를 넘기면 안된다. 문제가 많다.

DTO로 변환해야 한다.

  • 페이지를 유지하면서 엔티티를 DTO로 변환
1
2
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());

벌크성 수정 쿼리

ex) 모든 직원의 연봉을 10% 인상해라.

  • JPA 변경 감지 = 단건

우선 JPA를 사용하는 경우를 보자.

1
2
3
4
5
6
7
public int bulkAgePlus(int age) {  
	int resultCount = em.createQuery("update Member m set m.age = m.age + 1 where m.age >= :age")  
			.setParameter("age", age)  
			.executeUpdate();  
	  
	return resultCount;  
}

스프링 데이터 JPA의 벌크성 수정 쿼리

1
2
3
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
  • @Modifying 애노테이션을 사용하면 된다.
    • 사용하지 않으면 예외가 발생한다.
  • 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶다면 옵션을 수정하면 된다.
    • @Modifying(clearAutomatically = true)
    • 기본값은 false이다.
    • 이러한 옵션을 신경쓰지 않고 회원을 다시 조회한다면 영속성 컨텍스트에 과거 값이 남아 문제가 발생할 수 있다.
      • DB에는 update 되었는데 막상 값을 조회하면 영속성 컨텍스트의 값을 꺼내 매칭이 안되는 문제.
    • 다시 조회해야 하는 경우에는 영속성 컨텍스트를 반드시 초기화 하자.

권장 방안

  • 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행
  • 영속성 컨텍스트에 엔티티가 있다면 벌크 연산 직후 영속성 컨텍스트 초기화

@EntityGraph

연관된 엔티티들을 SQL 한 번으로 조회하는 방법.

즉 페치조인.

JPQL로 페치조인을 해보자.

1
2
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();

스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하도록 도와준다. JPQL 없이 페치 조인을 사용할 수 있다.

스프링 데이터 JPA에서의 페치조인

1
2
3
4
5
6
7
8
9
10
11
12
13
//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

//메서드 이름으로 쿼리 (특히 편리)
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username);
  • 사실상 페치 조인의 간편한 버전이라고 보면 된다.
  • LEFT OUTER JOIN을 사용.

JPA Hint, Lock

JPA Hint

JPA 쿼리 힌트(SQL 힌트가 아닌 JPA 구현체에게 제공하는 힌트)

  • 쿼리 힌트 사용
1
2
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
1
2
3
4
5
6
7
8
memberRepository.save(new Member("member1", 10));
em.flush();
em.clear();

Member member = memberRepository.findReadOnlyByUsername("member1");
member.setUsername("member2");

em.flush(); //update 쿼리 실행 X = 변경 감지 X

위의 예제에서는 단순하게 조회용도로 쓰겠다고 선언한 것이다.

Lock

1
2
@Lock(LockModeType.PESSMISTIC_WRITE)
List<Member> findByUsername(String name);
This post is licensed under CC BY 4.0 by the author.