예외 처리
- JPA 표준 예외 정리
JPA 표준 예외들은 PersistenceException의 자식 클래스이며 이 예외 클래스는 RuntimeException의 자식임
따라서 JPA 예외는 모두 언체크 예외이며 크게 2가지로 나눌 수 있음
- 트랜잭션 롤백을 표시하는 예외
트랜잭션 롤백을 표시하는 예외는 심각한 예외이므로 복구해서는 안 됨
이 예외가 발생하면 트랜잭션을 강제로 커밋해도 트랜잭션이 커밋되지 않고 대신에 RollbackException 예외가 발생 - 트랜잭션 롤백을 표시하지 않는 예외
트랜잭션 롤백을 표시하지 않는 예외는 심각한 예외가 아니므로 개발자가 트랜잭션을 커밋할지 롤백할지를 판단하면 됨
- 트랜잭션 롤백을 표시하는 예외
- 스프링 프레임워크와 JPA 예외 반환
서비스 계층에서 JPA의 예외를 직접 사용하면 JPA에 의존하게 되므로 스프링 프레임워크는 이런 문제를 해결하려고
데이터 접근 계층에 대한 예외를 추상화해서 (JPA 예외가 스프링 예외로 변환되어) 개발자에게 제공함
또한 JPA 표준 명세상 발생할 수 있는 두 예외도 추상화해서 제공함
- 스프링 프레임워크에 JPA 예외 변환기 적용
JPA 예외를 스프링 프레임워크가 제공하는 추상화된 예외로 변경하려면
PersistenceExceptionTranslationPostProcessor를 스프링 빈으로 등록하면 됨
이것은 @Reposotiry 어노테이션을 사용한 곳에 예외 변환 AOP를 적용해서
JPA 예외를 스프링 프레임워크가 추상화한 예외로 변환해줌
// 설정 방법
<bean class="org.springframework.dao.annotation. PersistenceExceptionTranslationPostProcessor" />
// JavaConfig를 사용할 때 등록
@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
retrun new PersistenceExceptionTranslationPostProcessor();
}
// 예외 변환 코드
@Repository
public class NoResultExceptionTestRepository {
@PersistenceContext EntityManager em;
public Member findMember() {
// 조회된 데이터가 없음
/* getSingleResult() 메소드는 조회된 결과가 없으면 NoResultException이 발생
이 예외가 findMember() 메소드를 빠져 나갈 때 PersistenceExceptionTranslationPostProcessor에서
등록한 AOP 인터셉터가 동작해서 해당 예외를 EmptyResultDataAccessException 예외로 변환해서 반환
따라서 이 메소드를 호출한 클라이언트는 스프링 프레임워크가 추상화한 예외를 받게 됨 */
return em.createQuery("select m from Member m", Member.class).getSingleResult();
}
}
// 예외를 변환하지 않는 코드
@Repository
public class NoResultExceptionTestRepository {
@PersistenceContext EntityManager em;
/* 만약 예외를 반환하지 않고 그대로 반환하고 싶으면
throws 절에 그대로 반환할 JPA 예외나 JPA 예외의 부모 클래스를 직접 명시
참고로 Exception을 선언하면 모든 예외의 부모이므로 예외를 반환하지 않음 */
public Member findMember() throws javax.persistence.NoResultException {
return em.createQuery("select m from Member m", Member.class).getSingleResult();
}
}
- 트랜잭션 롤백 시 주의사항
트랜잭션을 롤백하는 것은 데이터베이스의 반영사항만 롤백하는 것이지 수정한 자바 객체까지 원상태로 복구해주지는 않음
예) 엔티티를 조호해서 수정하는 중에 문제가 있어서 트랜잭션을 롤백하면
데이터베이스의 데이터는 원래대로 복구되지만 객체는 수정된 상태로 영속성 컨텍스트에 남아있음
따라서 트랜잭션이 롤백된 영속성 컨텍스트를 그대로 사용하는 것은 위험하므로
새로운 영속성 컨텍스트를 생성해서 사용하거나 EntityManager.clear()를 호출해 영속성 컨텍스트를 초기화한 후 사용해야 함
스프링 프레임워크는 이런 문제를 예방하기 위해 영속성 컨텍스트의 범위에 따라 다른 방법을 사용
- 기본 전략인 트랜잭션당 영속성 컨텍스트 전략은 문제가 발생하면
트랜잭션 AOP 종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함께 종료하므로 문제가 발생하지 않음 - 영속성 컨텍스트 범위를 트랜잭션 범위보다 넓게 사용해 여러 트랜잭션이 하나의 영속성 컨텍스트를 사용하는 OSIV의 경우
트랜잭션을 롤백해서 영속성 컨텍스트에 이상이 발생해도 다른 트랜잭션에서 해당 영속성 컨텍스트를 그대로 사용해 문제
그러므로 스프링 프레임워크는 이처럼 영속성 컨텍스트의 범위를 트랜잭션의 범위보다 넓게 설정할 경우
트랜잭션 롤백시 EntityManager.clear()로 영속성 컨텍스트를 초기화해서 잘못된 영속성 컨텍스트를 사용하는 문제 예방
- 기본 전략인 트랜잭션당 영속성 컨텍스트 전략은 문제가 발생하면
엔티티 비교
- 영속성 컨텍스트 내부에는 엔티티 인스턴스를 보관하기 위한 1차 캐시가 존재하며 이는 영속성 컨텍스트와 생명주기가 같음
영속성 컨텍스트를 통해 데이터를 저장하거나 조회하면 1차 캐시에 엔티티가 저장되고
이 1차 캐시 덕분에 변경 감지 기능도 동작하고, 데이터베이스를 통하지 않고 데이터를 바로 조회할 수 있음
또한 1차 캐시 덕분에 애플리케이션 수준의 반복 가능한 읽기가 가능해져
같은 영속성 컨텍스트에서 엔티티를 조회하면 항상 같은 엔티티 인스턴스를 반환 (주소값이 같은 인스턴스 반환)
Member member1 = em.find(Member.class, "1L");
Member member2 = em.find(Member.class, "1L");
assertTrue(member1 == member2); // 둘은 같은 인스턴스 (주소값이 같은 인스턴스)
- 영속성 컨텍스트가 같을 때 엔티티 비교
테스트는 트랜잭션 안에서 시작하므로 테스트의 범위와 트랜잭션의 범위가 같아 테스트 전체에서 같은 영속성 컨텍스트에 접근
영속성 컨텍스트가 같으면 엔티티를 비교할 때 3가지 조건을 모두 만족
- 동일성 : == 비교가 같음
- 동등성 : equals() 비교가 같음
- 데이터베이스 동등성 : @Id인 데이터베이스 식별자가 같음
// 테스트와 트랜잭션 범위 예제 코드
@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 - memberRepository에서 em.persist(member)로 회원을 영속성 컨텍스트에 저장
Long saveId = memberService.join(member);
// Then - 저장된 회원을 찾아서 저장한 회원과 비교
// 같은 트랜잭션 범위에 있으므로 같은 영속성 컨텍스트를 사용해 완전히 같은 인스턴스
Member findMember = memberRepository.fineOne(saveId);
assertTrue(member == findMember); // 참조값 비교
}
}
@Transactional
public class MemberService {
@Autowired MemberRepository memberRepository;
public Long join(Member member) {
//...
memberRepository.save(member);
return member.getld();
}
}
@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);
}
}
- 영속성 컨텍스트가 다를 때 엔티티 비교
테스트 클래스에 @Transactional이 없고 서비스에만 @Transanctional이 있으면
아래 그림와 같은 트랜잭션 범위와 영속성 컨텍스트 범위를 같게 되어 테스트가 실패함
하지만 member와 findMember는 인스턴스는 다르지만 같은 데이터베이스 로우를 가르키고 있으므로 사실상 같은 엔티티
이처럼 영속성 컨텍스트가 다르면 동일성 비교에 실패함
같은 영속성 컨텍스트를 보장하면 동일성 비교만으로 충분하므로
따라서 OSIV처럼 요청의 시작부터 끝까지 같은 영속성 컨텍스트를 사용할 때는 동일성 비교가 성공함
하지만 지금처럼 영속성 컨텍스트가 달라지면 동일성 비교는 실패하므로 엔티티의 비교에 데이터베이스 동등성 비교 방법 사용
하지만 데이터베이스 동등성 비교는 엔티티를 영속화해야 식별자를 얻을 수 있다는 문제가 있어
엔티티를 영속화하기 전에는 식별자 값이 null이므로 정확한 비교를 할 수 없음
물론 식별자 값을 직접 부여하는 방식을 사용할 때는 데이터베이스 식별자 비교도 가능하지만
항상 식별자를 먼저 부여하는 것을 보장하기는 쉽지 않음
남은 것은 equals()를 사용한 동등성 비교인데, 엔티티를 비교할 때는 비즈니스 키를 활용한 동등성 비교를 권장함
동등성 비교를 위해 equals()를 오버라이딩할 때는 비즈니스 키가 되는 필드를 선택하면 됨
비즈니스 키가 되는 필드는 보통 중복되지 않고 거의 변하지 않는 데이터베이스 키 후보들이 좋은 대상임
예) 주민등록번호
@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
/* 1-1. 테스트 코드에서 memberService.join(member)를 호출해서 회원가입을 시도하면 */
Long saveld = memberService.join(member);
// Then
/* 4. 테스트 코드에서 membmerRepository.findOne(saveId)를 호출해서 저장한 엔티티를 조회하면
리포지토리 계층에서 새로운 트랜잭션이 시작되면서 새로운 영속성 컨텍스트2가 생성됨 */
Member findMember = memberRepository.fineOne(saveld);
// findMember는 준영속 상태
// 둘은 다른 주소값을 가진 인스턴스이므로 실패
/* 9. member와 findMember는 각각 다른 영속성 컨텍스트에서 관리되었기 때문에 둘은 다른 인스턴스 */
assertTrue(member == findMember);
}
}
@Transactional // 서비스 클래스에서 트랜잭션이 시작됨
public class MemberService {
@Autowired MemberRepository memberRepository;
/* 1-2. 서비스 계층에서 트랜잭션이 시작되고 영속성 컨텍스트1이 만들어짐 */
public Long join(Member member) {
//...
memberRepository.save(member);
return member.getld();
}
/* 3. 서비스 계층이 끝날 때 트랜잭션이 커밋되면서 영속성 컨텍스트가 플러스됨
이 때 트랜잭션과 영속성 컨텍스트가 종료되므로
member 엔티티 인스턴스는 준영속 상태가 됨 */
}
@Repository
@Transactional // 예제를 구성하기 위해 추가
public class MemberRepository {
@Persistencecontext
EntityManager em;
/* 2. memberRepository에서 em.persist()를 호출해서 member 엔티티를 영속화함 */
public void save(Member member) {
em.persist(member);
}
/* 5. 저장된 회원을 조회하지만 새로 생성된 영속성 컨텍스트2에는 찾는 회원이 존재하지 않음
6. 따라서 데이터베이스에서 회원을 찾아옴
7. 데이터베이스에서 조회된 회원 엔티티를 영속성 컨텍스트에 보관하고 반환함
8. memberRepository.findOne() 메소드가 끝나면서 트랜잭션이 종료되고 영속성 컨텍스트2도 종료됨 */
public Member findOne(Long id) {
return em.find(Member.class, id);
}
}
// 데이터베이스 동등성 비교
member.getId().equals(findMember.getId()) // 데이터베이스 식별자 비교
- 즉, 동일성 비교는 같은 영속성 컨텍스트의 관리를 받는 영속 상태의 엔티티에만 적용하며
그렇지 않을 때는 비즈니스 키를 사용한 동등성 비교를 해야 함
프록시 심화 주제
- 프록시는 원본 엔티티를 상속받아서 만들어지므로
엔티티를 사용하는 클라이언트는 엔티티가 프록시인지 아니면 원본 엔티티인지 구분하지 않고 사용할 수 있음
따라서 원본 엔티티를 사용하다가 지연 로딩을 하려고 프록시로 변경해도 클라이언트의 비즈니스 로직을 수정하지 않아도 됨
하지만 프록시를 사용하는 방식의 기술적인 한계로 인해 예상하지 못한 문제들이 발생하기도 함 - 영속성 컨텍스트와 프록시
영속성 컨텍스트는 자신이 관리하는 영속 엔티티의 동일성을 보장하며 프록시로 조회한 엔티티의 동일성도 보장함
프록시를 먼저 조회하고 원본 엔티티를 조회하면 영속 엔티티의 동일성을 보장
반대로 원본 엔티티를 먼저 조회하고 나서 프록시를 조회할 경우에도 영속 엔티티의 동일성을 보장
// 영속성 컨텍스트와 프록시 예제 코드
@Test
public void 영속성컨텍스트와_프록시() {
Member newMember = new Member("member1", "회원1");
em.persist(newMember);
em.flush();
em.clear();
// member1를 em.getReference() 메소드를 사용해서 프록시로 조회 -> 프록시
Member refMember = em.getReference(Member.class, "member1");
/* 영속성 컨텍스트는 프록시로 조회된 엔티티에 대해서 같은 엔티티를 찾는 요청이 오면
원본 엔티티가 아닌 처음 조회된 프록시를 반환함 */
// member1을 em.find()를 사용해서 조회 -> 원본 엔티티가 아닌 프록시 반환
Member findMember = em.find(Member.class, "member1");
System.out.println("refMember Type = " + refMember.getClass());
// refMember Type = class jpabook.advanced.Member_$$_jvst843_0 -> 프록시
System.out.println("findMember Type = " + findMember.getClass());
// findMember Type = class jpabook.advanced.Member_$$_jvst843_0 -> 프록시
/* 프록시로 조회되므로 같은 인스턴스
즉, 프록시로 조회해도 영속성 컨텍스트는 영속 엔티티의 동일성을 보장 */
Assert.assertTrue(refMember == findMember); // 성공
}
// 원본 먼저 조회하고 나서 프록시로 조회하기 예제 코드
@Test
public void 영속성컨텍스트와_프록시2() {
Member newMember = new Member("member1", "회원1");
em.persist(newMember);
em.flush();
em.clear();
/* 원본 엔티티를 먼저 조회하면
영속성 컨텍스트는 원본 엔티티를 이미 데이터베이스에서 조회헸으므로
프록시를 반환할 이유가 없어 em.getReference()를 호출해도 프록시가 아닌 원본을 반환 */
Member findMember = em.find(Member.class, "member1");
Member refMember = em.getReference(Member.class, "member1");
System.out.println("refMember Type = " + refMember.getClass());
// refMember Type = class jpabook.advanced.Member -> 원본 엔티티
System.out.println("findMember Type = " + findMember.getClass());
// findMember Type = class jpabook.advanced.Member -> 원본 엔티티
// 영속성 컨텍스트는 영속 엔티티의 동일성을 보장
Assert.assertTrue(refMember == findMember); // 성공
}
- 프록시 타입 비교
프록시는 원본 엔티티를 상속 받아서 만들어지므로 프록시로 조회한 엔티티의 타입을 비교할 때는 instanceof를 사용해야 함
// 프록시 타입 비교 예제 코드
@Test
public void 프록시_타입비교() {
Member newMember = new Member("member1", "회원1");
em.persist(newMember);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, "member1");
System.out.println("refMember Type = " + refMember.getClass());
// refMember Type = class jpabook.advanced.Member_$$_jvsteXXX -> 프록시로 조회했으므로 프록시
// 부모 클래스와 자식 클래스를 == 비교한 것이 되므로 false
Assert.assertFalse(Member.class == refMember.getClass());
// 프록시는 원본 엔티티의 자식 타입이므로 instanceof 연산을 사용해 타입을 비교하면 true
Assert.assertTrue(refMember instanceof Member);
}
- 프록시 동등성 비교
엔티티의 동등성을 비교하려면 비즈니스 키를 사용해서 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; }
/* name이 중복되는 회원이 없다고 가정하고
name 필드를 비즈니스 키로 사용해서 equals() 메소드를 오버라이딩 */
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
// 프록시의 타입 비교는 == 비교 대신 instanceof를 사용
/* 타입을 동등성(==) 비교하는데 프록시는 원본을 상속받는 자식 타입이므로
프록시의 타입을 비교할 때는 == 비교가 아닌 instanceof를 사용해야 함
그러므로 다음처럼 변경해야 함
if(!(obj instanceof Member)) return false; */
if (this.getClass() != obj.getClass()) return false;
Member member = (Member) obj; // member는 프록시
// 프록시의 멤버변수에 직접 접근하면 안 되고 대신 접근자 메소드를 사용해야 함
/* member.name을 보면 프록시의 멤버변수에 직접 접근하는데
equals() 메소드를 구현할 때는 일반적으로 멤버변수를 직접 비교하는데, 프록시의 경우는 문제가 됨
프록시는 실제 데이터를 가지고 있지 않으므로 프록시의 멤버변수에 직접 접근하면 아무값도 조회 불가능
따라서 member.name의 결과는 null이 반환되고 equals()는 false를 반환
name 멤버변수가 private이므로 일반적인 상황에서는 프록시의 멤버변수에 직접 접근하는 문제가 발생하지 않지만
equals() 메소드는 자신을 비교하기 때문에 private 멤버변수에도 접근할 수 있음
그렇기 때문에 프록시의 데이터를 조회할 때는 접근자를 사용하도록 해야 함
그러므로 다음처럼 변경해야 함
if (name != null ? !name.equals(member.getName()) : member.getName() != null) */
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
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");
/* 새로 생성한 newMember와 프록시로 조회한 회원 refMember의 name 속성은
둘 다 회원1로 같지만 동등성 비교시 false
반면 프록시가 아닌 원본 엔티티를 조회해서 비교하면 true */
Assert.assertTrue(newMember.equals(refMember));
}
// 프록시 동등성 비교 예제, 수정
@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 instanceof Member)) return false;
Member member = (Member) obj;
if (name != null ? !name.equals(member.getName()) : member.getName() != null)
return false;
return true;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
}
// 프록시 동등성 비교, 실행
@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)); // true
}
- 상속관계와 프록시의 문제점
상속관계를 프록시로 조회할 때 발생할 수 있는 문제점으로는 프록시를 부모 타입으로 조회하면 문제가 발생하는 것
즉, 프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성되는 문제가 발생하기 때문에
instanceof 연산을 사용할 수 없고 하위 타입으로 다운캐스팅을 할 수 없음
프록시를 부모 타입으로 조회하는 문제는 특히 다형성을 다루는 도메인 모델에서 나타남
// 프록시 부모 타입으로 조회
@Test
public void 부모타입으로_프록시조회() {
// 테스트 데이터 준비
Book saveBook = new Book();
saveBook.setName("jpabook");
saveBook.sestAuthor("kim");
em.persist(saveBook);
em.flush();
em.clear();
// 테스트 시작
/* 실제 조회된 엔티티는 Book이므로 Book 타입을 기반으로 원본 엔티티 인스턴스가 생성됨
그런데 em.getReference() 메소드에서 Item 엔티티를 대상으로 조회했으므로
프록시인 proxyItem은 Item 타입을 기반으로 만들어지며
이 프록시 클래스는 원본 엔티티로 Book 엔티티를 참조
그 결과 proxyItem이 Book이 아닌 Item 클래스를 기반으로 만들어져 false를 반환함
즉, proxyItem은 Item$Proxy 타입이고 이 타입은 Book 타입과 관계가 없음
따라서 직접 다운캐스팅을 해도 proxyItem은 Book 타입이 아닌
Item 타입을 기반으로 한 Item$Proxy 타입이므로 ClassCastException 예외가 발생 */
Item proxyItem = em.getReference(Item.class, saveBook.getId()); // Item 엔티티를 프록시로 조회
System.out.println("proxyItem = " + proxyItem.getClass());
// proxyItem = class jpabook.proxy.advanced.item.Item_$$_jvstXXX
// instanceof 연산을 사용해서 proxyItem이 Book 클래스 타입인지 조회
if (proxyItem instanceof Book) {
// Book 타입이면 다운캐스팅해서 Book 타입으로 변경하고 저자 이름을 출력
System.out.println("proxyItem instanceof Book");
Book book = (Book) proxyItem;
// 하지만 여기서는 저자가 출력되지 않는 문제 발생 -> Book 타입이 아니라 Item$Proxy 타입이라서
System.out.println("책 저자 = " + book.getAuthor());
}
// 결과 검증
Assert.assertFalse(proxyItem.getClass() == Book.class); // false 반환
Assert.assertFalse(proxyItem instanceof Book); // false 반환
Assert.assertTrue(proxyItem instnaceof Item); // true 반환
}
// 다형성과 프록시 조회 정의
@Entity
public class OrderItem {
@Id @GanaratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // 지연로딩으로 설정해서 item이 프록시로 조회됨
@JoinColumn(name = "ITEM_ID")
private Item item;
public Item getItem() {
return item;
}
public void setItem(Item item) {
this.item = item;
}
...
}
// 다형성과 프록시 조회 실행
@Test
public void 상속관계와_프록시_도메인모델() {
// 테스트 데이터 준비
Book book = new Book();
book.setName("jpabook");
book.setAuthor("kim");
em.persist(book);
OrderItem saveOrderItem = new OrderItem();
saveOrderItem.setItem(book);
em.persist(saveOrderItem);
em.flush();
em.clear();
// 테스트 시작
OrderItem orderItem = em.find(OrderItem.class, saveOrderItem.getId());
// 지연 로딩으로 인해 item이 프록시로 조회
Item item = orderItem.getItem();
System.out.println("item = " + item.getClass());
// item = class jpabook.proxy.advanced.item.Item_$$_jvstffa_3
// 결과 검증
Assert.assertFalse(item.getClass() == Book.class); // false 반환
Assert.assertFalse(item instanceof Book); // false 반환
Assert.assertTrue(item instanceof Item); // true 반환
}
- 상속관계와 프록시의 해결방안 - (1) JPQL로 대상 직접 조회
가장 간단한 해결 방법은 처음부터 자식 타입을 직접 조회해서 필요한 연산을 하면 되며 이 방법을 통해 다형성을 활용할 수 있음
Book jpqlBook = em.createQuery("select b from Book b where b.id=:bookId", Book.class)
.setParameter("bookId", item.getId())
.getSingleResult();
- 상속관계와 프록시의 해결방안 - (2) 프록시 벗기기
하이버네이트가 제공하는 기능을 사용해 프록시에서 원본 엔티티를 가져올 수 있음
// 프록시 벗기기 예제
...
Item item = orderItem.getItem();
// 프록시에서 원본 엔티티를 직접 꺼냄
// 원본 엔티티가 꼭 필요한 곳에서만 잠깐 사용하도록 함
Item unProxyItem = unProxy(item);
if (unProxyItem instanceof Book) {
System.out.println("proxyItem instance of Book");
// proxyItem instance of Book
Book book = (Book) unProxyItem;
System.out.println("책 저자 = " + book.getAuthor());
// 책 저자 = shk
}
Assert.assertTrue(item != unProxyItem); // true
}
// 하이버네이트가 제공하는 프록시에서 원본 엔티티를 찾는 기능을 사용하는 메소드
public static<T> T unProxy(Object entity) {
if (entity instanceof HibernateProxy) {
entity = ((HibernamteProxy) entity).getHibernateLazyInitializer().getImplementation();
}
return (T) entity;
}
- 상속관계와 프록시의 해결방안 - (3) 기능을 위한 별도의 인터페이스 제공
특정 기능을 제공하는 인터페이스를 사용
인터페이스를 제공하고 각각의 클래스가 자신에 맞는 기능을 구현하는 것으로 다형성을 화용하는 좋은 방법
TitleView라는 공통 인터페이스를 만들고 자식 클래스들은 인터페이스의 getTitle() 메소드를 각각 구현
다양한 상품 타입이 추가되어도 Item을 사용하는 OrderItem의 코드는 수정하지 않아도 되며
클라이언트 입장에서 대상 객체가 프록시인지 아닌지를 고민하지 않아도 되는 장점이 존재
이 방법을 사용할 대는 프록시의 특징 때문에 프록시의 대상이 되는 타입에 인터페이스를 적용해야하므로
Item이 프록시의 대상이므로 Item이 인터페이스를 받아야 함
// 프록시 인터페이스 제공 정의
public interface TitleView {
String getTitle();
}
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item implements TitleView {
@Id @GaneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQunatity;
// Getter, Setter
}
@Entity
@DiscriminatorColumn(name = "B")
public class Book extends Item {
private String author;
private String isbn;
@Override
public String getTitle() {
return "[제목:" + getName() + " 저자: " + author + "]";
}
}
@Entity
@DiscriminatorColumn(name = "M")
public class Movie extends Item {
private String director;
private String actor;
@Override
public String getTitle() {
return "[제목:" + getName() + " 감독: " + director + " 배우:" + actor + "]";
}
}
// 프록시 인터페이스 제공 사용 1
@Entity
public class OrderItem {
@Id @GaneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ITEM_ID")
private Item item;
...
public void printItem() {
System.out.println("TITLE=" + item.getTitle());
}
}
// 프록시 인터페이스 제공 사용 2
// Item의 구현체에 따라 각각 다른 getTitle() 메소드가 호출됨
OrderItem orderItem = em.find(OrderItem.class, saveOrderItem.getId());
// Book을 조회했으면 TITLE=[제목:jpabook 저자:kim] 출력
orderItem.printItem();
- 상속관계와 프록시의 해결방안 - (4) 비지터 패턴 사용
비지터 패턴은 Visitor와 Visitor를 받아들이는 대상 클래스로 구성됨
비지터 패턴을 사용하면 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근할 수 있고
instanceof나 타입캐스팅 없이 코드를 구현할 수 있는 장점이 존재
또한 알고리즘과 객체 구조를 분리해서 구조를 수정하지 않고 새로운 동작을 추가할 수 있음
반면 너무 복잡하고 더블 디스패치를 사용하기 때문에 이해하기 어려우며
객체 구조가 변경되면 모든 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);
}
// 비지터 구현
// Visitor의 구현 클래스로 대상 클래스의 내용을 출력해주는 PrintVisitor
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.getAuthor() + "]");
}
@Override
public void visit(Album album) { ... }
@Override
public void visit(Movie movie) { ... }
}
// 대상 클래스의 제목을 보관하는 TitleVisitor
public class TitleVisitor implement 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) { ... }
}
// 비지터 대상 클래스
// Item에 Visitor를 받아들일 수 있도록 accept(visitor) 메소드를 추가
/* 단순히 파라미터로 넘어온 Visitor의 visit(this) 메소드를 호출하면서
자신(this)을 파라미터로 넘기는 것이 전부이므로 실제 로직 처리를 visitor로 위임함 */
@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
@DiscriminatorColumn(name = "B")
public class Book extends Item {
private String author;
private String isbn;
// Getter, Setter
public String getAuthor() {
return author;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this); // this는 프록시가 아닌 원본임
}
}
@Entity
@DiscriminatorColumn(name = "A")
public class Album extends Item {
...
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
@Entity
@DiscriminatorColumn(name = "M")
public class Movie extends Item {
...
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
// 비지터 사용 코드
@Test
public void 상속관계_프록시_VisitorPattern() {
...
OrderItem orderItem = em.find(OrderItem.class, orderItemId);
Item item = orderItem.getItem();
// PrintVisitor
/* item.accept() 메소드를 호출하면서 파라미터로 PrintVisitor를 넘겨주고
item은 프록시이므로 먼저 프록시(ProxyItem)가 accept() 메소드를 받고
원본 엔티티(book)의 accept()를 실행해서 자신(this)을 visitor에 파라미터로 넘겨줌
visitor가 PrintVisitor타입이므로 PrintVisitor.visit(this) 메소드가 실행되는데
이때 this가 Book 타입이므로 visit(Book book) 메소드가 실행됨 */
item.accept(new PrintVisitor());
}
// 출력 결과
book.class = class.jpabook.advanced.item.Book
[PrintVisitor] [제목:jpabook 저자:kim]
// 비지터 패턴과 확장성
// TitleVisitor를 사용해보기
TitleVisitor titleVisitor = new TitleVisitor();
item.accept(titleVisitor);
String title = titleVisitor.getTitle();
System.out.println("TITLE=" + title);
// 출력 결과
book.class = class jpabook.advanced.item.Book
Title=[제목:jpabook 저자:kim]
'Java-Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 트랜잭션과 락, 2차 캐시 (0) | 2022.06.10 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 고급 주제와 성능 최적화 ② (0) | 2022.06.07 |
[자바 ORM 표준 JPA 프로그래밍] 컬렉션과 부가 기능 (0) | 2022.05.27 |
[자바 ORM 표준 JPA 프로그래밍] 웹 애플리케이션과 영속성 관리 (0) | 2022.05.26 |
[자바 ORM 표준 JPA 프로그래밍] 스프링 데이터 JPA ③ (0) | 2022.05.18 |