# 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` 예외를 스프링 프레임워크가 추상화한 예외로 변환해준다.

```java
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
```

```java
@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
	return new PersistenceExceptionTranslationPostProcessor();
}
```

만약 예외를 변환하지 않고 그대로 반환하고 싶으면 다음처럼 `throws` 절에 그대로 반환할 `JPA` 예외나 `JPA` 예외의 부모 클래스를 직접 명시 하면 된다.

```java
@Repository
public class NoResultExceptionTestService {
	@PersistenceContext EntityManager em;

	public Member findMember() throws javax.persistence.NoResultException {
		return em.creteQuery("select m from Member m", Member.class).getSingleResult();
	}
}
```

### 트랜잭션 롤백 시 주의사항

`트랜잭션`을 `롤백`하는 것은 데이터베이스의 반영 사항만 롤백 하는 것이지 수정한 `자바 객체`까지 원 상태로 복구해주지는 않는다. **예를 들어 엔티티를 조회해서 수정하는 중에 문제가 있어서 트랜잭션을 롤백하면 데이터베이스의 데이터는 원래대로 복구되지만 객체는 수정된 상태로 영속성 컨텍스트에 남아 있다.** 따라서 새로운 `영속성 컨텍스트`를 생성해서 사용하거나 `EntityManager.clear()`를 호출해서 `영속성 컨텍스트`를 초기화한 다음에 사용해야 한다.

**기본 전략인 트랜잭션당 영속성 컨텍스트 전략은 문제가 발생하면 트랜잭션 `AOP` 종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함께 종료하므로 문제가 발생하지 않는다.**

문제는 `OSIV`처럼 영속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 사용해서 여러 트랜잭션이 하나의 영속성 컨텍스트를 사용할 때 발생한다. 스프링 프레임워크는 영속성 컨텍스트의 범위를 트랜잭션의 범위보다 넓게 설정하면 트래잭션 롤백시 영속성 컨텍스트를 초기화해서 잘못된 영속성 컨텍스트를 사용하는 문제를 예방한다.

## 엔티티 비교

영속성 컨텍스트를 통해 데이터를 저장하거나 조회하면 `1차 캐시`에 엔티티가 저장된다. 이 1차 캐시 덕분에 `변경 감지` 기능도 동작하고, 이름 그대로 `1차 캐시`로 사용 되어서 데이터베이스를 통하지 않고 데이터를 바로 `조회`할 수도 있다.

```java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:appConfig.xml")
@Transactional
public class MemberServiceTest {

	@Test
	public void 회원가입() throws Exception {
		// Given
		Member member = new Member("kim");

		// When
		Long saveId = memberService.join(member);

		// Then
		Member findMember = memberRepository.findOne(saveId);
		assertTrue(member == findMember); // true 참조값 비교
	}
}
```

이것은 같은 트랜잭션 범위에 있으므로 같은 영속성 컨텍스트를 사용하기 때문이다. 따라서 영속성 컨텍스트가 같으면 엔티티를 비교할 때 다음 3가지 조건을 모두 만족한다.

* 동일성: `==` 비교가 같다
* 동등성 : `equals()` 비교가 같다
* 데이터베이스 동등성 : `@Id`인 데이터베이스 식별자가 같다

![](https://2649832514-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M5HOStxvx-Jr0fqZhyW%2F-MAK5k8zeTSu7qf8xf5z%2F-MAKAYE0YwURQjdOHOry%2F1.png?alt=media\&token=f35b5624-96c6-4f66-acf3-88cfab7fabe6)

정리하자면 `동일성 비교`는 같은 영속성 컨텍스트의 관리를 받는 영속 상태의 엔티티에만 적용할 수 있다. 그렇지 않을 때는 `비즈니스 키`를 사용한 `동등성 비교`를 해야 한다.

## 프록시 심화 주제

`프록시`는 원본 엔티티를 상속받아서 만들어지므로 `엔티티`를 사용하는 클라이언트는 엔티티가 프록시인지 아니면 원본 엔티티인지 구분하지 않고 사용할 수 있다. 이로 인해 예상하지 못한 문제들이 발생하기도 하는데, 어떤 문제가 발생하고 어떻게 해결해야 하는지 알아보자

### 영속성 컨텍스트와 프록시

`엔티티`의 동등성을 비교하려면 비즈니스 키를 사용해서 `equals()` 메소드를 오버라이딩하고 비교하면 된다. 그런데 IDE나 외부 라이브러리를 사용해서 구현한 equals() 메소드로 엔티티를 비교할 때, 비교 대상이 원본 엔티티면 문제가 없지만 `프록시`면 문제가 발생할 수 있다.

```java
@Entity
public class Member {
	@Id
	private String id;
	private String name;

	...

	@Override
	public boolean equals(Object obj) {
		if (this == obj) return true;
		if (obj == null) return false;
		if (this.getClass() != obj.getClass()) return false; // 1

		Member member = (Member) obj;

		if (this.name != null ? !this.name.equals(member.name) : member.name != null) //
			return false;

		return true;
	}
}
```

테스트 코드를 작성해보자

```java
@Test
public void 프록시와_동등성비교() {
        Member saveMember = new Member("member1", "회원1");
        em.persist(saveMember);
        em.flush();
        em.clear();
        
        Member newMember = new Member("member1", "회원1");
        Member refMember = em.getReference(Member.class, "member1");
        
        Assert.assertTrue(newMember.equals(refMember)); // false
}
```

#### 왜 이런 문제가 발생할까?

* 프록시는 원본을 상속받은 `자식 타입`이므로 프록시의 타입을 비교할 때는 `==` 비교가 아닌 `instanceof`를 사용해야 한다,. 따라서 다음처럼 변경해야 한다.

  ```java
  if (!(obj instanceof Member)) return false;
  ```
* `memner.name`을 보면 프록시의 멤버변수에 직접 접근하는데 이 부분을 주의깊게 봐야 한다. 프록시는 실제 데이터를 가지고 있지 않기 때문에 프록시의 `멤버 변수`에 직접 접근하면 아무값도 조회할 수 없다. 그러므로 프록시의 데이터를 조회할 때는 `접근자(Getter)`를 사용해야 한다.

### 상속관계와 프록시

`상속 관계`를 프록시로 조회할 때 발생할 수 있는 문제점과 해결방안을 알아보자

![](https://2649832514-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M5HOStxvx-Jr0fqZhyW%2F-MAK5k8zeTSu7qf8xf5z%2F-MAKAeIw9Gwb3DXYv31K%2F2.png?alt=media\&token=a1b350ac-e252-42fa-8c63-041a09efc288)

위와 같은 구조의 클래스 모델을 생성할 경우 프록시를 부모 타입으로 조회하면 문제가 발생한다.

```java
@Test
public void 부모타입으로_프록시조회() {
	//테스트 데이터 준비
	Book saveBook = new Book();
	saveBook.setName("jpaBook");
	saveBook.setAuthor("kim");
	em.persist(saveBook);

	em.flush();
	em.clear();

	//테스트 시작
	Item proxyItem = em.getReference(Item.class, saveBook.getId());
	System.out.println("proxyItem = " + proxyItem.getClass());

	if (proxyItem instanceof Book) {
		System.out.println("proxyItem instanceof Book");
		Book book = (Book) proxyItem;
		System.out.println("책 저자 = " + book.getAuthor());
	}

	//결과 검증
	Assert.assertFalse(proxyItem.getClass() == Book.class);
	Assert.assertFalse(proxyItem instanceof Book);
	Assert.assertTrue(proxyItem instanceof Item);
}
```

그런데 출력 결과를 보면 기대와는 다르게 저자가 출력되지 않은 것을 알 수 있다.

![](https://2649832514-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M5HOStxvx-Jr0fqZhyW%2F-MAK5k8zeTSu7qf8xf5z%2F-MAKAfuCbYCpkBkThqP6%2F3.png?alt=media\&token=0e6944c3-96a1-4360-af9d-f4685819c5f5)

#### 왜 원하는 출력값이 다를까?

실제 조회된 엔티티는 `Book`이므로 `Book` 타입을 기반으로 원본 엔티티 인스턴스가 생성된다. 그런데 `em.getReference()` 메소드에서 `Item` 엔티티를 대상으로 조회 했으므로 프록시인 `proxyItem`은 `Item` 타입을 기반으로 만들어진다. 이런 이유로 다음 연산이 기대와 다르게 `false`를 반환한다. 왜냐하면 `proxyItem`은 `Item&Proxy` 타입이고 이 타입은 `Book` 타입과 관계가 없기 때문이다.

```java
proxyItem instanceof Book // false
```

따라서 직접 다운캐스팅을 해도 문제가 발생한다.

```java
Book book = (Book) proxyItem; // java.lang.ClassCastException
```

#### 그렇다면 상속관계에서 발생하는 프록시 문제를 어떻게 해결해야 할까?

* JPQL로 대상 직접 조회

  ```java
  Book jpqlBook = em.createQuery
  	("select b from Book b where b.id=:bookId", Book.class)
  	.setParameter("bookId", item.getId())
  	.getSingleResult();
  ```
* 프록시 벗기기

  ```java
  	...
  	Item item = orderItem.getItem();
  	Item unProxyItem = unProxy(item);
  	
  	if (unProxyItem instanceof Book) {
  		System.out.println("proxyItem instanceod 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;
  }
  ```

  그런데 이 방법은 프록시에서 원본 엔티티를 직접 꺼내기 때문에 프록시와 원본 엔티티의 `동일성 비교`가 실패한다는 문제점이 있다.

  ```java
  item == unProxyItem // false
  ```
* 기능을 위한 별도의 인터페이스 제공

  ```java
  public interface TitleView {
  	String getTitle();
  }

  @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
  @DiscriminatorColumn(name = "DTYPE")
  public abstract class Item implements TitleView {
  	@Id @GeneratedValue
  	@Column(name = "ITEM_ID")
  	private Long id;
  	
  	...
  }
  ```
* 비지터 패턴

  ```java
  public interface Visitor {
  	void visit(Book book);
  	void visit(Album album);
  	void visit(Movie movie);
  }
  ```

  ```java
  public class PrintVisitor implements Visitor {
  	@Override
  	public void visit(Book book) {
  		//넘어오는 book은 Proxy가 아닌 원본 엔티티다. 
  		System.out.println("book.class = " + book.getClass());
  	}
  	
  	@Override
  	void visit(Album album) {...}
  	@Override
  	void visit(Movie movie) {...}
  ```

  ```java
  @Entity
  @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
  @DiscriminatorColumn(name = "DTYPE")
  public abstract class Item {

  	@Id @GeneratedValue
  	@Column(name = "ITEM_ID")
  	private Long id;

  	...

  	public abstract void accept(Visitor visitor);
  }

  @Entity
  @DiscriminatorValue("B")
  public class Book extends Item {

  	...
  	@Override
  	public void accept(Visitor visitor){
  		visitor.visit(this);
  	}
  }
  ```

  ```java
  @Test
  public void 상속관계와_프록시_VisitorPattern() {
  	...
  	OrderItem orderItem = em.find(OrderItem.class, orderItemId);
  	Item item = orderItem.getItem();

  	//PrintVisitor
  	item.accept(new PrintVisitor());
  }
  ```

  ```java
  public class PrintVisitor implements Visitor {
  	public void visit(Book book) {
  		//넘어오는 book은 Proxy가 아닌 원본 엔티티다. 
  		System.out.println("book.class = " + book.getClass());

  	}
  	public void visit(Album album) {...}
  	public void visit(Movie movie) {...}
  }
  ```

  **비지터 패턴의 장점은 다음과 같다.**

  * 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근할 수 있다.
  * `instanceof`와 타입 캐스팅 없이 코드를 구현할 수 있다.
  * 알고리즘과 객체 주고를 분리해서 구조를 수정하지 않고 `새로운 동작`을 추가할 수 있다.

  **단점은 다음과 같다.**

  * 너무 복잡하고 `더블 디스패치`를 사용하기 때문에 이해하기 어렵다.
  * 객체 구조가 변경되면 모든 Visitor를 `수정`해야 한다.

## 성능 최적화

JPA로 애플리케이션을 개발할 때 발생하는 다양한 성능 문제와 해결 방안을 알아보자.

### N+1 문제

```java
@Entity
public class Member {

	@Id @GeneratedValue
	private Long id;

	@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
	private List<Order> orders = new ArrayList<Order>();

	...
}
```

```java
@Entity
public class Order {
	@Id @GeneratedValue
	private Long id;

	@ManyToOne
	private Member member;
	...
}
```

#### 즉시 로딩과 N+1

`em.find()` 메소드로 조회하면 즉시 로딩으로 설정한 주문정보도 함께 조회한다.

```java
em.find(Member.class, id);

// 결과값
SELECT M.*, O.*
FROM
	MEMBER M
OUTER JOIN ORDERS O ON M.ID = O.MEMNBER_ID
```

문제는 `JPQL`을 사용할 때 발생한다.

```java
List<Member> members = 
	em.createQuery("select m from Member m", Member.class)
	.getResultList();

// 결과 SQL 로그
SELECT * FROM MEMNER
SELECT * ORDERS WHERE MEMBER_ID =1 // 회원과 연관된 주문
SELECT * ORDERS WHERE MEMBER_ID =2 // 회원과 연관된 주문
SELECT * ORDERS WHERE MEMBER_ID =3 // 회원과 연관된 주문
SELECT * ORDERS WHERE MEMBER_ID =4 // 회원과 연관된 주문
SELECT * ORDERS WHERE MEMBER_ID =5 // 회원과 연관된 주문
SELECT * ORDERS WHERE MEMBER_ID =6 // 회원과 연관된 주문
```

#### 지연 로딩과 N+1

```java
for (Member member : members) {
	//지연 로딩 초기화
	System.out.println("member = " + member.getOrders().size();
}

// 결과 SQL 로그
SELECT * ORDERS WHERE MEMBER_ID =1 // 회원과 연관된 주문
SELECT * ORDERS WHERE MEMBER_ID =2 // 회원과 연관된 주문
SELECT * ORDERS WHERE MEMBER_ID =3 // 회원과 연관된 주문
SELECT * ORDERS WHERE MEMBER_ID =4 // 회원과 연관된 주문
SELECT * ORDERS WHERE MEMBER_ID =5 // 회원과 연관된 주문
```

### N+1 문제 해결 방법

#### 페치 조인 사용

페치 조인은 SQL 조인을 사용해서 연관된 엔티티를 함께 조회하므로 `N+1` 문제가 발생하지 않는다.

```java
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
```

> 참고로 이 예제는 일대다 조인을 했으므로 결과가 늘어나서 중복된 결과가 나타날 수 있다. 따라서 `JPQL`의 `DISTINCT`를 사용해서 중복을 제거하는 것이 좋다.

#### 하이버네이트 @BatchSize

`BatchSize` 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정한 size만큼 SQL의 IN절을 사용해서 조회한다. 만약 조회한 회원이 10명인데 `size=5`로 지정하면 2번의 `SQL`만 추가로 실행한다.

```java
@Entity
public class Member {

	@Id @GeneratedValue
	private Long id;

	@BatchSize(size = 5)
	@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
	private List<Order> orders = new ArrayList<Order>();

	...
}
```

#### 하이버네이트 @Fetch(FetchMode.SUBSELECT)

`FetchMode`를 `SUBSELECT`로 사용하면 연관된 데이터를 조회할 때 서브 쿼리를 사용해서 `N+1` 문제를 해결할 수 있다.

```java
@Entity
public class Member {

	@Id @GeneratedValue
	private Long id;

	@Fetch(FetchMode.SUBSELECT)
	@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
	private List<Order> orders = new ArrayList<Order>();

	...
}
```

```java
select m from Member m where m.id > 10

// 결과 SQL 로그
SELECT O FROM ORDERS O
	WHERE O.MEMBER_ID IN (
		SELECT 
			M.ID
		FROM
			MEMBER M
		WHERE M.ID > 10
	}
```

### 읽기 전용 쿼리의 성능 최적화

엔티티가 영속성 컨텍스트에 관리되면 `1차 캐시`부터 `변경 감지` 까지 얻을 수 있는 혜택이 많다. 하지만 영속성 컨텍스트는 `변경 감지`를 위해 `스냅샷` 인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있다. 이때는 읽기 전용으로 엔티티를 조회하면 `메모리 사용량`을 `최적화`할 수 있다.

```java
@Transactional(readOnly = true)
```

위와 같이 `읽기 전용`으로 설정하면 `트랜잭션`을 `커밋`해도 영속성 컨텍스트를 `플러시`하지 않는다. 그러므로 플러시할 때 일어나는 `스냅샷` 비교와 같은 무거운 로직들을 수행하지 않으므로 성능이 향상된다.

> 엔티티 매니저의 플러시 설정에는 `AUTO`, `COMMIT` 모드만 있고, `MANUAL` 모드가 없다. 반면에 하이버네이트 세션의 플러시 설정에는 `MANUAL` 모드가 있다. `MANUAL` 모드는 강제로 플러시를 호풀하지 않으면 절대 플러시가 발생하지 않는다.

### 배치 처리

수백만 건의 데이터를 `배치 처리`해야 하는 상황이라 가정해보자. 일반적인 방식으로 엔티티를 계속 조회하면 `영속성 컨텍스트`에 아주 많은 엔티티가 쌓이면서 `메모리 부족 오류`가 발생한다. 따라서 이런 배치 처리는 적절한 단위로 영속성 컨텍스트를 초기화해야 한다. 배치 처리는 아주 많은 데이터를 조회해서 수정한다. 이때 수많은 데이터를 한번에 메모리에 올려둘 수 없어서 2가지 방법을 주로 사용한다.

* 페이징 처리
* 커서

### JPA 페이징 배치 처리

```java
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 product : resultList) {
		product.setPrice(product.getPrice() + 100);
	}

	em.flush();
	em.clear();
}

tx.commit();
em.close();
```

이는 한번에 100건씩 페이징 쿼리로 조회하면서 상품의 가격을 100원씩 증가한다. 그리고 페이지 단위마다 영속성 컨텍스트를 플러시하고 초기화한다.

### 하이버네이트 scroll 사용

하이버네이트는 scroll이라는 이름으로 JDBC 커서를 지원한다.

```java
EntityTransaction tx = em.getTransaction();
Session session = em.unwrap(Session.class);

tx.begin();
ScrollableResults scroll = session.createQuery("select p from Product p")
					.setCacheMode.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();

```

scroll은 하이버네이트 전용 기능이므로 먼저 em.unwrap() 메소드를 사용해서 하이버네이트 세션을 구한다. 다음으로 쿼리를 조회하면서 scroll() 메소드로 ScrollableResults 객체를 반환받는다. 이 객체의 next() 메소드를 호출하면 엔티티를 하나씩 조회할 수 있다.

### SQL 쿼리 힌트 사용

SQL 힌트를 사용하려면 하이버네이트를 직접 사용해야 한다. SQL 힌트는 하이버네이트 쿼리가 제공하는 addQueryHint() 메소드를 사용한다.

```java
Session session = em.unwrap(Session.class); // 하이버네이트 직접 사용
List<Member> list = session.createQuery("select m from Member m")
				.addQueryHint("FULL (MEMBER)")
				.list();

// 실행 결과
select 
	/*+ FULL (MEMBER) */ m.id, m.name
from
	Member m
```

### 정리

* JPA의 예외는 트랜잭션 롤백을 표시하는 예외와 표시하지 않는 예외로 나눈다. 트랜잭션을 롤백하는 예외는 심각한 예외이므로 트랜잭션을 강제로 커밋해도 커밋되지 않고 롤백된다.
* 스프링 프레임워크는 JPA의 예외를 스프링 프레임워크가 추상화한 예외로 변환해준다.
* 프록시를 사용하는 클라이언트는 조회한 엔티티가 프록시인지 아니면 원본 엔티티인지 구분하지 않고 사용할 수 있어야 한다. 하지만 프록시는 기술적인 한계가 있으므로 한계점을 인식하고 사용해야 한다.
* JPA를 사용할 때는 N+1 문제를 가장 조심해야 한다. N+1 문제는 주로 페치 조인을 사용해서 해결한다.
* 엔티티를 읽기 전용으로 조회하면 스냅샷을 유지할 필요가 없고 영속성 컨텍스트도 초기화해야 한다.
* JPA는 SQL 쿼리 힌트를 지원하지 않지만 하이버네이트 구현체를 사용하면 SQL 쿼리 힌트를 사용할 수 있다.
* 트랜잭션을 지원하는 쓰기 지연 덕분에 SQL 배치 기능을 사용할 수 있다.

### 질문

예외에 따라 롤백기능이 다름

* 체크드 → 롤백 안됨
* 언 체크드 → 롤백됨

비관적 락 해결 방법?
