- 예외 처리: JPA를 사용할 때 발생하는 다양한 예외와 예외에 따른 주의점 설명
- 엔티티 비교: 엔티티를 비교할 때 주의점과 해결 방법을 설명
- 프록시와 심화 주제: 프록시로 인해 발생하는 다양한 문제점과 해결 방법을 다룸
- 성능 최적화
: N+1문제 발생하는 상황과 해결방법
: 엔티티를 단순회 조회만 하면 영속성 컨텍스트에 스냅샷을 유지할 필요도 없고 영속성 컨텍스트를 플러시할 필요도 없다. 엔티티를 읽기 전용으로 할 때 성능 최적화 방안을 다룸
: 수백만 건의 데이터를 처리해야 하는 배치 처리 상황에서 JPA를 어떻게 사용할 수 있는지 다룸
: 하이버네이트를 통해 SQL 쿼리 힌트를 사용하는 방법을 다룸
: 트랜잭션을 지원하는 쓰기 지연을 통해 성능을 최적화하는 방법을 다룸
15.1 예외 처리
15.1.1 JPA 표준 예외 정리
JPA 표준 예외들은 javax.persistence.PersistenceException의 자식 클래스다. 이 예외 클래스는 RuntimeException을 상속 받았기에 UnCheckedException이다.
JPA 표준 예외는 크게 2가지로 나눌 수 있다.
- 트랜잭션 롤백을 표시하는 예외
: 심각한 예외이므로 복구해선 안 된다. 이 예외가 발생하면 트랜잭션을 강제로 커밋해도 트랜잭션이 커밋되지 않고 대신에 javax.persistence.RollbackException 예외가 발생한다.
- 트랜잭션 롤백을 표시하지 않는 예외
: 심각한 예외가 아니므로 개발자가 트랜잭션을 커밋할지 롤백할지 판단하면 된다.
15.1.2 스프링 프레임워크의 JPA 예외 변환
서비스 계층에서 데이터 접근 계층의 구현 기술에 직접 의존하는 것은 좋은 설계라 할 수 없다. 예를 들어 서비스 계층에서 JPA의 예외를 직접 사용하면 JPA에 의존하게 된다. 스프링 프레임워크는 이런 문제를 해결하려고 데이터 접근 계층에 대한 예외를 추상화해서 개발자에게 제공한다.
ex)
JPA 예외 -> 스프링 변환 예외
javax.persistence.PersistenceException -> org.springframework.orm.jpa.JpaSystemException
15.1.3 스프링 프레임워크에 JPA 예외 변환기 적용
JPA 예외를 스프링 프레임워크가 제공하는 추상화된 예외로 변경하려면 PersistenceExceptionTranslationPostProcessor를 스프링 빈으로 등록하면 된다. 이것은 @Repository 어노테이션을 사용한 곳에 예외 변환 AOP를 적용해서 JPA 예외를 스프링 프레임워크가 추상화한 예외로 변환해준다.
// xml
<bean class = "org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
// Java Config
@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
return new PersistenceExceptionTranslationPostProcessor();
}
// 예외를 변환하지 않는 코드
@Repository
public class NoResultExceptionTestService {
@PersistenceContext EntityManager em;
public member findMember thros javax.persistence.NoResultException {
return em.createQuery("select ...").getSingleResult();
}
}
15.1.4 트랜잭션 롤백 시 주의 사항
- 트랜잭션을 롤백하는 것은 데이터베이스의 반영사항만 롤백하는 것이지 수정한 자바 객체까지 원상태로 복구해주지는 않는다. 데이터베이스의 데이터는 원래대로 복구되지만 객체는 수정된 상태로 영속성 컨텍스트에 남아 있다. 따라서 트랜잭션이 롤백된 영속성 컨텍스트를 그대로 사용하는 것은 위험하고 새로운 영속성 컨텍스트를 생성해서 사용하거나 EntityManager.clear()를 호출해서 영속성 컨텍스트를 초기화한 다음에 사용해야 한다.
- 스프링 프레임워크는 이런 문제를 예방하기 위해 영속성 컨텍스트의 범위에 따라 다른 방법 사용
: 기본 전략인 트랜잭션당 영속성 컨텍스트 전략은 문제가 발생하면 트랜잭션 AOP 종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함께 종료하므로 문제가 발생하지 않음
: OSIV처럼 영속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 사용해서 여러 트랜잭션이 하나의 영속성 컨텍스트를 사용할 때 발생한다. 그래스 스프링 프레임워크는 영속성 컨텍스트의 범위를 트랜잭잭션의 범위보다 넓게 설정하면 트랜잭션 롤백시 영속성 컨텍스트를 초기화해서 잘못된 영송성 컨텍스트를 사용하는 문제를 예방한다.
-> org.springframework.orm.jpa.JpaTransactionManger의 doRollback() 메소드 참고
15.2 엔티티 비교
- 영속성 컨텍스트 내부에는 엔티티 인스턴스를 보관하기 위한 1차 캐시가 있다. 이 1차 캐시는 영속성 컨텍스트와 생명주기를 같이 한다.
- 1차 캐시 덕분에 변경 감지 기능도 동작하고, 데이터베이스를 통하지 않고 데이터를 바로 조회할 수 있다. 또한 애플리케이션 수준의 반복 가능한 읽기가 가능하다. 같은 영속성 컨텍스트에서 엔티티를 조회하면 항상 같은 엔티티 인스턴스를 반환한다.
15.2.1 영속성 컨텍스트가 같을 때 엔티티 비교
- 영속성 컨텍스트가 같으면 엔티티를 비교할 때 동일성, 동등성, 데이터베이스 동등성 조건을 모두 만족한다.
15.2.2 영속성 컨텍스트가 다를 때 엔티티 비교
- 동일성 비교 실패, 동등성 비교는 equals 구현. 보통 비지니스 키로 구현
- 동일성 비교는 같은 영속성 컨텍스트의 관리를 받는 영속 상태의 엔티티에만 적용할 수 있다. 그렇지 않을 때는 비지니스 키를 사용한 동등성 비교를 해야 한다.
15.3 프록시 심화 주제
- 프록시는 원본 엔티티를 상속받아서 만들어지므로 엔티티를 사용하는 클리어은트는 엔티티가 프록시인지 원본 엔티티인지 구분하지 않고 사용할 수 있다.
15.3.1 영속성 컨텍스트와 프록시
- 영속성 컨텍스트는 프록시로 조회된 엔티티에 대해서 같은 엔티티를 찾는 요청이 오면 원본 엔티티가 아닌 처음 조회된 프록시를 반환한다.
- 원본 엔티티를 먼조 조회하면 영속성 컨텍스트는 원본 엔티티를 이미 데이터베이스에서 조회했으므로 프록시를 조회하더라도 프록시를 반환하지 않고 원본을 반환한다.
// 프록시로 먼저 조회 -> 동일성 보장
Member refMember = em.getReference(Member.class, "member1"); // 프록시로 조회
Member findMember = em.find(Member.class, "member1");
// 원본 먼저 조회 -> 동일성 보장
Member findMember = em.find(Member.class, "member1");
Member refMember = em.getReference(Member.class, "member1");
15.3.2 프록시 타입 비교
- 프록시는 원본 엔티티를 상속 받아서 만들어지므로 프록시로 조회한 엔티티의 타입을 비교할 때는 == 비교를 하면 안 되고 대신에 instanceof를 사용해야 한다.
- 프록시로 조회하면 refMemer.getClass();는 출력 결과 끝에 프록시라는 의미의 _$$_jvsteXXX가 붙어 있다.
- 프록시는 원본 엔티티의 자식 타입이므로 instanceof 연산을 사용하면 된다.
15.3.3 프록시 동등성 비교
- 프록시 타입 비교는 == 비고 대신에 instanceof를 사용해야 한다.
- 프록시의 멤버변수에 직접 접근하면 안 되고 대신에 접근자 메소드를 사용해야 한다.
// 프로시 동등성 비교 예제, 수정
@Override
public boolean equals(Object obj) {
if(this == obj) return true;
if(!(obj instanceof Member)) return false;
Member member = (Member) obj;
if(name != null ? !name.equals(member.getName()) : member.getName() != null) return false;
return true;
}
15.3.4 상속관계와 프록시
- 상속관계를 프록시로 조회할 때 발생할 수 있는 문제점과 해결방안을 알아보자
- 프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성되는 문제가 있다.
: instanceof 연산을 사용할 수 없다.
: 하위 타입으로 다운캐스팅을 할 수 없다.
- 프록시를 부모 타입으로 조회하는 문제는 다형성을 다루는 도메인 모델에서 나타난다.
JPQL로 대상 직접 조회
- 가장 간단한 방법은 처음부터 자식 타입을 직접 조회해서 필요한 연산을 하면 된다.
Book jpqlBook =
em.createQuery("select b from Book b where b.id=:bookId", Book.class)
.setParameter("bookId", item.getId())
.getSingleResult();
프록시 벗기기
- 하이버네이트가 제공하는 기능을 사용하면 프록시에서 원본 엔티티를 가져올 수 있다. 이 방법은 프록시에서 원본 엔티티를 직접 꺼내기 때문에 프록시와 원본 엔티티의 동일성 비교가 실패한다는 문제점이 있다. 이 방법을 사용할 때는 원본 엔티티가 꼭 필요한 곳에서 잠깐 사용하고 다른 곳에서 사용되지 않도록 하는 것이 중요하다. 참고로 원본 엔티티의 값을 직접 변경해도 변경 감지 기능은 동작한다.
// 하이버네이트가 제공하는 프록시에서 원본 엔티티를 찾는 기능을 사용하는 메소드
public static <T> T unProxy(Object entity) {
if(entity instanceof HibernateProxy) {
entity = ((HibernateProxy) entity)
.getHibernateLazyInitializer()
.getImplementation();
}
return (T) entity;
}
기능을 위한 별도의 인터페이스 제공
- 인터페이스를 제공하고 각각의 클래스가 자신에 맞는 기능을 구현하는 것은 다형성을 활용하는 좋은 방법이다. 이 방법은 클라이언트 입장에서 대상 객체가 프록시인지 아닌지를 고민하지 않아도 되는 장점이 있다. 이 방법을 사용할 때는 프록시의 특징 때문에 프록시의 대상이 되는 타입에 인터페이스를 적용해야 한다.
public interface TitleView {
String getTitle();
}
@Entity
@Inheritance(strategy = InheritanceType.SINGE_TABLE)
@DiscriminatorColumn(name ="DTYPE")
public abstact class Item implements TitleView {
...
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
@Override
public String getTitle() {
return "[제목:" + getName() + " 저자:" + author + "]";
}
}
비지터 패턴 사용
- 비지터 패턴을 사용해서 상속관계와 프록시 문제를 해결해보자
- 비지터 패턴은 Visitor와 Visitor를 받아들이는 대상 클래스로 구성된다. Item이 accept(visitor) 메소드를 사용해서 Visitor를 받아들인다. 그리고 Item은 단순히 Visitor을 받아들이기만 하고 실제 로직은 Visitor가 처리한다. Visitor에는 visit()라는 메소드를 정의하고 모든 대상 클래스를 받아들이도록 작성하면 된다. 여기서는 Book, Album, Movie를 대상 클래스로 사용한다.
public interface Visitor {
void visit(Book book);
void visit(Album album);
void visit(Movie movie);
}
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DistriminatorColumn(name = "DTYPE")
public abstract class Item {
...
public abstract void accept(Visitor visitor);
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
@Override
public void accept(Visitor visitor) {
visitor.visit(this); // this는 프록시가 아닌 원본이다.
}
}
// 비지터 사용 코드
@Test
public void 상속관계와_프록시_VisitorPattern() {
...
OrderItem orderItem = em.find(OrderItem.class, orderItemId);
Item item = orderItem.getItem();
// PrintVisitor
item.accept(new PrintVisitor());
}
- 비지터 패턴과 확장성
: 비지터 패턴은 새로운 기능이 필요할 때 Visitor만 추가하면 된다. 따라서 기존 코드의 구조를 변경하지 않고 기능을 추가할 수 있는 장점이 있다.
- 장점
: 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근할 수 있다.
: instanceof와 타입캐스팅 없이 코드를 구현할 수 있다.
: 알고리즘과 객체 구조를 분리해서 구조를 수정하지 않고 새로운 동작을 추가할 수 있다.
- 단점
: 너무 복잡하고 더블 디스패치를 사용하기 때문에 이해하기 어렵다.
: 객체 구조가 변경되면 모든 Visitor를 수정해야 한다.
15.4 성능 최적화
15.4.1 N+1 문제
- 즉시 로딩은 JPQL을 실행할 때 N+1 문제가 발생할 수 있다.
em.find(Member.class, id);
// 실행된 SQL
SELECT M.*, O.*
FROM MEMBER M
OUTER JOIN ORDER O ON M.ID=O.MEMBER_ID
// JPQL
List<Member> members =
em.createQuery("select m from Member m", Member.class)
.getResultList();
// 실행된 SQL
SELECT * FROM MEMBER
SELECT * FROM ORDERS WHERE MEMBER_ID=1
SELECT * FROM ORDERS WHERE MEMBER_ID=2
SELECT * FROM ORDERS WHERE MEMBER_ID=3
SELECT * FROM ORDERS WHERE MEMBER_ID=4
SELECT * FROM ORDERS WHERE MEMBER_ID=5
- 지연 로딩에서는 JPQL 실행 시점에 N+1 문제가 발생하지 않지만 지연 로딩으로 설정된 연관관계 컬렉션을 사용할 때 발생한다.
페치 조인 사용
- N+1 문제를 해결하는 가장 일반적인 방법은 페치 조인을 사용하는 것이다. 페치 조인은 SQL 조인을 사용해서 연관된 엔티티를 함께 조회하므로 N+1 문재가 발생하지 않는다.
- 페치 조인을 사용하는 JPQL을 보자. 아래 예제는 일대다 조인을 했으므로 결과가 늘어나서 중복된 결과가 나타날 수 있다. 따라서 JPQL의 DISTINCT를 사용해서 중복을 제거하는 것이 좋다.
select m from Member m join fetch m.orders
// 실행된 SQL
SELECT M.*, O.* FROM MEMBER M
INNER JOIN ORDERS O ON M.ID=O.MEMBER_ID
하이버네이트 @BatchSize
- 하이버네이트가 제공하는 org.hibernate.annotations.BatchSize 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정한 size만큼 SQL의 IN 절을 사용해서 조회한다.
- 즉시 로딩으로 설정하면 조회 시점에 10건의 데이터를 모두 조해해야 하므로 다음 SQL이 두 번 실행된다. 지연 로딩으로 설정하면 지연 로딩된 엔티티를 최초 사용하는 시점에 다음 SQL을 실행해서 5건의 데이터를 미리 로딩해둔다. 그리고 6번째 데이터를 사용하면 IN 절로 4개 데이터를 조회한다.
@Entity
public class Member {
@org.hibernate.annotations.BatchSize(Size = 5)
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
private List<Order> orders = new ArrayList<Order>();
...
}
// hibernate.default_batch_fetch_size 속성을 사용하면 애플리케이션 전체에 기본으로 @BatchSize를 적용할 수 있다.
<property name="hibernate.default_batch_fetch_size" value="5" />
하이버네이트 @Fetch(FetchMode.SUBSELECT)
- 하이버네이트가 제공하는 org.hibernate.annotations.Fetch 어노테이션에 FetchMode를 SUBSELECT로 사용하면 연관된 데이터를 조회할 때 서브 쿼리를 사용해서 N+1 문제를 해결한다.
SELECT O FROM ORDERS O
WHERE O.MEMBER_ID IN (
SELECT
M.ID
FROM
MEMBER M
WHERE M.ID > 10
)
N+1 정리
- 즉시 로딩과 지연 로딩 중 추천하는 방법은 즉시 로딩은 사용하지 말고 지연 로딩만 사용하는 것이다. 즉시 로딩 전략은 N+1 문제는 물론이고 비지니스 로직에 따라 필요하지 않은 엔티티를 로딩해야 하는 상황이 자주 발생한다. 그리고 즉시 로딩의 가장 큰 문제는 성능 최적화가 어렵다는 점이다. 엔티티를 조회하다보면 즉시 로딩이 연속으로 발생해서 전혀 예상하지 못한 SQL이 실행될 수 있다. 따라서 모두 지연 로딩으로 설정하고 성능 최적화가 꼭 필요한 곳에는 JPQL 페치 조인을 사용하자.
15.4.2 읽기 전용 쿼리의 성능 최적화
- 엔티티가 영속성 컨텍스트에 관리되면 1차 캐피부터 변경 감지까지 얻을 수 있는 혜택이 많다. 하지만 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있다. 조회만 있는 경우 읽기 전용으로 엔티티를 조회하면 메모리 사용량을 최적화할 수 있다.
- 스칼라 타입으로 조회
: 엔티티가 아닌 스칼라 타입으로 모든 필드를 조회한다. 스칼라 타입은 영속성 컨텍스트가 결과를 관리하지 않는다.
select o.id, o.name, o.price form Order o
- 읽기 전용 쿼리 힌트 사용
: 하이버네이트 전용 힌트인 org.hibernate.readOnly를 사용하면 엔티티를 읽기 전용으로 조회할 수 있다. 읽기 전용이므로 영속성 컨텍스트는 스냅샷을 보관하지 않는다. 따라서 메모리 사용량을 최적화할 수 있다. 단 스냅샷이 없으므로 엔티티를 수정해도 데이터베이스에 반영되지 않는다.
TypeQuery<Order> query = em.createQuery("select o from Order o", Order.class)
query.setHint("org.hibernate.readOnly", true);
- 읽기 전용 트랜잭션 사용
: 스프링 프레임워크를 사용하면 트랜잭션 읽기 전용 모드로 설정할 수 있다. 트랜잭션에 readOnly=true 옵션을 주면 스프링 프레임워크가 하이버네이트 세션의 플러시 모드를 MANUAL로 설정한다. 이렇게 하면 강제로 플러시를 호출하지 않는 한 플러시가 일어나지 않는다. 따라서 트랜잭션을 커밋해도 영속성 컨텍스트를 플러시하지 않는다. 물론 트랜잭션을 시작했으므로 트랜잭션 시작, 로직수행, 트랜잭션 커밋의 과정은 이루어진다. 단지 영속성 컨텍스트를 플러시하지 않을 뿐이다.
@Transactional(readOnly = true)
엔티티 매니저 플러시 설정에는 AUTO, COMMIT 모드만 있고, MANUAL 모드가 없다. 반면에 하이버네이트 세션(org.hibernate.Session)의 플러시 설정에는 MANUAL 모드가 있다. MANUAL 모드는 강제로 플러시를 호출하지 않으면 절대 플러시가 발생하지 않는다.
엔티티 매니저의 unwrap() 메소드를 호출하면 하이버네이트 세션을 구할 수 있다.
Session session = entityManager.unwrap(Session.class);
- 트랜잭션 밖에서 읽기
: 트랜잭션 밖에서 읽는다는 것은 트랜잭션 없이 엔티티를 조회한다는 뜻이다. 아래처럼 트랜잭션을 사용하지 않으면 플러시가 일어나지 않으므로 조회 성능이 향상된다.
: 기본적으로 플러시 모드는 AUTO로 설정되어 있다. 따라서 트랜잭션을 커밋하거나 쿼리를 실행하면 플러시가 작동한다. 그런데 트랜잭션 자체가 존재하지 않으므로 트랜잭션을 커밋할 일이 없다. 그리고 JPQL 쿼리도 트랜잭션 없이 실행하면 플러시를 호출하지 않는다.
// 스프링 프레임워크
@Transactional(propagation = Propagation.NOT_SUPPORTED) // Spring
// J2EE 표준 컨테이너
@TransationAttribute(TransactionAttributeType.NOT_SUPPORTED) // J2EE
: 읽기 전용 데이터를 조회할 때, 메모리를 최적화하려면 스칼라 타입으로 조회하거나 하이버네이트가 제공하는 읽기 전용 쿼리 힌트를 사용하면 된다. 플러시 호출을 막아서 속도를 최적화하려면 읽기 전용 트랜잭션을 사용하거나 트랜잭션 밖에서 읽기를 사용하면 된다. 참고로 스프링 프레임워크를 사용하면 읽기 전용 트랜잭션을 사용하는 것이 편리하다.
@Transactional(readOnly = true) // 읽기 전용 트랜잭션: 플러시를 작동하지 않도록 해서 성능 향상
public List<DataEntity> findDatas() {
return em.createQuery("select d from DataEntity d", DataEntity.class)
.setHint("org.hibernate.readOnly", true) // 읽기 전용 쿼리 힌트: 엔티티를 읽기 전용으로 조회해서 메모리 절약
.getResultList();
}
15.4.3 배치 처리
- 배치 처리는 적절한 단위로 영속성 컨텍스트를 초기화해야 한다. 또한 2차 캐시를 사용하고 있다면 2차 캐시에 엔티티를 보관하지 않도록 주의해야 한다.
JPA 등록 배치
- 수천에서 수만 건 이상의 엔티티를 한 번에 등록할 때 주의할 점은 영속성 컨텍스트에 엔티티가 계속 쌓이지 않도록 일정 단위마다 영속성 컨텍스트의 엔티티를 데이터베이스에 플러시하고 영속성 컨텍스트를 초기화해야 한다. 만약 이런 작업을 하지 않으면 영속성 컨텍스트에 너무 많은 엔티티가 저장되면서 메모리 부족 오류가 발생할 수 있다.
JPA 수정 배치
- 배치 처리는 아주 많은 데이터를 조회해서 수정한다. 이때 수많은 데이터를 한번에 메모리에 올려둘 수 없어서 2가지 방법을 주로 사용한다.
- 페이징 처리: 데이터베이스 페이징 기능을 사용한다.
- 커서: 데이터베이스가 지원하는 커서 기능을 사용한다.
JPA 페이징 배치 처리
- 페이지 단위마다 영속성 컨텍스트를 플러시하고 초기화 한다.
하이버네이트 scroll 사용
- JPA는 JDBC 커서(CURSOR)를 지원하지 않는다. 커서를 사용하려면 하이버네이트 세션(Session)을 사용해야 한다.
- 하이버네이트는 scroll이라는 이름으로 JDBC 커서를 지원한다.
- scroll은 하이버네이트 전용 기능이므로 먼저 em.unwrap() 메소드를 사용해서 하이버네이트 세션을 구한다. 다음으로 쿼리를 조회하면서 scroll 메소드로 scrolalbleResults 객체를 반환받는다. 이 객체의 next() 메소드를 호출하면 엔티티를 하나씩 조회할 수 있다.
EntityTransaction tx = em.getTransaction();
Session session = em.unwrap(Session.class);
tx.begin();
ScrollableResults scroll =
session.createQuery("select p from Product p")
.setCacheMode(CacheMode.IGNORE) // 2차 캐시 기능을 끈다.
.scroll(ScrollMode.FORWARD_ONLY);
int count = 0;
while(scroll.next()) {
Product p = (Product) scroll.get(0);
p.setPrice(p.getPrice() + 100);
count++;
if(count % 100 == 0) {
session.flush(); // 플러시
session.clear(); // 영속성 컨텍스트 초기화
}
}
tx.commit();
session.close();
하이버네이트 무상태 세션 사용
- 하이버네이트는 무상태 세션이라는 특별한 기능을 제공한다. 이름 그대로 무상태 세션은 영속성 컨텍스트를 만들지 않고 심지어 2차 캐시도 사용하지 않는다. 무상태 세션은 영속성 컨텍스트가 없다. 그리고 엔티티를 수정하려면 무상태 세션이 제공하는 update() 메소드를 직접 호출해야 한다.
- 하이버네이트 무상태 세션은 일반 하이버네이트 세션과 거의 비슷하지만 영속성 컨텍스트가 없다. 따라서 영속성 컨텍스트를 플러시하거나 초기화하지 않아도 된다. 대신에 엔티티를 수정할 때 update() 메소드를 직접 호출해야 한다.
SessionFactory sessionFactory = entityMangerFactory.unwrap(SessionFactory.class);
StatelessSession session = sessionFactory.openStatelessSession();
Transaction tx = session.beginTransaction();
ScrollableResults scroll = sesison.createQuery("select p from Product p").scroll();
while(scroll.next)) {
Product p = (Product) scroll.get(0);
p.setPrice(p.getPrice() + 100);
session.update(p); // 직접 udpate를 호출해야 한다.
}
tx.commit();
session.close();
15.4.4 SQL 쿼리 힌트 사용
- JPA는 데이터베이스 SQL 힌트 기능을 제공하지 않는다. SQL 힌트를 사용하려면 하이버네이트를 직접 사용해야 한다. (여기서 말하는 SQL 힌트는 JPA 구현체에게 제공하는 힌트가 아니다. 데이터베이스 벤더에게 제공하는 힌트다.)
- SQL 힌트는 하이버네이트 쿼리가 제공하는 addQueryHint() 메소드를 사용한다.
Session session = em.unwrap(Session.class); // 하이버네이트 직접 사용
List<Member> list =
session.creatQuery("select m from Member m")
.addQueryHint("FULL (MEMBER)") // SQL HINT 추가
.list();
// 실행된 SQL
select
/*+ FULL (MEMBER) */ m.id, m.name
from
Member m
- 현재 하이버네이트 4.3.10 버전에는 오라클 방언에만 힌트가 적용되어 있다. 다른 데이터베이스에 SQL 힌트를 사용하려면 각 방언에서 org.hibernate.dialect.Dialect에 있는 다음 메소드를 오버라이딩해서 기능을 구현해야 한다.
public String getQueryHintString(String query, List<String> hints) {
return query;
}
15.4.5 트랜잭션을 지원하는 쓰기 지연과 성능 최적화
- 트랜잭션을 지원하는 쓰기 지연을 활용해서 애플리케이션 성능을 최적화하는 방법을 알아보자.
트랜잭션을 지원하는 쓰기 지연과 JDBC 배치
- JPA는 플러시 기능이 있으므로 SQL 배치 기능을 효과적으로 사용할 수 있다. 참고로 SQL 배치 최적화 전략은 구현체마다 조금씩 다르다. 하이버네이트에서 SQL 배치를 적용하려면 다음과 같이 설정하면 된다.
- hibernate.jdbc.batch_size 속성의 값을 50으로 주면 최대 50건씩 모아서 SQL 배치를 실행한다. 하지만 SQL 배치는 같은 SQL일 때만 유효한다. 중간에 다른 처리가 들어가면 SQL 배치를 다시 시작한다.
<property name="hibernate.jdbc.batch_size" value="50/>
엔티티가 영속 상태가 되려면 식별자가 꼭 필요하다. 그런데 IDENTITY 식별자 생성 전략은 엔티티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로 em.persist()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달된다. 따라서 쓰기 지연을 활용한 성능 최적화를 할 수 없다.
트랜잭션을 지원하는 쓰기 지연과 애플리케이션 확장성
- 트랜잭션을 지원하는 쓰기 지연과 변경 감지 기능 덕분에 성능과 개발의 편의성이라는 장점을 얻을 수 있었다. 그런데 진짜 장점은 데이터베이스 테이블 row에 lock이 걸리는 시간을 최소화한다는 점이다. 이 기능은 트랜잭션을 커밋해서 영속성 컨텍스트를 플러시하기 전까지는 데이터베이스에 데이터를 등록, 수정, 삭제하지 않는다. 따라서 커밋 직전까지 데이터베이스 row에 락을 걸지 않는다.
- JPA의 쓰기 지연 기능은 데이터베이스에 락이 걸리는 시간을 최소화해서 동시에 더 많은 트랜잭션을 처리할 수 있는 장점이 있다.
15.5 정리
- JPA의 예외는 트랜잭션 롤백을 표시하는 예외와 표시하지 않는 예외로 나눈다. 트랜잭션을 롤백하는 예외는 심각한 예외이므로 트랜잭션을 강제로 커밋해도 커밋되지 않고 롤백된다.
- 스프링 프레임워크는 JPA의 예외를 스프링 프레임워크가 추상화한 예외로 변환해준다.
- 같은 영속성 컨텍스트의 엔티티를 비교할 때는 동일성 비교를 할 수 있지만 영속성 컨텍스트가 다르면 동일성 비교에 실패한다. 따라서 자주 변하지 않는 비지니스 키를 사용한 동등성 비교를 해야 한다.
- 프록시를 사용하는 클라이언트는 조회한 엔티티가 프록시인지 아니면 원본 엔티티인지 구분하지 않고 사용할 수 있어야 한다. 하지만 프록시는 기술적인 한계가 있으므로 한계점을 인식하고 사용해야 한다.
- JPA를 사용할 때는 N+1 문제를 가장 조심해야 한다. N+1 문제는 주로 페치 조인을 사용해서 해결한다.
- 엔티티를 읽기 전용으로 조회하면 스냅샷을 유지할 필요가 없고 영속성 컨텍스트를 플러시하지 않아도 된다.
- 대량의 엔티티를 배치 처리하려면 적절한 시점에 꼭 플러시를 호출하고 영속성 컨텍스트도 초기화해야 한다.
- JPQ는 SQL 쿼리 힌트를 지원하지 않지만 하이버네이트 구현체를 사용하면 SQL 쿼리 힌트를 사용할 수 있다.
- 트랜잭션을 지원하는 쓰기 지연 덕분에 SQL 배치 기능을 사용할 수 있다.
다음 장에서는 트랜잭션과 락에 대해 알아보고, 애플리케이션 레벨의 캐시도 알아보자.
'JPA > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
Chap.16 트랜잭션과 락, 2차 캐시 (0) | 2021.09.13 |
---|---|
Chap.14 컬렉션과 부가 기능 (0) | 2021.08.28 |
Chap.13 웹 애플리케이션과 영속성 관리 (0) | 2021.07.30 |
Chap.12 스프링 데이터 JPA (0) | 2021.07.19 |
Chap.10 객체지향 쿼리 언어 - 2 (0) | 2021.07.05 |