- 연관관계가 있는 엔티티를 조회 시에 상황에 따라 한번에 연관관계 데이터까지 가져오거나 조회를 원한 특정 엔티티만 조회하고 싶은 경우가 있다.
- 회원과 팀이 연관관계가 있는 경우 회원 정보를 조회하면서 회원이 속한 팀의 정보도 함께 조회하고 싶은 경우
- 회원과 팀이 연관관계가 있는 경우 회원 정보만 조회하고 팀의 정보는 조회하고 싶지 않은 경우
- JPA는 이러한 상황을 해결하기 위해 프록시와 지연로딩을 통해 해결해준다.
프록시 기초
- em.getReference() : 데이터베이스 조회를 미루는 프록시(가짜) 엔티티 객체를 가져온다. 따라서 쿼리가 실행되지 않는다.
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
// 결과 : findMember = class com.hello.Member$HibernateProxy$ywht19mS
- hibernate 내부의 라이브러리를 사용해 실제 DB에서 조회한 엔티티를 반환하는 것이 아닌 프록시 엔티티 객체를 반환한다.
- 껍데기는 동일하지만 내부에는 target이 진짜 엔티티 객체를 가리키고 있다.
- 실제 엔티티를 상속받아서 사용하기 때문에 실제 엔티티와 프록시의 겉모양은 동일하다.
- 사용자의 입장에서는 프록시 객체인지 실제 객체인지 몰라도 사용이 가능하다.
- 프록시 객체는
Entity target
으로 실제 객체의 참조를 가지고 있다.
- 프록시 객체의 초기화
- getReference() 까지 한 경우 프록시 객체의 target에는 null이다
- getName()을 통해 프록시 객체로 조회를 시도하는 경우
- 데이터 조회
- 프록시 객체가 영속성 컨텍스트에 초기화를 요청, 프록시에 값이 없는 경우 진짜 값을 요청한다.
- 영속성 컨텍스트는 DB를 조회
- 실제 엔티티를 생성
- 프록시 객체의 타겟과 실제 엔티티 매핑
프록시 특징
- 프록시 객체는 처음 사용할 때 (내부의 target이 실제 엔티티의 래퍼런스를 가지고 있지 않은 경우) 한 번 초기화를 한다.
Member findMember = em.getReference(Member.class, member.getId());
//해당 시점에 영속성 컨텍스트에 초기화 요청
System.out.println("findMember.getUsername() = " + findMember.getUsername());
//두번째 요청의 경우 영속성 컨텍스트가 초기화를 했기 때문에 쿼리가 한번더 요청되지 않는다.
System.out.println("findMember.getUsername() = " + findMember.getUsername());
- 프록시 객체가 실제 엔티티 객체로 변환되는 것이 아니다!
findMember.getClass(); // 초기화 이전 -> Proxy (target = null)
findMember.getUsername(); // 영속성 컨텍스트에 초기화 요청 -> proxy가 실제 엔티티 객체 호출
findMember.getClass(); // 초기화 이후 -> Proxy (target = 실제 엔티티 래퍼런스)
- 프록시 객체는 원본 엔티티를 상속받기 때문에 타입 체크시 instance of를 사용한다.
Member findMember1 = em.find(Member.class, member1.getId());
Member findMember2 = em.getReference(Member.class, member2.getId());
// false
System.out.println("(member1.getClass() == member2.getClass()) = " + (findMember1.getClass() == findMember2.getClass()));
// true
System.out.println("member1 instanceof Member = " + (findMember1 instanceof Member));
System.out.println("member2 instanceof Member = " + (findMember2 instanceof Member));
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
- 영속성 컨텍스트 1차 캐시에 있는데 굳이 프록시를 가져올 필요가 없다.
- 한 트랜잭션 내에서 실제 엔티티, 프록시 상관없이 한 영속성 컨텍스트 내에서 동일한 PK를 가지는 경우
JPA는 “==”이 true임을 보장한다.
// 영속성 컨텍스트에 올라간 상태
Member findMember1 = em.find(Member.class, member1.getId());
System.out.println("findMember1.getClass() = " + findMember1.getClass());
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference.getClass() = " + reference.getClass());
// true (실제 엔티티 클래스 반환)
System.out.println("(findMember1 == reference) = " + (findMember1 == reference));
== 반대 경우 ==
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember.getClass() = " + refMember.getClass());
Member findMember1 = em.find(Member.class, member1.getId());
System.out.println("findMember1.getClass() = " + findMember1.getClass());
// true (프록시 반환)
System.out.println("(refMember == findMember1) = " + (refMember == findMember1));
- 준영속 상태인 경우 프록시를 초기화하면 예외가 발생한다. (LazyInitializationException)
- 트랜잭션과 영속성 컨텍스트 라이프 사이클을 일반적으로 맞추는데 트랜잭션이 끝나고 프록시를 초기화하는 경우 예외발생
// 영속성 컨텍스트에 올라간 상태
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember.getClass() = " + refMember.getClass()); // 프록시
em.detach(refMember); // 영속성 컨텍스트에서 꺼냄
// 영속성 컨텍스트를 이용해 프록시를 초기화해야하는데 영속성 컨텍스트에서 꺼낸 상태므로 예외
System.out.println("refMember.getUsername() = " + refMember.getUsername());
- 프록시를 초기화 했는지 확인하는 메서드 :
emf.getPersistenceUnitUtil().isLoaded(refMember)
- Hibernate 프록시 강제 초기화 메서드 :
Hibernate.initialize();
📃 References
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 : https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
반응형
'Backend > JPA' 카테고리의 다른 글
영속성 전이 (Cascade), 고아 객체 (0) | 2022.07.21 |
---|---|
즉시 로딩과 지연 로딩 (0) | 2022.07.21 |
상속관계 매핑 (0) | 2022.07.20 |
엔티티 설계의 주의사항 간단 정리 (0) | 2022.07.05 |
다양한 연관관계 매핑 (0) | 2022.05.02 |