PersistenceExceptionTranslationPostProcessor를 사용해 스프링 프레임워크가 제공하는 추상화된 예외로 변경할 수 있다.
@Repository 어노테이션을 사용한 곳에 예외 변환 AOP를 적용해 추상화된 예외로 변경해준다.
@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();
}
}
15.1.4 트랜잭션 롤백 시 주의사항
트랜잭션 롤백이란 데이터베이스의 반영사항만 롤백하는 것이다.
수정한 자바 객체까지 원상태로 복구해주는게 아니다.
객체는 수정된 상태로 영속성 컨텍스트에 남아있다.
이런 영속성 컨텍스트를 그대로 사용하는 것은 위험하다.
새로운 영속성 컨텍스트를 사용하거나 EntityManager.clear()를 호출해 기존의 영속성 컨텍스트를 초기화해 사용해야 한다.
스프링 프레임워크: 영속성 컨텍스트의 범위에 따라 다른 방법 사용
기본 전략: 트랜잭션당 영속성 컨텍스트 전략
문제 발생 시 트랜잭션 AOP 종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함께 종료하기 때문에 문제가 없다.
OSIV와 같이 영속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 사용해 여러 트랜잭션이 하나의 영속성 컨텍스트를 사용할 때 문제가 생길 수 있다.
15.2 엔티티 비교
15.2.1 영속성 컨텍스트가 같을 때 엔티티 비교
테스트 클래스에 @Transactional이 선언되어 있으면 트랜잭션을 먼저 시작하고 테스트 메소드를 실행한다.
테스트 메소드인 회원가입()은 이미 트랜잭션 범위에 들어있게 되고, 해당 메소드가 끝나면 트랜잭션이 종료된다.
즉, 회원가입()에서 사용된 코드는 항상 같은 트랜잭션과 같은 영속성 컨텍스트에 접근한다.
테스트는 성공한다.
참조 값을 비교하는데, 같은 트랜잭션 범위에 있어 같은 영속성 컨텍스트를 사용하기 때문에 둘은 완전히 동일한 인스턴스이기 때문이다.
@Transactional
@SpringBootTest(properties = "spring.profiles.active:local")
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
// given
Member member = new Member("kang");
// 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);
}
}
15.2.2 영속성 컨텍스트가 다를 때 엔티티 비교
15.2.1 의 예제와 달리, 테스트 클래스에 @Transaction이 없고 서비스에만 @Transaction이 있다면 아래와 같은 트랜잭션 범위와 영속성 컨텍스트 범위를 가지게 된다.
@Transactional
@SpringBootTest(properties = "spring.profiles.active:local")
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
// given
Member member = new Member("kang");
// when
Long saveId = memberService.join(member);
// then
// findMember는 준영속 상태
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
@Transactional // findOne 호출 시 새로운 트랜잭션이 시작된다.
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);
}
}
테스트 코드에서 memberService.join(member) 호출 시
서비스 계층에서 트랜잭션 시작되며 영속성 컨텍스트1이 만들어진다.
memberRepository에서 em.persist() 호출해 member 엔티티를 영속화한다.
서비스 계층이 끝나면 트랜잭션이 커밋되면서 영속성 컨텍스트가 플러시된다.
트랜잭션과 영속성 컨텍스트1 종료 (member 엔티티는 준영속 상태)
이후 memberRepository.findOne(saveId) 호출 시
리포지토리 계층에서 새로운 트랜잭션이 시작되면서 영속성 컨텍스트2가 만들어진다.
저장된 회원을 조회하지만 영속성 컨텍스트2에는 찾는 회원이 존재하지 않는다.
데이터베이스에 접근해 회원을 찾아온다.
해당 회원을 영속성 컨텍스트에 보관하고 반환한다.
memberRepository.findOne(saveId) 메소드가 끝나면 트랜잭션, 영속성 컨텍스트는 종료된다.
정리
같은 영속성 컨텍스트라면 동일성 비교로 충분히다.
OSIV과 같이 요청의 처음부터 끝까지 같은 영속성 컨텍스트를 사용할 때는 동일성 비교가 가능(성공)하다.
다른 영속성 컨텍스트라면 동일성 비교는 사용할 수 없다.
데이터베이스 동등성 비교도 가능하다.
예) 데이터베이스 식별자 비교
엔티티를 영속화해야 식별자를 얻기 때문에 한계가 있다.
equals()를 사용한 동등성 비교도 가능하다.
엔티티를 비교할 때는 비즈니스 키를 활용한 비교를 권장한다.
equals()를 오버라이딩할 때, 중복이 잘 되지 않고 변경되지 않는 비즈니스 키를 활용하는 것이 좋다.
롬복 사용도 편리
15.3 프록시 심화 주제
프록시는 원본 엔티티를 상속받아 만들어지기 때문에 클라이언트는 엔티티가 원본인지 프록시인지 구분 않고 사용이 가능하다.
원본 엔티티를 사용하다가 지연 로딩하려고 프록시로 변경해도 클라이언트 비즈니스 로직 수정하지 않아도 된다.
근데 프록시 사용 방식의 기술적인 한계로 인해 예상하지 못한 문제 발생.
15.3.1 영속성 컨텍스트와 프록시
영속성 컨텍스트는 자신이 관리하는 영속 엔티티의 동일성 보장
프록시로 조회한 엔티티의 동일성도 보장할까?
아래 예시를 참고하자.
예시1
em.getReference()로 member1을 프록시로 조회한다.
em.find()로 같은 member1을 조회한다.
refMember는 프록시, findMember는 원본 엔티티이기 때문에 다른 인스턴스?
그렇다면 영속성 컨텍스트가 영속 엔티티의 동일성을 보장하지 못하는 문제가 발생하게 된다.
영속성 컨텍스트는 프록시로 조회된 엔티티에 대해 같은 엔티티를 찾는 요청이 오면 원본이 아닌 프록시를 반환한다.
예시에서 member1 엔티티를 프록시로 처음 조회했기 때문에 이후에 em.find()를 사용해 같은 member1을 찾아도 원본이 아닌 프록시를 반환한다.
즉, 프록시로 조회해도 영속성 컨텍스트는 영속 엔티티의 동일성을 보장한다.
@Test
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");
// refMember Type = class jpabook.advanced.Member_$$_jvst843_0
// findMember Type = class jpabook.advanced.Member_$$_jvst843_0
System.out.println("refMember Type = " + refMember.getClass());
System.out.println("findMember Type = " + findMember.getClass());
assertTrue(refMember == findMember); // 성공
}
예시2
그렇다면 예시1과 반대로 원본을 먼저 조회하고 프록시를 조회한다면?
프록시가 아닌 원본을 반환한다.
이 경우 또한 영속성 컨텍스트가 관리하는 영속 엔티티의 동일성을 보장하게 되는 것이다.
@Test
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");
// refMember Type = class jpabook.advanced.Member
// findMember Type = class jpabook.advanced.Member
System.out.println("refMember Type = " + refMember.getClass());
System.out.println("findMember Type = " + findMember.getClass());
assertTrue(refMember == findMember); // 성공
}
15.3.2 프록시 타입 비교
프록시는 원본 엔티티를 상속받아 만들어지기 때문에 == 비교가 아닌 instanceof를 사용해야 한다.
예시
@Test
void 프록시_타입비교() {
Member newMember = new Member("member1", "회원1");
em.persist(newMember);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, "member1");
// refMember Type = class jpabook.advanced.Member
System.out.println("refMember Type = " + refMember.getClass());
assertFlase(Member.class == refMember.getClass()); // 성공
assertTrue(refMember instanceof Member); // 성공
}
15.3.3 프록시 동등성 비교
엔티티의 동등성을 비교하는 방법으로는 비즈니스 키를 사용해 equals() 메소드를 오버라이딩하고 비교하는 방법이 있다.
만약 롬복 등을 사용해 equals() 메소드로 엔티티 비교할 때, 프록시라면 문제가 발생할 수 있다.
예시
@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;
Member member = (Member) obj;
if (name != null ? !name.equals(member.name) : member.name != null) return false;
return true;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
}
@Test
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");
assertTrue(newMember.equals(refMember)); // 실패
}
테스트가 실패한 원인은 두 가지가 있다.
if (this.getClass() != obj.getClass()) return false;에 문제가 있다.
프록시 타입을 비교할 때는 ==가 아닌 instanceof를 사용해야하기 때문이다.
if (name != null ? !name.equals(member.name) : member.name != null) return false;에도 문제가 있다.
member.name으로 프록시 필드에 직접 접근하게 된다.
프록시의 데이터를 조회할 때는 접근자(Getter)를 사용해야 한다.
15.3.4 상속관계와 프록시
위와 같은 관계에서, 프록시를 부모 타입으로 조회하면 문제가 발생한다.
예시 참고
예시
Item이 Book 타입이면 저자 이름을 출력하도록 한다.
출력되는 결과 : proxyItem = class jpabook.proxy.advanced.item.Item_$$_jvstXXX
@Test
void 부모타입으로_프록시조회() {
// given
Book saveBook = new Book();
saveBook.setName("jpaBook");
saveBook.setAuthor("kim");
em.persist(saveBook);
em.flush();
em.clear();
// when
Item proxyItem = em.getReference(Item.class, saveBook.getId());
System.out.println("proxyItem = " + proxyItem.getClass());
if (proxyItem instanceof Book) { // false
System.out.println("proxyItem instanceof Book");
Book book = (Book) proxyItem; // ClassCastException 발생
System.out.println("책 저자 = " + book.getAuthor());
}
// then
assertFalse(proxyItem.getClass() == Book.class);
assertFalse(proxyItem instanceof Book);
assertTrue(proxyItem instanceof item);
}
em.getReference() 메소드에서 Item 엔티티를 대상으로 조회했기 때문에 proxyItem은 Item 타입 기반으로 만들어진다.
즉, 프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성되는 문제가 있다.
고급 주제와 성능 최적화
15.1 예외 처리
15.1.1 JPA 표준 예외 정리
javax.persistence.PersistenceException
의 자식 클래스이다.RollbackException
예외가 발생한다.15.1.2 스프링 프레임워크의 JPA 예외 변환
15.1.3 스프링 프레임워크에 JPA 예외 변환기 적용
PersistenceExceptionTranslationPostProcessor
를 사용해 스프링 프레임워크가 제공하는 추상화된 예외로 변경할 수 있다.@Repository
어노테이션을 사용한 곳에 예외 변환 AOP를 적용해 추상화된 예외로 변경해준다.15.1.4 트랜잭션 롤백 시 주의사항
EntityManager.clear()
를 호출해 기존의 영속성 컨텍스트를 초기화해 사용해야 한다.15.2 엔티티 비교
15.2.1 영속성 컨텍스트가 같을 때 엔티티 비교
@Transactional
이 선언되어 있으면 트랜잭션을 먼저 시작하고 테스트 메소드를 실행한다.회원가입()
은 이미 트랜잭션 범위에 들어있게 되고, 해당 메소드가 끝나면 트랜잭션이 종료된다.회원가입()
에서 사용된 코드는 항상 같은 트랜잭션과 같은 영속성 컨텍스트에 접근한다.15.2.2 영속성 컨텍스트가 다를 때 엔티티 비교
@Transaction
이 없고 서비스에만@Transaction
이 있다면 아래와 같은 트랜잭션 범위와 영속성 컨텍스트 범위를 가지게 된다.memberService.join(member)
호출 시영속성 컨텍스트1
이 만들어진다.em.persist()
호출해 member 엔티티를 영속화한다.영속성 컨텍스트1
종료 (member 엔티티는 준영속 상태)memberRepository.findOne(saveId)
호출 시영속성 컨텍스트2
가 만들어진다.영속성 컨텍스트2
에는 찾는 회원이 존재하지 않는다.memberRepository.findOne(saveId)
메소드가 끝나면 트랜잭션, 영속성 컨텍스트는 종료된다.정리
equals()
를 사용한 동등성 비교도 가능하다.equals()
를 오버라이딩할 때, 중복이 잘 되지 않고 변경되지 않는 비즈니스 키를 활용하는 것이 좋다.15.3 프록시 심화 주제
15.3.1 영속성 컨텍스트와 프록시
예시1
em.getReference()
로member1
을 프록시로 조회한다.em.find()
로 같은member1
을 조회한다.refMember
는 프록시,findMember
는 원본 엔티티이기 때문에 다른 인스턴스?member1
엔티티를 프록시로 처음 조회했기 때문에 이후에 em.find()를 사용해 같은member1
을 찾아도 원본이 아닌 프록시를 반환한다.예시2
15.3.2 프록시 타입 비교
상속
받아 만들어지기 때문에==
비교가 아닌instanceof
를 사용해야 한다.예시
15.3.3 프록시 동등성 비교
equals()
메소드를 오버라이딩하고 비교하는 방법이 있다.equals()
메소드로 엔티티 비교할 때, 프록시라면 문제가 발생할 수 있다.예시
if (this.getClass() != obj.getClass()) return false;
에 문제가 있다.==
가 아닌instanceof
를 사용해야하기 때문이다.if (name != null ? !name.equals(member.name) : member.name != null) return false;
에도 문제가 있다.member.name
으로 프록시 필드에 직접 접근하게 된다.Getter
)를 사용해야 한다.15.3.4 상속관계와 프록시
예시
proxyItem = class jpabook.proxy.advanced.item.Item_$$_jvstXXX
em.getReference()
메소드에서 Item 엔티티를 대상으로 조회했기 때문에 proxyItem은 Item 타입 기반으로 만들어진다.