JPA 성능최적화
N+1 문제
fetchType 이 Eager 로 설정 되어 있는 엔티티를 jpql로 조회할때 발생한다.
@Entity
public class Member {
...
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
private List<Order> orders = new ArrayList<Order>();
}
...
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
//먼저 Member 들을 조회하는 쿼리를 수행 후 엔티티에 할당하는데
//할당하는 동중에 FetchType.Eager 를 발견하게 되고, 각 MemberId마다 Order조회 쿼리를 수행한다.
//해당 문제는 지연로딩이라 해도 동일하다.
해결법 1. fetch join 사용
select m from Member m join fetch m.orders
해결법 2. 하이버네이트 @BatchSize
- 연관된 엔티티를 조회할 때 지정된 사이즈 만큼 in절을 이용해서 조회한다.
@Entity
public class Member {
...
@org.hibernate.annotations.BatchSize(size = 5)
@OneToMany(mappedBy="member", fetch=FetchType.EAGER)
private List<Order> orders = new ArrayList<Order>();
}
//SELECT * FROM ORDERS WHERE MEMBER_ID IN(?, ?, ?, ?, ?);
해결법 3. 하이버네이트 @Fetch(FetchMode.SUBSELECT)
@org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
//SELECT O FROM ORDERS O WHERE O.MEMBER IN (SELECT M.ID FROM....)
베스트는 왠만하면 지연로딩을 사용하도록 하고, 필요시 jpql fetch join 을 사용하자.
@OneToOne, @ManyToOne — 디폴느는 즉시로딩
@OneToMany, @ManyToMany — 디폴트는 지연로딩
단순 조회 쿼리 성능 최적화
- 스칼라 타입으로 조회
select o.id, o.name from Order o
- 읽기 전용 쿼리 힌트 사용
TypedQuery<Order> query = em.createQuery("select o from Order o",
Order.class);
query.setHint("org.hibernate.readOnly", true);
- 스프링 읽기전용 모드 설정
@Transactional(readOnly = true)//플러시 모드 MANUAL 설정
또는
@Transactional(propagation = Propagation.NOT_SUPPORTED)//트랜잭션 범위에서 제외
- 1, 2번은 persistence context에서 스냅샷을 관리하지 않으므로 flush를 해도 DB에 반영되지 않는다.
- 3번의 스프링 설정은 플러시 모드를 MANUAL로 설정하여, 플러시를 강제로 호출하지 않는 이상 플러시를 호출하지 않는다.
- 트랜잭션 범위에서 제외하는 방법은 당연하게 데이터 수정에서 제외된다.
- 스프링에서는 읽기전용 트랜잭션(readonly)와 읽기전용 엔티티 사용(setHint - readOnly) 를 사용하여 플러시도 작동하지 않고, 엔티티를 읽기전용으로 조회하도록 두가지 다 적용하는 것이 가장 효과적이다.
배치 처리
다건의 조회할 때 데이터를 전부 jpa 엔티티로 메모리에 실을 수 없기 때문에 페이징 처리를 해야 한다.
- JPA 페이징 처리
...
int pageSize = 100;
for(int i=0; i<10; i++) {
List<Product> resultList = em.createQuery("select p from Product p",
Product.class)
.setFirstResult(i * pageSize)
.setMaxResults(pageSize)
.getResultList();
for(Product product: resultList) {
product.setPrice(product.getPrice() + 100);
}
}
...
- 하이버네이트 scroll 사용 (DB 커서 사용)
EntityTransaction tx = em.getTransaction();
//하이버네이트 기능을 사용하기 위해 세션을 가져온다.
Session session = em.unwrap(Session.class);
tx.begin();
//scroll을 이용해서 엔티티를 하나씩 얻어올 수 있다.
ScrollableResults scroll = session.createQuery("select p from Product p")
.setCacheMode(CacheMode.IGNORE) //2차 캐시 비활성화
.scroll(ScrollMode.FORWARD_ONLY);
while(scroll.next()) {
Product p = (Product) scroll.get(0);
p.setPrice(p.getPrice() + 100);
count++;
if(count % 100 == 0) {
/100개 단위로 영속화
session.flush();session.clear();
}
}
tx.commit();
session.close();
'JAVA' 카테고리의 다른 글
JPA 캐시 (0) | 2021.04.28 |
---|---|
JPA 락 처리 (0) | 2021.04.28 |
JPA 엔티티 그래프 (0) | 2021.04.28 |
JPA 엔티티 생명주기에 리스너 등록 (0) | 2021.04.28 |
JPA Entity와 DB값 간 매핑 (0) | 2021.04.28 |