JPA 표준 예외는 모두 javax.persistence.PersistenceException의 자식 클래스다. 이 예외 클래스는 RuntimeException의 자식이므로 JPA 예외는 모두 언체크 예외다.
여기서 잠깐!
언체크 예외가 뭔데??
언체크 예외(Unchecked Exception)는 컴파일러가 예외 처리를 강제하지 않는 예외입니다. 즉, 코드 작성 시 예외 처리를 위한 try-catch 블록을 반드시 작성하지 않아도 컴파일 오류가 발생하지 않습니다.
JPA 표준 예외는 크게 심각한 예외와 그렇지 않은 예외 두 가지로 나뉜다.
트랜잭션 롤백을 표시하는 예외는 트랜잭션을 강제적으로 커밋해도 커밋되지 않고 javax.persistence.RollbackException 예외가 발생한다.
트랜잭션 롤백을 표시하지 않는 예외는 심각한 예외라가 아니라서 개발자가 트랜잭션을 커밋할지 롤백할지 판단하면 된다.

persist()는 이미 영속성 컨텍스트에 존재하는 동일한 식별자를 가진 엔티티가 있으면 중복 등록 문제로 예외가 발생한다.

서비스 계층에서 데이터 접근 계층의 구현 기술에 직접 의존하면 좋은 설계라 할 수 없는 것 처럼 예외도 마찬가지이다.
서비스 계층에서 JPA 예외를 직접 사용하면 JPA예외에 의존하게 되므로 스프링 프레임워크는 데이터 접근 계층에 대한 예외를 추상화해서 개발자에게 제공한다.


JPA 예외를 스프링 프레임워크에서 제공하는 추상화된 예외로 변경하려면 PersistenceExceptionTransactionPostProcessor를 스프링 빈으로 등록하면 된다. 이것은 @Repository를 사용한 곳에 예외 변환 AOP를 적용해서 JPA 예외를 스프링 프레임워크가 추상화환 예외로 변환해준다.
<beans xmlns="http://www.springframework.org/schema/beans" 
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
</beans>
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
@Configuration
public class AppConfig {
    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
        return new PersistenceExceptionTranslationPostProcessor();
    }
}
@Repository
public class NoResultExceptionTestService {
	
    @PersistenceContext EntityManager em;
    
    public member findMember() throws javax.persistence.NoResultException {
    	//조회된 데이터 없음
    	return em.createQuery("select m from Member m", Member.class)
        		.getSingleResult();
}              
getSingleResult() 메소드를 사용했는데 조회된 결과가 없다면 javax.persistence.NoResultException이 발생되는데 . 이때 PersistenceExceptionTranslationPostProcessor에서 등록한 AOP가 동작하며 org.springframework.dao.EmptyResultDataAccessException 예외로 변환해서 반환한다.
그러나 변환된 예외가 아닌 그대로의 예외를 사용하고 싶다면 위처럼 throws를 사용해서 JPA 예외의 부모 클래스를 명시적으로 적으면 된다. java.lang.Exception를 선언하면 모든 예외의 부모이므로 예외를 변환하지 않는다.
트랜잭션 롤백시 데이터베이스의 반영사항만 롤백한 것이지 수정한 자바 객체까지 원상태로 복구한 것이 아님을 주의해야 한다. 트랜잭션이 롤백된 영속성 컨텍스트를 그대로 사용하는 것은 위험하므로 새로운 영속성 컨텍스트를 생성해서 사용하거나 EntityManager.clear()를 호출해서 영속성 컨텍스트를 초기화해서 사용해야 한다.
스프링 프레임워크에서는 영속성 컨텍스트 범위에 따라서 해결방법이 달라진다.
기본 전략인 트랜잭션당 영속성 컨텍스트 전략은 문제가 발생하면 트랜잭션 AOP 종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함께 종료하므로 문제되지 않는다.
OSIV 처럼 영속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 사용해서 여러 트랜잭션이 하나의 영속성 컨텍스트를 사용할 때 발생한다. 이때는 트랜잭션을 롤백해서 영속성 컨텍스트에 이상이 발생해도 다른 트랜잭션에서 해당 영속성 컨텍스트를 그대로 사용하는 문제가 있다.
스프링 프레임워크는 영속성 컨텍스트의 범위를 트랜잭션의 범위보다 넓게 설정하면 트랜잭션 롤백시 영속성 컨텍스트를 초기화(EntityManager.clear())해서 잘못된 영속성 컨텍스트를 사용하는 문제를 예방한다.
트랜잭션 2, 3에서 엔티티 ㄱ, 엔티티 ㄴ을 수정했는데 아직 커밋되지 않고 쓰기 지연 저장소에만 저장되어 있는 상태라고 하자. 그러면 영속성 컨텍스트가 초기화되면서 해당 내용들은 db에 반영되지 않으니까 문제되잖아? 그러면 트랜잭션 1이 롤백되는 시점에 트랜잭션 2,3의 내용들은 다시 처음부터 시작되나? 자동으로?
영속성 컨텍스트 초기화는 단순히 엔티티 상태를 비우는 것이지 트랜잭션 자체를 초기화하거나 재시작하지 않습니다. 따라서 트랜잭션 2, 3에서 다음과 같은 문제가 발생할 수 있습니다:
OSIV 사용 시 이러한 문제를 피하기 위해 다음과 같은 방법을 고려할 수 있습니다:
트랜잭션과 영속성 컨텍스트를 가능한 한 짧고 독립적으로 유지하는 것이 가장 안전합니다.
스프링에서는 @Transactional(propagation = Propagation.REQUIRES_NEW)를 통해 트랜잭션 범위를 명확히 분리하고 새로운 영속성 컨텍스트를 생성할 수 있습니다.
Hibernate의 Session을 명시적으로 관리하거나, 트랜잭션 롤백 후 필요한 로직을 다시 실행하도록 코드를 설계합니다.
영속성 컨텍스트 초기화는 트랜잭션 상태와 별개로 발생하므로 트랜잭션 2, 3의 데이터가 자동으로 복구되거나 재시작되지는 않습니다. 따라서 OSIV 사용 시 트랜잭션 범위 관리를 더욱 신중하게 고려해야 합니다.
영속성 컨텍스트에는 1차 캐시가 있다. 영속성컨텍스트를 통해 데이터를 조회하거나 저장하면 1차 캐시에 엔티티가 저장된다. 이렇게 저장된 1차 캐시를 통해 변경감지도 하고 1차 캐시에 있는 엔티티를 반환하며 db와의 통신을 줄일 수도 있다. 이런 1차 캐시는 영속성 컨텍스트와 생명주기를 같이 한다.
1차 캐시의 장점인 애플리케이션 수준의 반복 가능한 읽기를 알아보자.
같은 영속성 컨텍스트에서 같은 식별자를 가지고 엔티티를 조회하면 동등성(equals)수준이 아니라 항상 주소값이 같은 인스턴스를 반환하는데 이를 애플리케이션 수준의 반복 가능한 읽기라고 한다.
일반적으로 반복 가능한 읽기(Repeatable Read)는 데이터베이스 트랜잭션 격리 수준(Isolation Level) 중 하나로, 동일한 트랜잭션 내에서 같은 쿼리를 여러 번 실행하더라도 결과가 동일하도록 보장합니다. 이를 애플리케이션 수준에서 구현한다는 의미는 데이터베이스에 의존하지 않고 애플리케이션 레벨에서 데이터 일관성을 유지하는 방법을 의미합니다.
예제를 통해 애플리케이션 수준의 반복 가능한 읽기에 대해 알아보자.

트랜잭션에서 실행하는 테스트 코드를 만들어 보자. 테스트의 범위와 트랜잭션의 범위가 아래 그림과 같은 트랜잭션에서 실행하는 테스트 코드를 만들어 보자. 테스트 전체에서 같은 영속성 컨텍스트에 접근하게 된다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:appConfig.xml")
@Transactional  //트랜잭션 안에서 테스트 실행
public class MemberServiceTest {
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;
    @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);   //참조값 비교
    }
    
    @Transactional
    public class MemberService() {
        
        @Autowired MemberRepository memberRepository;
        
        public Long join(Member member) {
            ...
            memberRepository.save(member);
            return member.getId();
        }
    }
    
    @Repository
    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);
        }
    }
}
MemberServiceTest에 @Transactional 어노테이션이 붙어 있어 이 안에서 실행하는 회원가입()메서드는 같은 트랜잭션 안에서 실행되고 종료된다. 그러므로 회원가입() 메서드에서 사용된 코드는 항상 같은 영속성 컨텍스트에 접근하게 된다. 아래 그림을 보자.

코드를 보면 회원가입()메서드에서 회원을 생성하고 memberRepository에서 em.persist(member)로 회원을 영속성 컨텍스트에 저장한다. 그리고 저장된 회원을 찾아서 저장한 회원과 비교한다.
같은 트랜잭션 범위에 있으므로 같은 영속성 컨텍스트를 사용하고 이는 참이 된다.
따라서 영속성 컨텍스트가 같으면 엔티티를 비교할 때 다음 3가지 조건을 모두 만족한다.
참고 : 테스트 클래스에도 @Transactional이 있고 서비스에도 같은 어노테이션이 있다. 이때 기본 전략은 이미 진행된 트랜잭션이 있으면 그 트랜잭션을 이어서 사용하고 없으면 새로 시작한다.
참고2 : 테스트 클래스에 @Transactional을 적용하면 테스트가 끝날 때 트랜잭션을 커밋하지 않고 트랜잭션을 강제로 롤백한다. 그래서 데이터베이스에 영향을 안 주고 반복해서 테스트를 진행 할 수 있다.
다만 롤백시 영속성 컨텍스트를 플러시하지 않기 때문에 플러시 시점에 어떤 SQL이 실행되는지 콘솔 로그가 남지 않는다. 어떤 SQL이 실행되는지 보고 싶으면 테스트 마지막에 em.flush()를 강제로 후출하면 된다.
테스트 클래스에 @Trasactional 을 없애서 트랜잭션 범위와 영속성 컨텍스트를 다르게 설정해 보자.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:appConfig.xml")
//@Transactional 
public class MemberServiceTest {
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;
    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member("kim");
        //When
        Long saveId = memberService.join(member);
        //Then
        Member findMember = memberRepository.findOne(saveId);
        //findMember는 준영속 상태
        
        //둘은 다른 주소값을 가진 인스턴스로 false
        assertTrue(member == findMember);   //참조값 비교
    }
    @Transactional  // 서비스에서 트랜잭션 시작
    public class MemberService() {
        @Autowired MemberRepository memberRepository;
        public Long join(Member member) {
            ...
            memberRepository.save(member);
            return member.getId();
        }
    }
    @Repository
    @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);
        }
    }
}
이 테스트는 assertTrue(member == findMember); 에서 false로 실패한다. 그림을 보며 자세히 살펴보자.

1.먼저 회원가입() 메서드에서 회원가입을 진행하며 서비스계층에서 트랜잭션이 시작된다.
2.영속성 컨텍스트1이 생성되며 여기서 memberRepository를 이용해서 엔티티를 해당 컨텍스트에 영속화한다.
3.서비스 계층이 끝나면서 트랜잭션이 커밋되면서 영속성 컨텍스트가 flush() 된다. 이 후 영속성 컨텍스트1과 트랜잭션이 종료된다. member는 준영속 상태가 된다.
4. 이 후 코드에서 memberRepository에서 저장한 엔티티를 조회하면 리포지토리 계층에서 새로운 트랜잭션이 시작되면서 새로운 영속성 컨텍스트2가 시작된다.
5. 영속성 컨텍스트2에는 찾는 회원이 존재 하지 않는다.
6. db를 조회하여 회원을 찾아온다.
7. db에서 조회한 엔티티를 영속성 컨텍스트2에 가져오고 저장한다.
8. memberRepository.findOne() 메소드가 끝나면서 트랜잭션이 종료되고 영속성 컨텍스트2도 종료된다.
이 처럼 member와 findMember는 다른 영송성 컨텍스트에서 관리되었기 때문에 다른 인스턴스이다.
member == findMember; (실패)
하지만 둘은 같은 데이터베이스 로우를 가르키고 있어 사실상 같은 엔티티로 보아야 한다.
이처럼 영속성 컨텍스트가 다르면 동일성 비교에 실패한다.
앞서 본 것 처럼 영속성 컨텍스트가 같으면 엔티티 비교는 동일성 비교만으로 충분하다. 따라서 같은 영속성 컨텍스트를 사용하는 OSIV에서는 동일성 비교가 성공하지만 영속성 컨텍스트가 다를 때는 다른 방법을 사용해야 한다.
member.getId().equals(findMember.getId())	//데이터베이스 식별자 비교
이렇게 식별자로 비교하는 방법이 있지만 엔티티를 먼저 영속화해야 한다는 문제점이 있다. 식별자 값을 먼저 부여한다는 방법도 있지만 항상 식별자를 먼저 부여하는 것을 보장하기는 쉽지 않다.
앞서 설명한 것처럼 비즈니스 키를 활용한 동등성 비교를 권장한다.
비즈니스 키가 되는 필드는 보통 중복되지 않고 거의 변하지 않는 데이터베이스 기본 키 후보들이 좋은 대상이다. 객체 상태에서만 비교하므로 유일성만 보장되면 데이터베이스 기본 키 같이 너무 딱딱하게 정하지 않아도 된다.
em.getRefence()로 프록시를 조회했다고 하자. 그 다음에 em.find()로 같은 식별자인 엔티티를 조회하면 어떻게 될까? 하나는 프록시고 하나는 엔티티라고 생각들겠지만 그렇지 않다.
영속성 컨텍스트는 프록시로 조회한 엔티티에 대해서 같은 엔티티를 찾는 요청이 오면 원본 엔티티가 아닌 처음 조회된 프록시를 반환한다. 따라서 프록시로 조회해도 영속성 컨텍스트는 영속 엔티티의 동일성을 보장한다. 
원본 엔티티를 먼저 조회하면 영속성 컨텍스트에서 원본 엔티티를 이미 데이터베이스에서 조회하여 가지고 있다. 그러므로 프록시를 조회해도 원본을 반환한다. 이렇게 영속성 컨텍스트에서는 자신이 관리하는 영속 엔티티의 동일성을 보장한다.
프록시는 원본 엔티티를 상속 받아서 만들었으므로 == 비교를 하면 안 된다. 대신에 instanceof를 통해 원본 엔티티의 자식 타입인지 확인한다.
Member엔티티의 프록시 proxyMember를 조회했다고 하자. 그러면 다음은 true이다!
(proxyMember instanceof Member) -> true
엔티티의 동등성을 비교하려면 비즈니스 키를 사용해서 equals() 메소드를 오버라이딩하면 된다. 그러나 IDE나 외부 라이브러리를 사용해서 구현한 equals() 메서드로 엔티티를 비교할 때, 비교 대상이 원본 엔티티면 문제가 없지만 프록시면 문제가 발생할 수 있다. equals() 메서드를 오버라이딩할 때 주의점을 알아보자.
(name을 비즈니스 키로 사용하는 회원 엔티티의 equals()를 오버라이딩한다고 가정)
 1. 프록시의 타입 비교는 == 대신에 instanceof를 사용해야 한다.
프록시는 앞서 말한 것처럼 == 동일성 비교를 하면 안 되고 instanceof를 사용해야 한다. 그러므로 eqauls()를 오버라이딩 할 때 다음과 같이 한다.
@Override
public boolean equals(Object obj) {
	...
   if (!(obj instanceof Member)) return false;
   if (name != null ? !name.equals(member.name) : member.name != null) return false
   ...
}
 2. 프록시의 멤버변수에 직접접근 하면 안 되고 getter를 사용해야 한다.
비즈니스 키를 이용해서 동등성을 비교한다고 했다. 그러나 member.name과 같이 멤버변수에 직접 접근하면 안 된다. 프록시의 경우 실제 데이터를 가지고 있지 않아 member.name의 결과가 항상 null을 반환하는 문제가 생기기 때문이다. 그러나 member.getName()처럼 getter를 통해 접근하면 Hibernate가 프록시 초기화를 수행해 원본 엔티티에 접근할 수 있어 동등성 비교를 할 수 있게 된다. 따라서 접근자 메서드를 사용해서 동등성 비교를 진행하자.
@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;
}
 프록시를 부모 타입으로 조회하면 문제가 발생한다.
프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성되기 때문에 다음과 같은 문제가 있다.

예제를 봐보자. Item을 상속하는 Book을 조회하고자 한다. 이때 Item을 프록시 객체로 받고 Book을 조회할 수 있을까?
@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());
        // 출력결과
        //proxyItem = class jpabook.proxy.advanced.item.Item_$$_jvstXXX
		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);	//false
		Assert.assertFalse(proxyItem instanceof Book);	//false
		Assert.assertTrue(proxyItem instanceof Item);	//true
}
// 출력결과
proxyItem = class jpabook.proxy.advanced.item.Item_$$_jvstXXX
em.getReference() 메소드를 사용해서 Item 엔티티를 프록시로 조회했다. 이때 실제 조회한 엔티티는 Book엔티티이므로 Book 엔티티를 기준으로 원본 엔티티 인스턴스가 생성된다. 그러나 Item 엔티티를 대상으로 프록시를 조회했으므로 proxyItem은 Item을 타입을 기반으로 만들어진다.
이런 문제를 해결하는 방법을 알아보자.
처음부터 자식 타입을 직접 조회해서 연산하기. 다만 이 방법은 다형성을 활용할 수 없다.
Book jpqlBook = em.createQuery
	("select b from Book b where b.id=:bookId", Book.class)
    .getSingleResult();
//하이버네이트가 제공하는 프록시에서 원본 엔티티를 찾는 기능을 사용하는 메서드
public static <T> T unProxy(Object entity) {
	if (entity instanceof HibernateProxy) {
    	entity = ((HibernateProxy) entity)
        			.getHibernateLazyInitializer()
                    .getImplememtation();
     }
     return (T) entity;
영속성 컨텍스트는 한 번 프록시로 노출한 엔티티는 계속 노출하여 영속 엔티티의 동일성을 보장한다. 그래서 클라이언트는 조회한 엔티티가 프록시인지 아닌지 구분하지 않고 사용할 수 있다. 그러나 이 방법은 프록시에서 원본 엔티티를 직접 꺼내기 때문에 프록시와 원본 엔티티의 동이성 비교가 실패한다는 문제가 있다.
이 방법을 사용할 때는 원본 엔티티가 필요한 곳에서 잠깐 사용하고 다른 곳에서 사용되지 않도록 하는 것이 중요하다. 참고로 원본 엔티티의 값을 직접 변경해도 변경 감지 기능은 동작한다.
Item 클래스가 상속하는 특별한 인터페이스를 만드는 방법도 있다.
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;
		//Getter, Setter
		...
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
		private String author;
		private String isbn;
		//Getter, Setter
		@Override
		public String getTitle() {
				return "[제목:" + getName() + " 저자:" + author + "]";
		}
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
		private String director;
		private String actor;
		//Getter, Setter
		
		@Override
		public String getTitle() {
				return "[제목:" + getName() + " 감독:" + director + " 배우 :" + actor + "]";
		}		
}
TitleView라는 공통 인터페이스를 만들고 자식 클래스들은 인터페이스의 getTitle()을 각자 구현한다.
이러고 OrderItem에 printItem() 메소드를 구현한다.
@Entity
public class OrderItem {
		@Id @GeneratedValue
		private Long id;
		@ManyToOne(fetch = FetchType.LAZY)
		@JoinColumn(name = "ITEM_ID")
		private Item item;
		...
		public void printItem() {
				System.out.println("TITLE=" + item.getTitle());
		}
}
이러면 이제 Item 구현체에 맞는 getTitle()이 동작한다.
이 방법은 두 가지 장점을 제공한다.
이 방법을 사용할 때는 프록시의 특징 때문에 프록시의 대상이 되는 타입에 인터페이스를 적용해야 한다.
여기서는 Item이 지연로딩으로 프록시 타입을 반환하므로 Item에 공통 인터페이스를 설정했다.
비지터패턴으로 상속관계와 프록시 문제를 해결해보자.

비지터 패턴은 Visitor와 Visitor를 받아들이는 대상 클래스로 구성된다. 여기서 Item이 accept(visitor)를 사용해서 Visitor를 받아들이고 실제 로직은 Visitor가 처리한다.
public interface Visitor {
		void visit(Book book);
		void visit(Album album);
		void visit(Movie movie);
}
// 비지터 구현 - 대상 클래스의 내용을 출려하는 visitor
public class PrintVisitor implements Visitor {
		@Override
		public void visit(Book book) {
				//넘어오는 book은 Proxy가 아닌 원본 엔티티
				System.out.println("book.class = " + book.getClass());
				System.out.println("[PrintVisitor] [제목: " + book.getName() + 
						"저자 :" + book.getAutor() + "]");
		}
		@Override
		public void visit(Album album) {...}
		@Override
		public void visit(Movie album) {...}
}
// 대상 클래스의 제목을 보관하는 visitor
public class TitleVisitor implements Visitor {
		private String title;
		
		public String getTitle() {
				return title;
		}
		@Override
		public void visit(Book book) {
				title = "[제목:" + book.getName() + "저자:" + book.getAuthor() + "]";
		}
		@Override
		public void visit(Album album) {...}
		@Override
		public void visit(Movie movie) {...}
}
@Entity
@Inheritance(strategy = InheritanceType.Single_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
		@Id @GeneratedValue
		@Column(name = "ITEM_ID")
		private Long id;
		private String name;
		
		...
			
		public abstract void accept(Visitor visitor);
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
		private String author;
		private String isbn;
		
		//Getter, Setter
		@Override
		public void accept(Visitor visitor) {
				visitor.visit(this);
		}
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
		...
		@Override
		public void accept(Visitor visitor) {
				visitor.visit(this);
		}
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
		...
		@Override
		public void accept(Visitor visitor) {
				visitor.visit(this);
		}
}
자식 클래스에서 구현한 accept를 보면 단순히 파라미터로 넘어온 Visitor의 visit(this) 메소드를 호출하면서 자신(this)를 파라미터로 넘기는 것이 전부다. 이렇게 해서 실제 로직은 visitor에 위임한다.
@Test
public void 상속관계와_프록시_VisitorPattern() {
		...
		OrderItem orderItem = em.find(OrderItem.class, orderItemId);
		Item item = orderItem.getItem();
		//PrintVisitor
		item.accept(new PrintVisitor());
}
//출력결과
book.class = class.jpabook.advanced.item.Book
[PrintVisitor][제목:jpabook 저자:kim]
item이 프록시여서 먼저 프록시가 accept() 메소드를 받고 원본 엔티티(book)의 accept를 싱행한다. 원본 엔티티는 자신(this)을 visitor에 파라미터로 넘겨주어 book의 클래스가 프록시가 아닌 원본 엔티티인것을 확인 할 수 있다.
이렇게 비지터 패턴을 사용하면 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근할 수 있고 instanceof없이 코드를 구현할 수 있는 장점이 있다.
비지터 패턴은 새로운 기능이 필요할 때 Visitor만 추가하면 된다. 따라서 기존 코드의 구조를 변경하지 않아도 된다.
더블 디스패치란 메서드 호출 시 두 개의 객체가 타입에 따라 적절한 메서드를 선택하는 패턴입니다. 객체 지향 프로그래밍에서는 일반적으로 하나의 객체 타입(레퍼런스)에 따라 메서드를 선택하는 싱글 디스패치가 기본입니다.
JPQL을 사용할 때 문제가 생긴다. 회원(Member)과 주문(Order)이 양방향 연관관계라고 할 때 다음 JQPL을 보자.
List<Member> members = em.createQuery("select m from Member m", Member.class)
	.getResultList();
JPQL은 즉시로딩, 지연로딩을 신경쓰지 않고 SQL을 실행한다.
이때 조회된 회원이 여러명이면 문제가 생긴다. 예를 들어 3명의 회원이 조회됐다고 해보자.
SELECT * FROM MEMBER //1번 실행으로 회원 여러명 조회
SELECT * FROM ORDERS WHERE MEMBER_ID = 1; //회원과 연관된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID = 2; //회원과 연관된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID = 3; //회원과 연관된 주문
이처럼 처음 실행한 SQL의 수만큼 추가적으로 SQL을 실행하는 것을 N+1문제라고 한다.
지연로딩도 N+1 문제를 피할 수 없다. 로직상 조회한 컬렉션을 초기화한다고 하면 회원 수 만큼 주문도 추가 조회된다.
for(Member member : memers) {
	//지연 로딩 초기화
    	System.out.println("member = " + member.getOrders().size());
}
//실행 SQL
SELECT * FROM ORDERS WHERE MEMBER_ID = 1; //회원과 연관된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID = 2; //회원과 연관된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID = 3; //회원과 연관된 주문
N+1 문제를 피할 수 있는 다양한 방법을 알아보자.
N+1 문제를 해결하는 가장 일반적인 방법으로 페치조인이 있다.
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
org.hibernate.annotations.BatchSize 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정한 size만큼 SQL의 IN 절을 사용해서 조회한다.
@Entity
public class Member {
		...
		@org.hibernate.annotaions.BatchSize(size = 5)
		@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
		private List<Order> orders = new ArrayList<Order>();
		...
}
위처럼 즉시로딩의 경우 10명을 조회하면 2번의 SQL이 발생한다.
지연로딩의 경우는 초기화할 때 5명을 미리 조회하고 6번째가 필요한 경우 추가적으로 SQL을 실행한다.
해당 엔티티는 연관된 데이터를 조회할 때 서브 쿼리를 사용해서 N+1문제를 해결한다.
@Entity
public class Member {
		...
		@org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
		@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
		private List<Order> orders = new ArrayList<Order>();
		...
}
즉시 로딩으로 설정하면 조회시점에 지연 로딩은 엔티티를 사용하는 시점에 SQL이 실행된다.
select m from Member m where m.id > 10
SELECT O FROM ORDERS O 
	WHERE O.MEMBER_ID IN (
			SELECT 
					M.ID
			FROM
					MEMBER M
			WHERE M.ID > 10
)
추천 방법 : 지연 로딩만 사용하기.
즉시 로딩 전략은 N+1문제와 비즈니스 로직에 따라 필요하지 않은 엔티티까지 조회하는 문제가 자주 발생한다.
또한 최적화가 어렵다.
따라서 모두 지연 로딩으로 설정하고 성능 최적화가 꼭 필요한 곳에는 JPQL 페치 조인을 사용하자.
기본값이 즉시로딩인 것들은 fetch=FetchType.LAZY로 설정하도록 하자.
엔티티가 영속성 컨텍스트에 관리되면 1차 캐시, 변경 감지등의 이점이 많다.
하지만 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있다.
조회만 하고 변경하는 일이 없다면 읽기 전용으로 엔티티를 조회하여 메모리 사용량을 최적화할 수 있다.
다음 JPQL 쿼리를 최적화하자.
select o from Order o
스칼라 타입으로 모든 타입을 조회하기. (스칼라 타입은 엔티티 영속성 컨텍스트가 관리하지 않음)
select o.id, o.name, o.price from Order o
하이버네이트에서 전용 힌트 org.hibernate.readOnly 사용하기.
해당 힌트를 이용하면 읽기 전용으로 엔티티를 조회할 수 있다. 영속성 컨텍스트에서 관리하지 않으므로 메모리 사용량을 최적화할 수 있다. 스냅샷이 없으므로 엔티티를 수정해도 데이터베이스에 반영되지 않는 점은 알아야 한다.
TypedQuery<Order> query = em.createQuery("select o from Order o", Order.class);
query.setHint("org.hibernate.readOnly", true);
스프링 프레임워크의 @Transactional(readOnly = true)로 트랜잭션을 읽기 전용으로 설정할 수 있다.
이 옵션을 주면 하이버네이트 세션의 플러시 모드를 MANAUAL로 설정하여 강제로 플러시를 호출하지 않는한 플러시가 일어나지 않는다.
트랜잭션을 시작했으므로 트랜잭션 시작, 로직수행, 트랜잭션 커밋의 과정은 이루어지지만 영속성 컨텍스트가 플러시를 하지 않을 뿐이다. 플러시를 하지 않으므로 스냅샷 비교와 같은 무거운 로직을 수행하지 않아 성능이 향상된다.
트랜잭션 없이 엔티티를 조회하는 방법이다. 물론 데이터 변경을 위해서는 트랜잭션이 필수이므로 조회할 때만 사용해야 한다.
트랜잭션을 사용하지 않으면 플러시가 일어나지 않으므로 조회성능이 좋아진다. JPQL도 트랜잭션 없이 실행하면 플러시를 호출하지 않는다.
지금까지 읽기 전용 최적화를 위해 여러 방법을 살펴보았는데 스프링 프레임워크를 사용하면 읽기 전용 트랜잭션을 사용하는 것이 편리하다.
읽기 전용 트랜잭션(또는 트랜잭션 밖에서 읽기)과 읽기 전용 쿼리 힌트(또는 스칼라 타입으로 조회)를 동시에 사용하는 것이 가장 효과적이다.
@Transactional(readOnly = true) //읽기 전용 트랜잭션 -- 1
public List<DataEntity> findDatas() {
		return em.createQuery("select d from DataEntity d", DataEntity.class)
							.setHint("org.hibernate.readOnly", true) //읽기 전용 쿼리 힌트 --2
							.getResultList();
}
수백만 건의 데이터를 배치 처리한다고 가정해보자.
엔티티를  계속 조회하면 영속성 컨텍스트에 메모리 부족 오류가 발생한다. 따라서 배치 처리는 적절한 단위로 영속성 컨텍스트를 초기화해야 한다. 또한 2차 캐시를 사용하고 있다면 2차 캐시에 엔티티르 보관하지 않도록 주의해야 한다.
// 100건마다 플러시 호출하고 영속성 컨텍스트 초기화
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();
영속성 컨텍스트에 너무 많은 엔티티가 쌓이지 않도록 일정 단위마다 데이터를 플러쉬하고 영속성컨텍스트를 초기화해야 한다.
수정 배치 처리는 수 많은 데이터를 한 번에 메모리에 올려둘 수 없어서 페이징 처리, 커서(CURSOR) 2 가지 방법을 사용한다.
EntityManger 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)
							.setMaxResult(pageSize)
							.getResultList();
		for(Product product : resultList) {
				product.setPrice(product.getPrice() + 100);
		}
		
		em.flush();
		em.clear();
}
tx.commit();
em.close();
100건씩 가져와서 가격을 100원씩 올리는 코드이다. 이 때 100건을 가져오고 100건을 수정하고 나면 영속성 컨텍스트를 플러쉬하고 초기화한다.
JPA는 JDBC 커서(CURSOR)를 지원하지 않아 하이버네이트 세션(Session)을 이용한다. 하이버네이트는 scroll이라는 이름으로 JDBC 커서를 지원한다.
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();
하이버네이트 전용 기능인 scroll을 사용하기 위해 먼저 em.unwrap() 메서드로 하이버네이트 세션을 구한다.
쿼리를 조회하면서 scroll() 메서드로 ScrollableResults 객체를 반환 받는다.
이 객체의 nest()를 통해 엔티티를 하나씩 조회한다.
하이버네이트 무상태 세션은 일반 하이버네이트 세션과 비슷하지만 영속성 컨텍스트도 없고 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); //직접 update를 호출
}
tx.commit();
session.close();
JPA는 데이터베이스 SQL 힌트 기능을 제공하지 않는다.
SQL 힌트를 사용하려면 하이버네이트를 직접 사용해야 한다.(데이터베이스 벤더에게 제공하는 힌트)
SQL 힌트는 하이버네이트 쿼리가 제공하는 addQueryHint() 메서드를 사용한다. 오라클 데이터베이스에 SQL 힌틀르 사용하는 예제를 보자.
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 힌트를 사용하려면 각 방언에 org.hibernate.dialect.Dialect에 있는 메소드 오버라이딩이 필요하다.
public String getQueryHintString(String query, List<String> hints) {
		return query
}
네트워크 호출 한 번은 단순한 메서드를 수만 번 호출하는 것보다 더 큰 비용이 든다. JDBC가 제공하는 SQL 배치 기능을 사용하면 SQL을 모아서 데이터베이스에 한 번에 보낼 수 있다.
하지만 코드를 많이 수정해야 하고, 코드가 많이 얽혀 있는 곳에서는 사용하기 쉽지 않아 수백 수천 건 이상의 데이터를 변경하는 특수한 상황에 SQL 배치 기능을 사용한다.
JPA는 플러시 기능이 있으므로 SQL 배치 기능을 효과적으로 사용할 수 있다.
하이버네이트에서는 다음과 같이 설정하면 데이터를 등록, 수정, 삭제할 때 SQL 배치 기능을 사용한다.
<property name="hibernate.jdbc.batch_size" value="50"/>
속성의 값으로 50을 주어서 최대 50건씩 모아서 SQL 배치를 실행하도록 했다. 같은 SQL일 때만 유효한 것을 주의하자. 예를 들어 다음과 같은 경우가 있다고 하자.
em.persist(new Member()); //1
em.persist(new Member()); //2
em.persist(new Member()); //3
em.persist(new Member()); //4
em.persist(new Child()); //5, 다른연산
em.persist(new Member()); //6
em.persist(new Member()); //7
4까지 모아서 하나의 SQL 배치를 실행하고 5에서 하나의 SQL 배치 실행, 6,7을 모아서 SQL 배치를 실행하여 총 번의 SQL 배치가 실행된다.
참고 : IDENTITY 식별자 생성전략을 쓰면 em.persist()를 호출하는 즉시 DB와 통신하므로 쓰기 지연을 활용한 성능 최적화를 할 수 없다.
트랜잭션을 지원하는 쓰기 지연과 변경 감지 덕분에 성능이 향상되고 편의를 봤지만 진짜 장점은 데이터베이스 테이블 로우에 락이 걸리는 시간을 최소회한다는 점이다.
이 기능은 트랜잭션을 커밋해서 영속성 컨텍스트를 플러시하기 전까지는 데이터베이스에 데이터를 등록, 수정, 삭제하지 않는다. 따라서 커밋 직전까지 데이터베이스 로우에 락을 걸지 않는다.
update(memberA);	//UPDATE SQL A
비즈니스로직A();		//UPDATE SQL ...
비즈니스로직B();		//INSERT SQL ...
commit();
JPA를 사용하지 않고 SQL을 직접 사용하면 맨 처음 update()를 호출할 때 UPDATE SQL을 실행하면서 데이터베이스 로우에 락이 걸린다. 이 락은 비즈니스로직이 모두 실행되고 commit()이 실행될 때까지 유지된다.
트랜잭션 격리 수준에 따라 다르지만 보통 많이 사용하는 커밋된 읽기(Read Committed) 격리 수준이나 그 이상에서는 데이터베이스에 현재 수정 중인 데이터(로우)를 수정하려는 다른 트랜잭션은 락이 풀릴 때까지 대기한다.
JPA는 커밋을 해야 플러시를 호출하고 데이터베이스에 수정 쿼리를 보낸다. commit()을 호출할 때에야 UPDATE SQL을 실행하고 바로 데이터베이스 트랜잭션을 커밋한다. 쿼리를 보내고 바로 트랜잭션을 커밋하므로 데이터베이스에 락이 걸리는 시간을 최소화한다.
JPA의 쓰기 지연 기능은 데이터베이스에 락이 걸리는 시간을 최소화해서 동시에 더 많은 트랜잭션을 처리할 수 있는 장점이 있다.
참조 : [자바 ORM 표준 JPA 프로그래밍]