트랜잭션 롤백을 표시하는 예외
| 예외 | 설명 |
|---|---|
| EntityExistsException | EntityManager.persist() 호출 시 이미 같은 엔티티가 있으면 발생. |
| EntityNotFoundException | EntityManager.getReference()를 호출했는데, 실제 사용 시 엔티티가 존재하지 않으면 발생, refresh(), lock()에서도 발생. |
| OptimisticLockException | 낙관적 락 충돌 시 발생 |
| PessimisticLockException | 비관적 락 충돌 시 발생 |
| RollbackException | EntityTransaction.commit() 실패 시 발생. 롤백 |
트랜잭션 롤백을 표시하지 않는 예외
| 예외 | 설명 |
|---|---|
| NoResultException | Query.getSingleResult() 호출 시 결과가 하나도 없을 때 발생 |
| NonUniqueResultException | Query.getSingleResult() 호출 시 결과가 둘 이상일 때 발생. |
| LockTimeoutException | 비관적 락에서 시간초과 시 발생 |
| QueryTimeoutException | 쿼리 실행 시간 초과 시 발생 |
PersistenceExceptionTranslationPostProcessor를 스프링 빈으로 등록@Repository 어노테이션을 사용한 곳에 예외 변환 AOP를 적용해 JPA 예외를 스프링 프레임워크가 추상화한 예외로 변환해줌.설정방법:
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
JavaConfig를 사용하여 등록:
@Bean
public PersistenceExceptionTranslationPostProcessorexceptionTranslation(){
return new PersistenceExceptionTranslationPostProcessor();
}
예) 예외 변환 예제
@Repository
public class NoResultExceptionTestRepository {
@PersistenceContext EntityManager em;
public Member findMember() {
//조회된 데이터 X
return em.createQuery("select m from Member m", Member.class).getSingleResult();
}
}
findMember() 메소드에서 엔티티 조회를 위해 getSingleResult() 메소드를 사용findMember() 메소드를 빠져나갈 때, PersistenceExceptionTranslationPostProcessor에 등록한 AOP 인터셉터가 동작해 해당 예외를 EmptyResultDataAccessException 예외로 변환해서 반환 ⇒ 따라서 해당 메소드를 호출한 클라이언트는 스프링 프레임워크가 추상화한 예외를 받음.throws 절에 그대로 반환활 JPA 예외 또는 JPA 예외의 부모 클래스를 직접 명시예) 예외를 반환하지 않는 코드
@Repository
public class NoResultExceptionTestService {
@PersistenceContext EntityManager em;
public member findMember() throws NoResultException {
return em.createQuery("select m from Member m", Member.class).getSingleResult();
}
}
EntityManager.clear()를 호출해 영속성 컨텍스트 초기화 후 사용.위의 문제를 예방하기 위해 영속성 컨텍스트의 범위에 따라 다른 방법을 사용함
EntityManager.clear())”를 통해 잘못된 영속성 컨텍스트 사용 문제를 예방.Member member1 = em.find(Member.class, "1L");
Member member2 = em.find(Member.class, "1L");
assertTrue(member1 == member2); //둘은 같은 인스턴스예) 회원가입 테스트 케이스
테스트는 트랜잭션 안에서 시작하기에 테스트의 범위와 트랜잭션의 범위가 아래와 같음.
즉, 테스트 전체에서 같은 영속성 컨텍스트에 접근함

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:appConfig.xml")
@Transactional //트랜잭션 안에서 테스트를 실행
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {...}
}
@Transactional
public class MemberRepository {
@PersistenceContext EntityManager em;
public void save (Member member) {
em.persist(member);
}
public Member findOne (Long id) {
return em.find(Member.class, id);
}
}
@Transactional이 선언되어 있는 경우 트랜잭션을 먼저 시작하고 테스트 메소드를 실행함회원가입()은 이미 트랜잭션 범위에 들어있고, 해당 메소드가 끝나면 트랜잭션이 종료됨회원가입() 에서 사용된 코드는 항상 같은 트랜잭션과 같은 영속성 컨텍스트에 접근아래 3가지 조건을 모두 만족함
== 비교가 같음equals() 비교가 같음@Id인 데이터베이스 식별자가 같음예) 테스트 클래스에 @Transactional이 없고 서비스에만 @Transactional이 있을 때

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:appConfig.xml")
//@Transactional: 테스트에서 트랜잭션을 사용하지 않음
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {...}
}
@Transactional
public class MemberRepository {
@PersistenceContext EntityManager em;
public void save (Member member) {
em.persist(member);
}
public Member findOne (Long id) {
return em.find(Member.class, id);
}
}
@Transactional이 붙어있는지 않는 경우 테스트는 실패함memberService.join()을 호출하여 회원가입 시도 → 트랜잭션이 서비스 계층에서 시작→ 영속성 컨텍스트1 생성 2- memberRepository에서 em.persist()를 호출하여 member 엔티티를 영속화 3- 서비스 계층이 끝날 때 트랜잭션이 커밋되어 영속성 컨텍스트가 플러시 → 이때 트랜잭션과 영속성 컨텍스트 종료. 즉, member 엔티티 인스턴스는 준영속 상태. 4- 테스트코드에서 memberRepository.findOne()을 호출해 엔티티를 조회 → 레포지토리 계층에서 새로운 트랜잭션이 시작 → 새로운 영속성 컨텍스트2 생성 5- 저장된 회원을 조회 → but 새로 생긴 영속성 컨텍스트2에는 회원이 존재하지 않음 6- 데이터베이스에서 회원을 찾아옴 7- 데이터베이스에서 조회된 회원 엔티티를 영속성 컨텍스트에 보관 및 반환 8- memberRepository.findOne() 메소드가 끝나며 트랜잭션이 종료. 영속성 컨텍스트2도 종료됨member와 findMember는 각각 다른 영속성 컨텍스트에서 관리 → 즉, 둘은 다른 인스턴스 assertTrue(member == findMember): 실패== 비교가 실패함equals() 비교가 만족하지만, 반드시 equals()를 구현해야함. 보통 비즈니스키로 구현@Id인 데이터베이스 식별자가 같음member.getId().equals(findMember.getId()) → 데이터베이스 식별자를 비교
equals()를 이용한 동등성 비교엔티티 비교에서는 비즈니스 키를 활용한 동등성 비교를 권장함
equals() 오버라이딩할 때 비즈니스 키가되는 필드들을 선택의문점: 영속성 컨텍스트 = 관리하는 영속 엔티티들의 동일성을 보장 → 다만, 프록시로 조회한 엔티티의 동일성도 보장할까?
예) 프록시 조회 → 원본 엔티티 조회
@Test
public void 영속성컨텍스트와_프록시() {
Member newMember = new Member("member1", "회원1");
em.persist(newMember);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, "member1");
Member findMember = em.find(Member.class, "member1");
Assert.assertTrue(refMember == findMember); //성공
}
getReference() 메소드를 통해 프록시로 조회, 이후 같은 member1을 em.find()로 조회. 전자는 프록시, 후자는 원본 엔티티만일 이 둘을 다른 인스턴스로 보게되면 영속성 컨텍스트가 영속 엔티티의 동일성을 보장하지 못하는 문제 발생
영속성 컨텍스트에서 프록시로 조회된 엔티티에 대해 같은 엔티티를 찾는 요청이 들어올 때, 원본 엔티티가 아닌 처음 조회된 프록시를 반환함
⇒ 즉, 프록시로 조회할 때도 영속성 컨텍스트는 영속 엔티티의 동일성을 보장함
예) 원본 엔티티 조회 → 프록시 조회
@Test
public void 영속성컨텍스트와_프록시2() {
Member newMember = new Member("member1", "회원1");
em.persist(newMember);
em.flush();
em.clear();
Member findMember = em.find(Member.class, "member1");
Member refMember = em.getReference(Member.class, "member1");
Assert.assertTrue(refMember == findMember); //성공
}
getReference()를 호출해도 프록시가 아닌 원본을 반환함. ⇒ 즉, 이런 경우에도 영속성 컨텍스트는 자신이 관리하는 영속 엔티티의 동일성을 보장함프록시는 원본 엔티티를 상속 받아 만들어지기에 프록시로 조회한 엔티티 타입의 비교에는 == 비교가 아닌 instanceof를 사용해야함
@Test
public void 영속성컨텍스트와_프록시2() {
Member newMember = new Member("member1", "회원1");
em.persist(newMember);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, "member1");
Assert.assertFalse(refMember == findMember); //false
Assert.assertTrue(refMember instanceof Member); //true
}
== 비교: 부모 클래스와 자식 클래스를 비교하는 것. 즉, 결과가 false.동등성 비교를 위해서는 비즈니스 키를 사용해 equals() 메소드를 오버라이딩하여 비교.
IDE/외부 라이브러리를 사용해 구현한 equals() 메소드를 사용해 엔티티 비교 시, 비교 대상이 원본 엔티티이면 괜찮지만, 프록시인 경우에는 문제 발생 가능
@Entity
public class Member {
@Id
private String id;
private String name;
...
public String getName() {return name;}
public void setName(String name) {this.name = name;}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (this.getClass() != obj.getClass()) return false;
Member member = (Member) obj;
if (name != null ? !name.equals(member.name) : member.name != null) return false;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
}
name 필드를 비즈니스 키로 사용해 equals() 메소드를 오버라이딩.newMember와 프록시로 조회한 회원 refMember의 name 속성은 같지만, 동등성 비교 시 실패(false 반환).문제1:
this.getClass() != obj.getClass()의 부분은 타입을 동등성 비교함 → 즉, 프록시는 원본을 상속받은 자식 타입이기에 프록시의 타입을 비교할 때는 instanceof를 사용해야함.
문제2:
Member member = (Member) obj;
if(name != null ? !name.equals(member.name) : member.name != null) return false;
member.name을 보면 프록시의 멤버변수에 직접 접근함equals() 메소드를 구현할 때는 일반적으로 멤버변수를 직접 비교하는데 프록시의 경우에는 문제가 생김 ⇒ 왜냐하면 프록시는 실제 데이터를 가지고 있지 않음.equals()는 false 반환함해결: 프록시의 데이터 조회를 위해서는 접근자(getter)를 사용해야함.
instanceof를 사용해야함프록시를 부모타입으로 조회할 시 문제가 발생할 수 있음.
예) Item(부모 클래스) - Book(하위 클래스). Item을 조회하여 Book 타입인 경우 저자 이름을 출력하려할 때.
em.getReference()를 통해 Item 엔티티를 프록시로 조회 → instanceof 연산을 통해 Book 클래스 타입인지를 확인 → Book 타입인 경우 다운캐스팅하여 Book 타입으로 변경 후 저자이름 출력.
⇒ 다만 결과로 저자는 출력되지 않음.
instanceof의 비교가 false를 반환하게됨.프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성되게 됨
이런 문제는 주로 다형성을 다루는 도메인 모델에서 발생함.
간단한 방법으로, 처음부터 자식 타입을 직접 조회하여 필요한 연산을 수행. 단점으로는 다형성을 활용할 수 없다는 한계 존재.
Book jpqlBook = em.createQuery("select b from Book b where b.id = :bookId",
Bookclass)
.setParameter("bookId", item.getId())
.getSingleResult();
하이버네이트에서 제공하는 기능을 사용해 프록시에서 원본 엔티티를 가져올 수 있음
...
Item item = orderItem.getItem();
**Item unProxyItem = unProxy(item);**
**if (unProxyItem instanceof Book)** {
System.out.println("proxyItem instanceof Book");
Book book = (Book) unProxyItem;
System.out.println("책 저자 = " + book.getAuthor());
}
Assert.assertTrue(item != unProxyItem);
}
//하이버네이트가 제공하는 프록시에서 원본 엔티티를 찾는 기능을 사용하는 메소드
**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.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
**public abstract class Item implements TitleView** {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
...
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
@Override
public String getTitle() {
return "[제목: " + getName() + " 저자:" + author + "]";
}
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
@Override
public String getTitle() {
return "[제목: " + getName() + " 감독:" + director + " 배우:" + actor + "]";
}
}
getTitle() 메소드를 각각 구현.@Entity
public class OrderItem {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ITEM_ID")
private Item item;
...
}
OrderItem orderItem = em.find(OrderItem.class, saveOrderItem.getId());
orderItem.printItem();

accept(visitor) 메소드를 사용해 Visitor를 받아들임. Item은 단순히 Visitor를 받아들이기만하고 실제 로직은 Visitor가 처리.1-Visitor 정의와 구현
public interface Visitor {
void visit(Book book);
void visit(Album album);
void visit(Movie movie);
}
visit() 메소드를 정의하고 모든 대상 클래스를 받아들이도록 작성2- 대상 클래스 작성
Item에 Visitor를 받아들일 수 있도록 accept(visitor) 메소드 추가
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
**public abstract void accept(Visitor visitor);**
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
...
**@Override
public void accept(Visitor visitor) {
visitor.visit(this)
}**
}
accept(visitor) 메소드를 구현했는데, 구현 내용은 단순히 파라미터로 넘어온 Visitor의 visit(this) 메소드를 호출해 자신을 파라미터로 넘김3-비지터 패턴 실행
@Test
public void 상속관계와_프록시_VisitorPattern() {
OrderItem orderItem = em.find(OrderItem.class, orderItemId);
Item item = orderItem.getItem();
//PrintVisitor
**item.accept(new PrintVisitor());**
}
item.accept() 메소드를 호출하여 파라미터로 PrintVisitor를 넘겨줌.accept() 메소드를 받고 원본 엔티티의 accept()를 실행함.⇒ 비지터 패턴을 사용하면 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근할 수 있고 instanceof나 타입 캐스팅 없이 코드를 구현할 수 있음
새로운 기능이 필요할 때 Visitor만 추가하면 되기에 기존 코드의 구조를 변경하지 않고 기능을 추가할 수 있는 장점.
장점:
단점:
N+1 문제: JPA 애플리케이션 개발 시 성능 상 가장 주의해야하는 문제
예)
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
private List<Order> orders = new ArrayList<Order>();
}
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne
private Member member;
}
회원과 주문정보 → 1:N, N:1 양방향 관계 + 회원이 참조하는 주문정보인 Member.orders를 즉시로딩으로 설정.
예시에서 특정 회원 하나를 em.find()로 조회하면 즉시로딩으로 설정한 주문정보들도 함께 조회하게됨.
이때 SQL을 두 번 실행하지 않고, 조인을 통해 하나의 SQL로 회원+주문정보를 모두 조회.
SELECT M.*, O.*
FROM MEMBER M OUTER JOIN
ORDERS O ON M.ID=O.MEMBER_ID
문제점: JPQL을 사용할 때 발생.
N+1 문제: 처음 실행한 SQL의 결과 수만큼 추가로 SQL을 실행하는 문제
List<Member> members =
em.createQuery("select m from Member m", Member.class)
.getResultList();for (Member member : members) {
//지연로딩 초기화
System.out.println("member = "+ member.getOrders().size());
} ⇒ 주문 컬렉션을 초기화하는 수만큼 SQL이 실행되며 N+1 문제 발생N+1 문제를 해결하는 가장 일반적인 방법. 페치 조인은 SQL 조인을 사용해 연관 엔티티를 함께 조회하기에 N+1 문제가 발생하지 않음.
예)
select m from Member m join fetch m.orders
SELECT M.*, O.*
FROM MEMBER M
INNER JOIN ORDERS O ON M.ID = O.MEMBER_ID
하이버네이트에서 제공하는 @BatchSize 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정한 size만큼 SQL의 IN절을 사용해 조회함
@Entity
public class Member {
...
**@BatchSize(size = 5)**
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
private List<Order> orders = new ArrayList<Order>();
}
즉시로딩 설정: 10건의 데이터 모두 조회가 필요하면 SQL이 2번 실행됨
지연로딩 설정: 엔티티 최초 사용 시점에 SQL 1번 실행해 5건은 미리 로딩 → 이후 6번째 데이터 필요 시 추가 SQL 시행
SELECT O
FROM ORDERS O
WEHRE MEMBER_ID IN (?,?,?,?,?)
하이버네이트에서 제공하는 @Fetch 어노테이션에 FetchMode를 SUBSELECT로 사용 → 연관된 데이터 조회 시 서브 쿼리를 사용해 N+1 문제를 해결
예)
@Entity
public class Member {
@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
private List<Order> orders = new ArrayList<Order>();
}
즉시로딩으로 설정 시 조회 시점에, 지연로딩으로 설정 시 엔티티를 사용하는 시점에 아래 SQL이 실행됨
SELECT O
FROM ORDERS O
WEHRE O.MEMBER_ID IN (SELECT M.ID
FROM MEMBER M
WHERE M.ID > 10)
추천하는 방법: 즉시로딩은 사용하지 않고 지연 로딩 + 페치 조인만 사용하는 것.
@OneToOne, @ManyToOne: 기본 페치 전략은 즉시 로딩@OneToMany, @ManyToMany: 기본 페치 전략은 지연 로딩엔티티가 영속성 컨텍스트에서 관리 → 1차 캐시, 변경 감지 등 이점이 많지만 영속성 컨텍스트는 스냅샷 인스턴스를 보관하기에 더 많은 메모리를 사용한다는 단점이 존재함
예) 100건의 구매 내용을 출력하는 조회 화면
가장 확실한 방법으로 엔티티가 아닌 스칼라 타입으로 모든 필드를 조회하는 것. 스칼라 타입의 경우 영속성 컨텍스트가 결과를 관리하지 않음
select o.id, o.name, o.price
from Order p
하이버네이트 전용 힌트인 readOnly를 사용해 읽기 전용으로 조회 가능. 읽기 전용이기에 영속성 컨텍스트는 스냅샷을 보관하지 않음 → 즉, 메모리 사용량 최적화 가능.
(단, 스냅샷이 없기에 엔티티를 수정해도 데이터베이스에 반영 X)
TypedQuery<Order> query = em.createQuery("select o from Order o", Order.class);
**query.setHint("readOnly", true);**
스프링 프레임워크 사용 시 트랜잭션을 읽기 전용으로 설정할 수 있음
@Transactional(readOnly = true)
위처럼 설정하면 스프링 프레임워크가 하이버네이트 세션의 플러시 모드를 MANUAL로 설정함.
트랜잭션의 밖 = 트랜잭션 없이 엔티티를 조회. JPA에서 데이터 변경 시 트랜잭션은 필수이기에 조회가 목적일 때만 사용해야함.
@Transactional(propagation = Propagation.NOT_SUPPORTED)
트랜잭션을 사용하지 않으면 플러시가 일어나지 않기에 조회 성능이 향상됨.
수백만건의 데이터를 처리해야할 때 → 일반적인 방식은 영속성 컨텍스트에 많은 엔티티가 쌓여 메모리 부족 오류가 발생하기에 배치 처리는 적절한 단위로 영속성 컨텍스트를 초기화해야함. 또한, 2차 캐시를 사용하지 않고 있다면, 2차 캐시에 엔티티를 보관하지 않도록 주의 필요.
여러 엔티티를 한 번에 등록할 때의 주의점 = 영속성 컨텍스트에 엔티티가 계속 쌓이지 않도록 일정 단위마다 영속성 컨텍스트의 엔티티를 데이터베이스에 플러시 → 영속성 컨텍스트 초기화할 필요 존재
위의 작업을 하지 않으면 영속성 컨텍스트에 너무 많은 엔티티가 저장되며 메모리 부족 오류가 발생할 수 있음
EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
for (int i=0; i<100000; i++) {
Product product = new Product("item" + i, 10000);
em.persist(product);
//100건마다 플러시와 영속성 컨텍스트 초기화
if (i % 100 == 0) {
em.flush();
em.clear();
}
}
tx.commit();
em.close();
EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
int pageSize = 100;
for (int i=0; i<10; i++) {
**List<Product> resultList = em.createQuery("select p from Product p", Product.class)
.setFirstResult(i * pageSize)
.setMaxResults(pageSize)
.getResultList();**
for (Product p : resultList) {
p.setPrice(p.getPrice() + 100);
}
em.flush();
em.clear();
}
tx.commit();
em.close();
JPA는 JDBC 커서를 지원하지 않기에, 커서 사용을 위해서는 하이버네이트 세션을 사용해야함. 하이버네이트는 scroll이라는 이름으로 JDBC 커서를 지원함
EntityTranscation 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(); //영속성 컨텍스트 초기화
}
}
em.unwrap() 메소드를 통해 하이버네이트 세션을 구함scroll() 메소드로 ScrollableResults 객체를 반환받음. 해당 객체의 next() 메소드를 호출하여 엔티티를 하나씩 조회할 수 있음.무상태 세션: 영속성 컨텍스트를 만들지 않고, 2차 캐시를 사용하지 않음. 즉, 엔티티 수정을 위해서는 무상태 세션이 제공하는 update() 메소드를 직접 호출해야함
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
StatelessSession session = sessionFactory.openStatelessSession();
Transaction tx = session.beginTransaction();
ScrollableResults scroll = session.createQuery("select p from Product p).scroll();
while(scroll.next()) {
Product p = (Product) scroll.get(0);
p.setPrice(p.getPrice() + 100);
**session.update(p);** //직접 호출 필요
}
tx.commit();
session.close();
하이버네이트 무상태 세션은 일반 하이버네이트 세션과 거의 비슷하지만 영속성 컨텍스트가 없음. 즉, 영속성 컨텍스트를 플러시나 초기화할 필요가 없고 대신 엔티티 수정 시 update() 메소드를 직접 호출해야함.
JPA는 데이터베이스 SQL 힌트 기능을 제공하지 않기에, SQL 힌트 사용을 위해서는 하이버네이트를 직접 사용해야함.
SQL 힌트는 하이버네이트 쿼리가 제공하는 addQueryHint() 메소드를 사용함.
Session session = em.unwrap(Session.class); //하이버네이트 직접 사용
List<Member> list = session.createQuery("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 힌트 사용을 위해서는 각 방언에서 Dialect에 있는 아래의 메소드를 오버라이딩하여 구현해야함
public String getQueryHintStirng(String query, List<String> hints) {
return query;
}
예)
insert(member1);
insert(member2);
insert(member3);
insert(member4);
insert(member5);
commit();
네트워크 호출 한 번은 단순 메소드 수만 번 호출보다 더 큰 비용이 듬. 위의 코드는 5번의 INSERT SQL과 1번의 커밋을 통해 6번 데이터베이스와 통신.
→ 최적화를 위해서 5번의 INSERT SQL을 모아서 한 번에 데이터베이스로 보내면됨.
트랜잭션을 지원하는 쓰기 지연과 변경 감지 기능 → 성능과 개발의 편의성을 줌과 동시에 데이터베이스 테이블 로우(row)에 락(lock)이 걸리는 시간 최소화한다는 장점 존재
예)
update(memberA); //UPDATE SQL A
비즈니스로직A(); //UPDATE SQL
비즈니스로직B(); //INSERT SQL
commit();
update(memberA)를 호출할 때 UPDATE SQL을 실행하며 데이터베이스 테이블 로우에 락을 검.