최적화

예외 처리

  • JPA 표준 예외들은 jakarta.persistence.PersistenceException의 자식클래스다.
    1. 트랜잭션 롤백 표시 예외
      • jakarta.persistence.EntityExistsException
      • jakarta.persistence.EntityNotFoundException
      • jakarta.persistence.OptimisticLockException
      • jakarta.persistence.PessimisticLockException
      • jakarta.persistence.RollbackLockException
      • jakarta.persistence.TransactionRequiredLockException
    2. 트랜잭션 롤백 미표시 예외
      • 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. 상속 관계, 프록시

  • 프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성된다. 이러면
    1. instanceof 불가
    2. 하위 타입으로 다운 캐스팅 불가.
  • 결국에 1. JPQL로 조회, 2. 프록시 벗겨내기 둘 중 하나를 해야 한다.

성능 최적화

N+1

  • 즉시 로딩(Eager)로 생기는 문제다.
  • entityManage로 em.find()하면 join으로 가져 온다.
  • JPQL로 던지면 단일 대상으로 긁으면 내부에 연관관계도 원치 않더라도 긁는다.
  • 즉 하나를 갖고 오면 내부 연관관계 맞춰서 N개를 긁는 식으로 문제가 생긴다.

지연로딩?

  • 지연로딩(Lazy)으로 해도 문제다.
  • 처음엔 문제가 없어보이지만 나중에 비즈니스 로직 돌리면 결국 따로 조회한다.
  • 특히 컬렉션으로 된 연관 관계를 긁으면 element 개수 만큼 던진다.

여기 까지 정리

  • Eager, Lazy 결국 다 날 수 있다.
  • Eager는 그냥 들고만 와도
  • Lazy는 결국 사용하면

그래서?

  1. join fetch(페치 조인) 사용하면 된다. sql join을 이용해서 던진다.
  2. @org.hibernate.annotations.BatchSize로 조회할 때 지정한 size만큼 IN 절을 사용해서 조회한다.
  3. Fetch(FetchMode.SUBSELECT)로 서브쿼리로 N+1를 해결한다. (SELECT에 던진 쿼리를 연관관계 찾을 때 WHERE에 넣고 돌린다.)

읽기 전용

  • 엔티티 1차 캐싱은 더티체킹에서 혜택을 받는다 당연히 스냅샷을 보관하므로 메모리를 사용한다.
    1. 읽기 전용(@Transactional(readOnly=true))으로 하면 이 과정을 무시해서 메모리 최적화를 할 수 있다. -> 영속성 컨텍스트 관리 무시
    2. 엔티티 통이 아닌 필드(스칼라 타입)으로 던지면 영속성 컨텍스트가 관리하지 않는다. -> 영속성 컨텍스트 관리 무시
    3. org.hibernate.readOnly로 엔티티를 읽기 전용으로 조회할 수 있다. -> 영속성 컨텍스트 관리 무시
    4. 트랜잭션 밖에서 읽으면 된다. @Transactional(propagation = Propagation.NOT_SUPPORTED) 플러시가 일어나지 않는다. -> 영속성 컨텍스트 플러시 비활성

배치 처리

  • 배치 INSERT를 영속성 컨텍스트에 대고 하면 메모리 부족이 일어날 수도 있다.
    1. 페이징 처리

      예를 들어 1000개면 100개씩 저장하고 flush 하는 식으로 하는 것이다.

    1. 커서

      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) 시간을 줄일 수 있다. 벤더에 따라 갭락도 걸 수도 있다.