15장 고급 주제와 성능 최적화
자바 ORM 표준 JPA 프로그래밍 15장을 요약한 내용 입니다.
예외 처리
JPA 표준 예외들은 javax.persistence.PersistenceException
의 자식 클래스다. 그리고 이 예외 클래스는 RuntimeException
의 자식이다. 따라서 JPA
예외는 모두 언체크 예외다. JPA 표준 예외는 크게 2가지로 나눌 수 있다.
트랜잭션 롤백을 표시하는 예외
트랜잭션 롤백을 표시하지 않는 예외
트랜잭션 롤백을 표시하는 예
예 | 설 |
javax.persistence.EntityExistException | EntityManager.persist(...) 호출 시 이미 같은 엔티티가 있으면 발생한다. |
java.persistence.EntityNotFoundException | EntityManager.getReference(...)를 호출했는데 실제 사용 시 엔티티가 존재하지 않으면 발생. refersh(...), lock(...)에서도 발생한다. |
javax.persistence.OptimisticLockException | 낙관적 락 충돌 시 발생한다. |
javax.persistence.PessimisticLockException | 비관적 락 충돌 시 발생한다. |
javax.persistence.RollbackException EntityTransaction.commit() | 실패 시 발생, 롤백이 표시되어 있는 트랜잭션 커밋 시에도 발생한다. |
javax.persistence.TransactionRequiredException | 트랜잭션이 필요할 때 트랜잭션이 없으면 발생. 트랜잭션 없이 엔티티를 변경할 때 주로 발생한다. |
트랜잭션 롤백을 표시하지 않는 예
예외 | 설명 |
javax.persistence.NoResultException | Query.getSingleResult() 호출 시 결과가 하나도 없을 때 발생한다 |
javax.persistence.NonUniqueResultException | Query.getSingleResult() 호출 시 결과가 둘 이상일 때 발생한다. |
javax.persistence.LockTimeoutException | 비관적 락에서 시간 초과 시 발생한다 |
javax.persistence.QueryTimeoutException | 쿼리 실행 시간 초과 시 발생한다. |
JPA 예외를 스프링 예외로 변
JPA 예외 | 스프링 변환 예외 |
javax.persistence.PersistenceException | org.springgramework.orm.jpa.JpaSystemException |
javax.persistence.NoResultException | org.springfreamework.dao.ExptyResultDataAccessException |
javax.persistence.NonUniqueResultException | org.springframework.dao.IncorrectrResultSizeDataAccessException |
javax.persistence.LockTimeoutException | org.springframework.dao.CannotAcquireLockException |
javax.persistence.QueryTimeoutException | org.springframework.dao.DataIntegrityViolationException |
javax.persistence.EntityExistException | org.springframework.orm.jpa.JpaObjectRetrievalFailurewException |
javax.persistence.EntityNotFoundException | org.springframework.orm.jpa.JpaObjectRetrivalFailtueException |
javax.persistence.OptimisticLockException | org.springframework.dao.JpaOptimisticLockingFailureException |
javax.persistence.PessimisticLockException | org.springframework.dao.PessimisticLockingFailureException |
javax.persistence.TransactionRequiredException | org.springframework.dao.InvalidDataAccessApiUsageException |
javax.persistence.RollbackException | org.springframework.transaction.TransactionSystemException |
JPA 예외를 스프링 예외로 변경 추
JPA 예외 | 스프링 변환 예외 |
java.lang.IllegalStateException | org.springframework.dao.InvalidDataAccessApiUsageException |
java.lang.IllegalArgumentException | org.springframework.dao.InvalidDataAccessApiUsageException |
스프링 프레임워크에 JPA 예외 변환기 적용
JPA
예외를 스프링 프레임워크가 제공하는 추상화된 예외로 변경하려면 PersistenceExceptionTransactionPostProcessor
를 스프링 빈으로 등록하면 된다. 이것은 @Repository
어노테이션을 사용한 곳에 예외 변환 AOP
를 적용해서 JPA
예외를 스프링 프레임워크가 추상화한 예외로 변환해준다.
만약 예외를 변환하지 않고 그대로 반환하고 싶으면 다음처럼 throws
절에 그대로 반환할 JPA
예외나 JPA
예외의 부모 클래스를 직접 명시 하면 된다.
트랜잭션 롤백 시 주의사항
트랜잭션
을 롤백
하는 것은 데이터베이스의 반영 사항만 롤백 하는 것이지 수정한 자바 객체
까지 원 상태로 복구해주지는 않는다. 예를 들어 엔티티를 조회해서 수정하는 중에 문제가 있어서 트랜잭션을 롤백하면 데이터베이스의 데이터는 원래대로 복구되지만 객체는 수정된 상태로 영속성 컨텍스트에 남아 있다. 따라서 새로운 영속성 컨텍스트
를 생성해서 사용하거나 EntityManager.clear()
를 호출해서 영속성 컨텍스트
를 초기화한 다음에 사용해야 한다.
기본 전략인 트랜잭션당 영속성 컨텍스트 전략은 문제가 발생하면 트랜잭션 AOP
종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함께 종료하므로 문제가 발생하지 않는다.
문제는 OSIV
처럼 영속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 사용해서 여러 트랜잭션이 하나의 영속성 컨텍스트를 사용할 때 발생한다. 스프링 프레임워크는 영속성 컨텍스트의 범위를 트랜잭션의 범위보다 넓게 설정하면 트래잭션 롤백시 영속성 컨텍스트를 초기화해서 잘못된 영속성 컨텍스트를 사용하는 문제를 예방한다.
엔티티 비교
영속성 컨텍스트를 통해 데이터를 저장하거나 조회하면 1차 캐시
에 엔티티가 저장된다. 이 1차 캐시 덕분에 변경 감지
기능도 동작하고, 이름 그대로 1차 캐시
로 사용 되어서 데이터베이스를 통하지 않고 데이터를 바로 조회
할 수도 있다.
이것은 같은 트랜잭션 범위에 있으므로 같은 영속성 컨텍스트를 사용하기 때문이다. 따라서 영속성 컨텍스트가 같으면 엔티티를 비교할 때 다음 3가지 조건을 모두 만족한다.
동일성:
==
비교가 같다동등성 :
equals()
비교가 같다데이터베이스 동등성 :
@Id
인 데이터베이스 식별자가 같다
정리하자면 동일성 비교
는 같은 영속성 컨텍스트의 관리를 받는 영속 상태의 엔티티에만 적용할 수 있다. 그렇지 않을 때는 비즈니스 키
를 사용한 동등성 비교
를 해야 한다.
프록시 심화 주제
프록시
는 원본 엔티티를 상속받아서 만들어지므로 엔티티
를 사용하는 클라이언트는 엔티티가 프록시인지 아니면 원본 엔티티인지 구분하지 않고 사용할 수 있다. 이로 인해 예상하지 못한 문제들이 발생하기도 하는데, 어떤 문제가 발생하고 어떻게 해결해야 하는지 알아보자
영속성 컨텍스트와 프록시
엔티티
의 동등성을 비교하려면 비즈니스 키를 사용해서 equals()
메소드를 오버라이딩하고 비교하면 된다. 그런데 IDE나 외부 라이브러리를 사용해서 구현한 equals() 메소드로 엔티티를 비교할 때, 비교 대상이 원본 엔티티면 문제가 없지만 프록시
면 문제가 발생할 수 있다.
테스트 코드를 작성해보자
왜 이런 문제가 발생할까?
프록시는 원본을 상속받은
자식 타입
이므로 프록시의 타입을 비교할 때는==
비교가 아닌instanceof
를 사용해야 한다,. 따라서 다음처럼 변경해야 한다.memner.name
을 보면 프록시의 멤버변수에 직접 접근하는데 이 부분을 주의깊게 봐야 한다. 프록시는 실제 데이터를 가지고 있지 않기 때문에 프록시의멤버 변수
에 직접 접근하면 아무값도 조회할 수 없다. 그러므로 프록시의 데이터를 조회할 때는접근자(Getter)
를 사용해야 한다.
상속관계와 프록시
상속 관계
를 프록시로 조회할 때 발생할 수 있는 문제점과 해결방안을 알아보자
위와 같은 구조의 클래스 모델을 생성할 경우 프록시를 부모 타입으로 조회하면 문제가 발생한다.
그런데 출력 결과를 보면 기대와는 다르게 저자가 출력되지 않은 것을 알 수 있다.
왜 원하는 출력값이 다를까?
실제 조회된 엔티티는 Book
이므로 Book
타입을 기반으로 원본 엔티티 인스턴스가 생성된다. 그런데 em.getReference()
메소드에서 Item
엔티티를 대상으로 조회 했으므로 프록시인 proxyItem
은 Item
타입을 기반으로 만들어진다. 이런 이유로 다음 연산이 기대와 다르게 false
를 반환한다. 왜냐하면 proxyItem
은 Item&Proxy
타입이고 이 타입은 Book
타입과 관계가 없기 때문이다.
따라서 직접 다운캐스팅을 해도 문제가 발생한다.
그렇다면 상속관계에서 발생하는 프록시 문제를 어떻게 해결해야 할까?
JPQL로 대상 직접 조회
프록시 벗기기
그런데 이 방법은 프록시에서 원본 엔티티를 직접 꺼내기 때문에 프록시와 원본 엔티티의
동일성 비교
가 실패한다는 문제점이 있다.기능을 위한 별도의 인터페이스 제공
비지터 패턴
비지터 패턴의 장점은 다음과 같다.
프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근할 수 있다.
instanceof
와 타입 캐스팅 없이 코드를 구현할 수 있다.알고리즘과 객체 주고를 분리해서 구조를 수정하지 않고
새로운 동작
을 추가할 수 있다.
단점은 다음과 같다.
너무 복잡하고
더블 디스패치
를 사용하기 때문에 이해하기 어렵다.객체 구조가 변경되면 모든 Visitor를
수정
해야 한다.
성능 최적화
JPA로 애플리케이션을 개발할 때 발생하는 다양한 성능 문제와 해결 방안을 알아보자.
N+1 문제
즉시 로딩과 N+1
em.find()
메소드로 조회하면 즉시 로딩으로 설정한 주문정보도 함께 조회한다.
문제는 JPQL
을 사용할 때 발생한다.
지연 로딩과 N+1
N+1 문제 해결 방법
페치 조인 사용
페치 조인은 SQL 조인을 사용해서 연관된 엔티티를 함께 조회하므로 N+1
문제가 발생하지 않는다.
참고로 이 예제는 일대다 조인을 했으므로 결과가 늘어나서 중복된 결과가 나타날 수 있다. 따라서
JPQL
의DISTINCT
를 사용해서 중복을 제거하는 것이 좋다.
하이버네이트 @BatchSize
BatchSize
어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정한 size만큼 SQL의 IN절을 사용해서 조회한다. 만약 조회한 회원이 10명인데 size=5
로 지정하면 2번의 SQL
만 추가로 실행한다.
하이버네이트 @Fetch(FetchMode.SUBSELECT)
FetchMode
를 SUBSELECT
로 사용하면 연관된 데이터를 조회할 때 서브 쿼리를 사용해서 N+1
문제를 해결할 수 있다.
읽기 전용 쿼리의 성능 최적화
엔티티가 영속성 컨텍스트에 관리되면 1차 캐시
부터 변경 감지
까지 얻을 수 있는 혜택이 많다. 하지만 영속성 컨텍스트는 변경 감지
를 위해 스냅샷
인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있다. 이때는 읽기 전용으로 엔티티를 조회하면 메모리 사용량
을 최적화
할 수 있다.
위와 같이 읽기 전용
으로 설정하면 트랜잭션
을 커밋
해도 영속성 컨텍스트를 플러시
하지 않는다. 그러므로 플러시할 때 일어나는 스냅샷
비교와 같은 무거운 로직들을 수행하지 않으므로 성능이 향상된다.
엔티티 매니저의 플러시 설정에는
AUTO
,COMMIT
모드만 있고,MANUAL
모드가 없다. 반면에 하이버네이트 세션의 플러시 설정에는MANUAL
모드가 있다.MANUAL
모드는 강제로 플러시를 호풀하지 않으면 절대 플러시가 발생하지 않는다.
배치 처리
수백만 건의 데이터를 배치 처리
해야 하는 상황이라 가정해보자. 일반적인 방식으로 엔티티를 계속 조회하면 영속성 컨텍스트
에 아주 많은 엔티티가 쌓이면서 메모리 부족 오류
가 발생한다. 따라서 이런 배치 처리는 적절한 단위로 영속성 컨텍스트를 초기화해야 한다. 배치 처리는 아주 많은 데이터를 조회해서 수정한다. 이때 수많은 데이터를 한번에 메모리에 올려둘 수 없어서 2가지 방법을 주로 사용한다.
페이징 처리
커서
JPA 페이징 배치 처리
이는 한번에 100건씩 페이징 쿼리로 조회하면서 상품의 가격을 100원씩 증가한다. 그리고 페이지 단위마다 영속성 컨텍스트를 플러시하고 초기화한다.
하이버네이트 scroll 사용
하이버네이트는 scroll이라는 이름으로 JDBC 커서를 지원한다.
scroll은 하이버네이트 전용 기능이므로 먼저 em.unwrap() 메소드를 사용해서 하이버네이트 세션을 구한다. 다음으로 쿼리를 조회하면서 scroll() 메소드로 ScrollableResults 객체를 반환받는다. 이 객체의 next() 메소드를 호출하면 엔티티를 하나씩 조회할 수 있다.
SQL 쿼리 힌트 사용
SQL 힌트를 사용하려면 하이버네이트를 직접 사용해야 한다. SQL 힌트는 하이버네이트 쿼리가 제공하는 addQueryHint() 메소드를 사용한다.
정리
JPA의 예외는 트랜잭션 롤백을 표시하는 예외와 표시하지 않는 예외로 나눈다. 트랜잭션을 롤백하는 예외는 심각한 예외이므로 트랜잭션을 강제로 커밋해도 커밋되지 않고 롤백된다.
스프링 프레임워크는 JPA의 예외를 스프링 프레임워크가 추상화한 예외로 변환해준다.
프록시를 사용하는 클라이언트는 조회한 엔티티가 프록시인지 아니면 원본 엔티티인지 구분하지 않고 사용할 수 있어야 한다. 하지만 프록시는 기술적인 한계가 있으므로 한계점을 인식하고 사용해야 한다.
JPA를 사용할 때는 N+1 문제를 가장 조심해야 한다. N+1 문제는 주로 페치 조인을 사용해서 해결한다.
엔티티를 읽기 전용으로 조회하면 스냅샷을 유지할 필요가 없고 영속성 컨텍스트도 초기화해야 한다.
JPA는 SQL 쿼리 힌트를 지원하지 않지만 하이버네이트 구현체를 사용하면 SQL 쿼리 힌트를 사용할 수 있다.
트랜잭션을 지원하는 쓰기 지연 덕분에 SQL 배치 기능을 사용할 수 있다.
질문
예외에 따라 롤백기능이 다름
체크드 → 롤백 안됨
언 체크드 → 롤백됨
비관적 락 해결 방법?
Last updated