예외 처리
- 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 를 빈으로 등록해야함
- 트랜잭션 롤백 시 주의사항
- 트랜잭션을 롤백하는 것은 데이터베이스의 반영사항을 롤백하는것
- 엔티티(자바)를 원상 복구하는 것은 아님
- 객체는 수정된 상태로 영속성 컨텍스트에 남아있음
- 따라서 롤백된후에는 새로운 영속성 컨텍스트를 생성하여 사용하거나 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는 커밋을 해야 플러시를 호출하고 데이터베이스에 수정 쿼리를 보냄