객체지향 쿼리와 관련된 다양한 고급 주제를 알아보자.
벌크 연산
엔티티를 수정하려면 영속성 콘텍스트의 변경 감지 기능이나 병합을 사용하고, 삭제하려면 EntityManager.remove() 메서드를 사용한다. 하지만 이 밥벙으로 수백 개 이상의 엔티티를 하나씩 처리하기에는 시간이 너무 오래 걸린다.
이때 여러 건을 한 번에 수정하거나 삭제하는 벌크 연산을 사용하면 된다.
// 재고가 10개 미만인 상품의 가격을 10% 상승
String sql = "UPDATE product p SET p.price = p.price * 1.1 WHERE p.stockAmount < :stockAmount";
int resultCount = em.createQuery(sql).setParameter("stockAmount", 10).executeUpdate();
벌크 연산은 executeUpdate() 메서드를 사용한다. 이 메서드는 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다.
삭제도 같은 메서드를 사용한다.
// 100원 미만 상품을 모두 삭제
String sql = "DELETE FROM product p WHERE p.price < :price";
int resultCount = em.createQuery(sql).setParameter("price", 100).executeUpdate();
벌크 연산의 주의점
벌크 연산을 사용할 때는 벌크 연산이 영속성 콘텍스트를 무시하고 데이터베이스에 직접 쿼리 한다는 점에 주의해야 한다.
데이터를 조회하고 그 후에 벌크 연산을 했을 경우 영속성 콘텍스트를 무시하기 때문에 조회한 데이터는 바뀌지 않는다.
가장 실용적인 해결책은 벌크 연산을 가장 먼저 실행하는 것이지만 상황에 따라 다를 수 있다.
// A 상품 조회
String sql = "SELECT p FROM product p WHERE p.name = :name";
Product product = em.createQuery(sql).setParameter("name", "A_Product").getSingleResult();
// 벌크 연산 전 출력 결과 : A Product Price = 1000
System.out.println("A Product Price = " + product.getPrice());
// 모든 상품의 가격을 10% 상승 (벌크 연산)
String sql = "UPDATE product p SET p.price = p.price * 1.1";
em.createQuery(sql).executeUpdate();
// 벌크 연산 후 출력 결과 : A Product Price = 1000
System.out.println("A Product Price = " + product.getPrice());
벌크 연산을 수행한 직후에 정확한 A 상품의 데이터를 사용해야 한다면 영속성 콘텍스트를 초기화해서 저장된 모든 엔티티를 제거하거나 em.refresh()를 사용해서 다시 조회해야 한다.
em.refresh(product); // 데이터베이스에서 상품을 다시 조회
이렇게 영속성 콘텍스트와 데이터베이스 간에 데이터 차이가 발생할 수 있으므로 주의해서 사용해야 한다.
영속성 콘텍스트와 JPQL
JPQL의 조회 대상은 엔티티, 임베디드 타입, 값 타입 같이 다양한 종류가 있다.
JPQL로 엔티티를 조회하면 영속성 콘텍스트에서 관리되지만 임베디드 타입 또는 값 타입이면 조회해서 값을 변경해도 영속성 콘텍스트가 관리하지 않으므로 변경 감지(Dirty Checking)가 발생하지 않는다.
정리하자면 조회한 엔티티만 영속성 콘텍스트가 관리한다.
그럼 JPQL에서 엔티티를 조회하면 어떻게 되는지 알아보자.
JPQL을 SQL로 변환하여 데이터베이스에 조회한 후 조회한 결과와 영속성 콘텍스트를 비교한다.
이때 식별자 값을 기준으로 이미 영속성 콘텍스트 에콘 텍스트에 가지고 있다면 버리고 기존에 영속성 콘텍스트에 있던 엔티티를 반환한다.
그런데 왜 데이터베이스에서 새로 조회한 엔티티는 버리고 영속성 콘텍스트에 있는 기존 엔티티를 반환할까?
검색한 엔티티로 대체해서 반환할 경우 영속성 콘텍스트에 수정 중인 데이터가 사라질 수 있으므로 위험하다.
영속성 콘텍스트는 엔티티의 동일성을 보장하기 때문에 새로 조회한 엔티티는 버리고 가지고 있던 엔티티를 반환해야 한다.
find()로 조회하던 JPQL로 조회하던 영속성 콘텍스트가 같으면 동일한 엔티티를 반환한다.
find() vs JPQL
find() 메서드는 엔티티를 영속성 콘텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾는다.
따라서 해당 엔티티가 영속성 콘텍스트에 있으면 메모리(1차 캐시)에서 바로 찾으므로 성능상 이점이 있다.
JPQL은 항상 데이터베이스에 SQL을 먼저 조회한 후 영속성 콘텍스트와 비교한다.
JPQL과 플러시 모드
JPA는 플러시가 일어날 때 영속성 콘텍스트에 등록, 수정, 삭제한 엔티티를 찾아서 INSERT, UPDATE, DELETE SQL을 만들어 데이터베이스에 반영한다. 플러시를 호출하려면 em.flush() 메서드를 직접 사용해도 되지만 보통 플러시 모드(FlushMode)에 따라 커밋하기 직전이나 쿼리 실행 직전에 자동으로 플러시가 호출된다.
em.setFlushMode(FlushModeType.AUTO); // 커밋 또는 쿼리 실행 시 플러시(default)
em.setFlushMode(FlushModeType.COMMIT) // 커밋시에만 플러시
FlushModeType.COMMIT은 커밋 시에만 플러시를 호출하고 쿼리 싱 행시에는 플러시를 호출하지 않는다.
이 옵션은 성능 최적화를 위해 꼭 필요할 때만 사용해야 한다.
쿼리와 플러시 모드
JPQL은 영속성 컨텍 승에 있는 데이터를 고려하지 않고 데이터베이스에서 데이터를 조회한다. 따라서 JPQL을 실행하기 전에 영속성 콘텍스트의 내용을 데이터베이스에 반영해야 한다.
// 상품 가격을 1000원 -> 2000원으로 변경
product.setPrice(2000);
// 가격이 2000원인 상품을 조회
Product product2 =
em.createQuery("SELECT p FROM product p WHERE p.price = 2000", Product.class).getSingleResult();
만약 product의 상품을 2000원으로 변경 후 2000원인 product2를 조회한다고 할 때
아직 플러시 되지 않아 product는 1000원일 것이다.
이런 상황에 product도 2000원으로 조회하려면 플러시 모드를 COMMIT으로 설정하고 em.flush()를 직접 호출하거나 Query 객체에 플러시 모드를 설정해주어야 한다.
// 상품 가격을 1000원 -> 2000원으로 변경
product.setPrice(2000);
// em.flush() // 직접 호출
// 가격이 2000원인 상품을 조회
Product product2 =
em.createQuery("SELECT p FROM product p WHERE p.price = 2000", Product.class)
.setFlushModel(FlushModeType.AUTH) // setFlushMode 설정
.getSingleResult();
이렇게 쿼리에 설정하는 플러시 모드는 엔티티 매니저에 설정하는 플러시 모드보다 우선권을 가진다.
플러시 모드와 최적화
FlushModeType.COMMIT 모드는 트랜잭션을 커밋할 때만 플러시하고 쿼리를 실행할 때는 플러시 하지 않는다.
JPA 쿼리를 사용할 때 영속성 콘텍스트에는 있지만 아직 데이터베이스에 반영하지 않은 데이터를 조회할 수 없다.
이런 상황은 잘못하면 데이터 무결성에 심각한 피해를 줄 수 있다.
그럼에도 AUTO 모드일 때 플러시가 너무 자주 일어나는 상황이라면 COMMIT 모드를 사용해 플러시 횟수를 줄여 성능을 최적화할 수 있다.
JPA를 사용하지 않고 JDBC를 직접 사용해서 SQL을 실행할 때도 플러시 모드를 고민해야 한다.
JDBC로 쿼리를 직접 사용하면 JPA가 실행한 쿼리를 인식할 방법이 없다. 따라서 AUTH로 설정해도 플러시가 일어나지 않는다. 이 때는 JDBC로 쿼리를 실행하기 직전에 em.flush()를 호출해서 영속성 콘텍스트의 내용을 데이터베이스에 동기화하는 것이 안전하다.
'JPA' 카테고리의 다른 글
JPA Secification을 이용한 검색 (0) | 2022.01.20 |
---|---|
Spring Data JPA 소개 (0) | 2022.01.18 |
JPA 스토어드 프로시저(Stored Procedure) (1) | 2022.01.17 |
JPA N+1 문제 해결하기 (0) | 2022.01.17 |
자주 사용하는 JPQL 문법 정리 (0) | 2022.01.14 |