최적화
예외 처리
- JPA 표준 예외들은
jakarta.persistence.PersistenceException의 자식클래스다.- 트랜잭션 롤백 표시 예외
- jakarta.persistence.EntityExistsException
- jakarta.persistence.EntityNotFoundException
- jakarta.persistence.OptimisticLockException
- jakarta.persistence.PessimisticLockException
- jakarta.persistence.RollbackLockException
- jakarta.persistence.TransactionRequiredLockException
- 트랜잭션 롤백 미표시 예외
- jakarta.persistence.NoResultException
- jakarta.persistence.NonUniqueResultException
- jakarta.persistence.LockTimeoutException
- jakarta.persistence.QueryTimeoutException
- 트랜잭션 롤백 표시 예외
- Spring의 JPA 예외 변환
- Jpa의 의존도를 떨구기 위해서 스프링은 JPA 예외를 translate한다.
| JPA 예외 | 스프링 변환 예외 |
|---|---|
| jakarta.persistence.PersistenceException | org.springframework.orm.jpa. JpaSystemException |
| jakarta,persistence.NoResultException | org.springframework.dao. EmptyResultDataAccessException |
| jakarta.persistence.NonUniqueResultException | org.springframework.dao.incorrectResultSize DataAccessException |
| jakarta.persistence.LockTimeoutException | org.springframework.dao. CannotAcquireLockException |
| jakarta.persistence.QueryTimeoutException | org.springframework.dao. QueryTimeoutException |
| jakarta. persistence.EntityExistsException | org.springframework,dao. DataIntegrityViolationException |
| jakarta.persistence.EntityNotFoundException | org.springframework.orm.jpa.JpaObjectRetrievalFailureException |
| jakarta.persistence.OptimisticLockException | org.springframework,orm.jpa.JpaOptimisticLockingFailureException |
| jakarta.persistence.PessimisticLockException | org.springframework.dao.PessimisticLockingFailureException |
| jakarta,persistence.TransactionRequiredException | org.springframework.dao.invalidDataAccessApiUsageException |
| jakarta.persistence.RollbackException | org. springframework.transaction.TransactionSystemException |
| java.lang.IllegalStateException | org.springframework.dao.InvalidDataAccessApiUsageException |
| java.lang.IllegalArgumentException | org.springframework,dao.InvalidDataAccessApiUsageException |
- PersistenceExceptionTranslationPostProcessor를 등록하면
@Repository마킹 된 곳에서 Exception을 AOP로 catch 해서 바꿔서 던진다.
엔티티 비교
- PersistenceContext 내부적으로 1차 캐시를 가지고 생명주기를 같이 한다.
- 1차 캐시덕분에 더티 체킹도 가능하다.
- 1차 캐시는 Application level RepeatableRead를 지원한다. 항상 영속성 컨텍스트는 저장한 엔티티 인스턴스를 그대로 반환한다.
- 정말로 주소값까지 같은 인스턴스를 반환한다.
- 추가로 트랜잭션 시작 - 끝이 영속성 컨텍스트 범위이므로 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트를 가진다.
- 트랜잭션이 다르면 영속성 컨텍스트가 달라지고 객체의 동일성을 보장하지 않는다.
프록시
- 프록시는 원본 엔티티를 상속 받아서 만들어서 엔티티를 사용하는 클라이언트가 진짜 엔티티인지 아닌지 구별하지 않고 사용할 수 있다.
- 원본을 사용하다가 지연로딩을 위해서 프록시를 사용해도 로직 수정이 불필요하다.
- 그러나 문제점도 있는데
1. 영속석 컨텍스트와 프록시
- 엔티티, 프록시 간 동일성이 보장이 될까? -> 된다. 그냥 보면
아니다가 맞다. 그래서 둘이 비교하면 엔티티끼리 비교하는 형태로 비교해서 동일성을 보장한다.
2. 프록시 타입 비교
- 프록시로 조회한 엔티티 타입은
==가 아니라 instanceof를 사용해야 한다.
3. 프록시 동등성
- 프록시 타입은
==대신instanceof - 멤버 변수에 직접 접근 말고 getter를 사용해서
4. 상속 관계, 프록시
- 프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성된다. 이러면
- instanceof 불가
- 하위 타입으로 다운 캐스팅 불가.
- 결국에 1. JPQL로 조회, 2. 프록시 벗겨내기 둘 중 하나를 해야 한다.
성능 최적화
N+1
- 즉시 로딩(Eager)로 생기는 문제다.
- entityManage로
em.find()하면 join으로 가져 온다. - JPQL로 던지면 단일 대상으로 긁으면 내부에 연관관계도 원치 않더라도 긁는다.
- 즉 하나를 갖고 오면 내부 연관관계 맞춰서 N개를 긁는 식으로 문제가 생긴다.
지연로딩?
- 지연로딩(Lazy)으로 해도 문제다.
- 처음엔 문제가 없어보이지만 나중에 비즈니스 로직 돌리면 결국 따로 조회한다.
- 특히 컬렉션으로 된 연관 관계를 긁으면 element 개수 만큼 던진다.
여기 까지 정리
- Eager, Lazy 결국 다 날 수 있다.
- Eager는 그냥 들고만 와도
- Lazy는 결국 사용하면
그래서?
- join fetch(페치 조인) 사용하면 된다. sql join을 이용해서 던진다.
@org.hibernate.annotations.BatchSize로 조회할 때 지정한 size만큼 IN 절을 사용해서 조회한다.Fetch(FetchMode.SUBSELECT)로 서브쿼리로 N+1를 해결한다. (SELECT에 던진 쿼리를 연관관계 찾을 때 WHERE에 넣고 돌린다.)
읽기 전용
- 엔티티 1차 캐싱은 더티체킹에서 혜택을 받는다 당연히 스냅샷을 보관하므로 메모리를 사용한다.
- 읽기 전용(
@Transactional(readOnly=true))으로 하면 이 과정을 무시해서 메모리 최적화를 할 수 있다. -> 영속성 컨텍스트 관리 무시 - 엔티티 통이 아닌 필드(스칼라 타입)으로 던지면 영속성 컨텍스트가 관리하지 않는다. -> 영속성 컨텍스트 관리 무시
org.hibernate.readOnly로 엔티티를 읽기 전용으로 조회할 수 있다. -> 영속성 컨텍스트 관리 무시- 트랜잭션 밖에서 읽으면 된다.
@Transactional(propagation = Propagation.NOT_SUPPORTED)플러시가 일어나지 않는다. -> 영속성 컨텍스트 플러시 비활성
- 읽기 전용(
배치 처리
- 배치 INSERT를 영속성 컨텍스트에 대고 하면 메모리 부족이 일어날 수도 있다.
- 페이징 처리
예를 들어 1000개면 100개씩 저장하고 flush 하는 식으로 하는 것이다.
- 페이징 처리
-
- 커서
hibernate scroll을 이용한다. DB 커서를 지원한다.
EntityTransaction tx = em. getTransaction () ; Session session = em. unwrap (Session.class) ; tx.begin(); ScrollableResults scroll = session.createQuery("select p from Product p") .setCahceMode(CacheMode.IGNORE) .scroll(ScrollMode.FORWARD_ONLY); int count = 0; while(scoll.next()) { Product p = (Product) scroll.get(0); p.setPrice(p.getPrice() + 100); count += 1; if( count % 100 == 0) { session.flush(); session.clear() } } tx.commit(); session.close(); /** * StatelessSession session = sessionFactory.openStatelessSession (); * 위 코드로 영속성 컨텍스트 없고, 2차 캐시도 없는 무상태 세션을 만들 수 있다. * 엔티티 수정을 위해서는 update를 수동으로 해야한다. * session.update();다. */
- 커서
트랜잭션을 지원하는 쓰기 지연, 성능 최적화
- insert를 여러 번 던지면 하나로 묶어서 처리할 수 있다.
hibernate.jdbc.batch_size에 숫자를 주면 최대 해당 건까지 묶어서 보낸다. (연속해서 같은 류의 문장일 경우)- 이러면 rowLock(recordLock) 시간을 줄일 수 있다. 벤더에 따라 갭락도 걸 수도 있다.