고급 주제와 성능 최적화

예외 처리

  • JPA 표준 예외들은 javax.persistence.PersistenceException 자식 클래스
    • 그리고 모두 RuntimeException, 즉 언체크 예외
  • JPA 표준 예외는 크게 두 가지로 구분
    • 트랜잭션 롤백을 표시하는 예외
      • 심각한 예외
      • 트랜잭션이 롤백 - 강제 커밋 불가능
    • 트랜잭션 롤백을 표시하지 않는 예외
      • 심각하지 않은 예외
      • 개발자가 트랜잭션을 롤백할지 판단가능
  • 트랜잭션 롤백을 표시하는 예외
    • EntityExistsException
    • EntityNotFoundException
    • OptimisticLockException
    • PessimisticLockException
    • RollbackException
    • TransactionRequiredException
  • 트랜잭션 롤백을 표시하지 않는 예외
    • NoResultException
    • NonUniqueResultException
    • LockTimeoutException
    • QueryTimeoutException

스프링 프레임워크의 JPA 예외 변환

  • 서비스계층에서 데이터 접근 계층의 구현 기술에 직접의존 하는것은 좋은 설계라 할 수 없음
  • 예외도 마찬가지
  • 서비스계층에서 JPA의 예외를 직접사용하면 JPA에 의존하는것
  • 스프링 프레임워크는 이런 문제를 해결하기 위해 예외를 추상해서 제공중
    • JpaSystemException
    • EmptyResultDataAccessException
    • IncorrectResultSizeDataAccessException
    • CannotAcquireLockException
    • QueryTimeoutException
    • DataIntegrityViolationException
    • ,,,
  • JPA 예외를 스프링 프레임워크 예외로 변경하기 위해서는 PersistenceExceptionTranslateionPostProcessor 를 빈으로 등록해야함
    • 이후 AOP로 동작
  • 트랜잭션 롤백 시 주의사항
    • 트랜잭션을 롤백하는 것은 데이터베이스의 반영사항을 롤백하는것
    • 엔티티(자바)를 원상 복구하는 것은 아님
      • 객체는 수정된 상태로 영속성 컨텍스트에 남아있음
    • 따라서 롤백된후에는 새로운 영속성 컨텍스트를 생성하여 사용하거나 EntityManager.clear()를 호출해 영속성 컨텍스트를 초기화하고 사용해야함
    • 스프링 프레임워크는 이런 문제를 예방하기 위해 영속성 컨텍스트 범위에 따라 다른 방법을 사용
      • 기본 전략인 트랜잭션당 영속성 컨텍스트 전략은 문제 발생시 트랜잭션 AOP 종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트를 종료하므로 문제가 발생하지 않음
      • 문제는 OSIV처럼 영속성 컨텍스트의 범위가 트랜잭션 범위보다 넒게 사용하는 것
        • 트랜잭션을 롤백해서 영속성 컨텍스트에 이상이 발생해도 다른 트랜잭션에서는 해당 영속성 컨텍스트를 그래도 사용하는 문제
        • 이 경우 스프링 프레임워크는 트랜잭션 롤백시 영속성 컨텍스트를 초기화(EntityManager.clear()) gotj 잘못된 영속성 컨텍스트를 사용하는 문제를 예방
        • 자세한것은 org.springframework.orm.jpa.JpaTransactionManager의 doRollback() 메서드 참조

엔티티 비교

  • 영속성 컨텍스트 내부에는 엔티티 인스턴스를 보관하기 위한 1차 캐시가 존재
    • 1차 캐시는 영속성 컨텍스트와 생명주기가 같음
  • 영속성 컨텍스트를 통해 데이터를 저장하거나 조회하면 1차 캐시에 엔티티가 저장
  • 1차 캐시 덕분에 변경 감지 기능이 동작, 데이터베이스를 통하지 않고 조회도 가능
  • 영속성 컨텍스트를 더 정확히 이해하기 위해서는 1차캐시의 가장 큰 장점인 애플리케이션 수준의 반복 가능한 읽기를 이해해야함
  • 단순 같은 동등성(equals) 수준이 아닌 같은 주소의 인스턴스를 반환
저장후 조회 비교
  • 한 트랜잭션내에서 저장안 엔티티와 조회한 엔티티는 완전히 같은 인스턴스
    • 같은 트랜잭션 범위에 있으므로 같은 영속성 컨텍스트를 사용하기 때문
  • 따라서 영속성 컨텍스트가 같으면 엔티티 비교시 다음 3가지 조건을 모두 만족
    • identical: == 성공
    • equinalent: equals() 성공
    • 데이터베이스 동등성: @Id 식별자가 같음
  • 다른 영속성 컨텍스트의 엔티티 비교
    • identical: == 실패
    • equinalent: equals() 성공, 그러나 equals가 재정의 되어있어야함
    • 데이터베이스 동등성: @Id 식별자가 같음
  • OSIV처럼 요청의 시작과 끝까지 같은 영속성 컨텍스트를 사용할 때는 동일성 비교가 성공함
  • 그러나 영속성 컨텍스트가 다른 두 엔티티를 비교하면 equals 등을 재정의하고 사용해야함
  • 데이터베이스 동등성 비교는 엔티티를 영속화하고 식별자를 얻어야 가능함
    • 식별자를 직접 부여하는 방식에서는 영속화 되지 않은 상태에서 가능

프록시 심화 주제

  • 프록시는 원본 엔티티를 상속받아 만들어지므로 클라이언트는 엔티티가 프록시인지 원본 엔티티인지 구분하지 않고 사용가능
  • 따라서 지연 로딩을 하려고 프록시로 변경해도 클라이언트는 비즈니스 로직을 수정할 필요가 없음
  • 하지만 프록시를 사용하는 방식의 기술적인 한계로 예상하지 못한 문제가 발생하기도 함

영속성 컨텍스트와 프록시

  • 영속성 컨텍스트는 자신이 관리하는 영속 엔티티의 동일성(identity)을 보장
    • 프록시(지연로딩)로 조회된 엔티티와 같은 엔티티에 대한 find요청이오면 프록시를 반환
    • 반대로 엔티티를 먼저조회하고 프록시(지연로딩)를 조회하면 먼저 반환된 엔티티를 반환
    • 먼저 반환된 엔티티(또는 프록시)를 반환해서 동일성 보장

프록시 타입 비교

  • 프록시는 엔티티를 상속받아 만들어지므로 타입비교시 == 가 아닌 instanceof를 사용해야함

프록시 동등성 비교

  • 엔티티의 동등성을 비교할려면 equals() 메서드를 오버라이딩해서 비교하면됨
  • 이때 비교대상이 프록시인 경우 문제가 발생할 수 있음
    • 프록시는 맴버 필드는 null을 가지고 있음
    • 그래서 .name으로 접근시 null로 처리됨
    • .getName()로 접근하면 원하는 값을 가져올 수 있음

상속관계와 프록시

  • 상속관계를 가진 엔티티가 존재할때 프록시를 부모 타입으로 조회시 문제가 발생타입검증
    • Item을 상속하는 Book 엔티티가 존재
    • Item으로 프록시를 조회
    • Item의 프록시와 Book엔티티는 관계가 없음(둘다 Item을 상속하였을뿐), Item의 프록시를 Book엔티티로 캐스팅(정상적인 의도라면 다운캐스팅, 직접 다운캐스팅도 실패) 불가능, proxyItem instanceof Book // false
  • 이를 해결하기 위해 다음과 같은 방법이 존재
    • JPQL로 대상 직접 조회 (다형성 활용 불가능)
    • 프록시 벗기기
    • 기능을 위한 별도의 인터페이스 제공
    • 비지터 패턴 사용

성능 최적화

N+1 문제

  • JPA로 개발시 성능상 가장 주의해야 하는것은 N+1
  • 즉시로딩과 N+1
  • 지연로딩과 N+1
  • 패치 조인 사용
  • 하이버네이트 @BatchSize
  • 하이버네이트 @Fetch(FetchMode.SUBSELECT)
  • 정리
    • 모두 지연로딩으로 설정하고 성능 최적화가 꼭 필요한 곳은 JPQL패치 조인 사용

읽기 전용쿼리 성능 최적화

  • 엔티티가 영속성 컨텍스트에 관리되면 1차 캐시부터 변경 감지까지 얻을 수 있음
  • 하지만 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 더 많은 메모리를 사용
  • 조회만 수행하는 경우 읽기 전용으로 엔티티를 조회하면 메모리 사용량을 최적화 할 수 있음
  • 읽기 전용으로 수행 방법
    • 스칼라 타입으로 조회
    • 읽기 전용 쿼리 힌트 사용
    • 읽기 전용 트랜잭션 사용
      • 영속성 컨텍스트를 플러시 하지 않음
      • 플러시할때 발생하는 스냅샷 비교 등과 같은 무거운 로직이 수행이 안됨
      • 트랜잭션을 시작했으므로 트랜잭션 시작, 로직 수행, 커밋과정은 이루어지지만 영속성 컨텍스트에 륽러시 하지 않을 뿐임
    • 트랜잭션 밖에서 읽기

배치처리

  • JPA 등록 배치
  • JPA 페이징 배치 처리
  • 하이버네이트 무상태 세션 사용

SQL 쿼리 힌트 사용

트랜잭션을 지원하는 쓰기 지연과 성능 최적화

  • 트랜잭션을 지원하는 쓰기 지연과 JDBC 배치
    • hibernate.jdbc.batch_size
    • 모아서 SQL 배치를 수행
    • 배치는 같은 SQL일때만 유효
  • 트랜잭션을 지원하는 쓰기 지연과 애플리케이션 확장성
    • 트랜잭션을 지원하는 쓰기 지연과 변경 감지 기능 덕분에 성능과 개발의 편의성이라는 두 마리 토끼를 모두 잡을 수 있었음
    • 진짜 장점은 데이터베이스 테이블 로우에 락이 걸리는 시간을 최소화 한다는 점
    • JPA는 커밋을 해야 플러시를 호출하고 데이터베이스에 수정 쿼리를 보냄