Case1) Member와 Team을 모두 조회하고 싶다.
public void printMemberAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
System.out.println("소속팀: " + team.getName());
}
Case2) Member만 조회하고 싶다.
public void printUser(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
}
이 경우 Team
의 정보를 매번 가져오면 리소스가 낭비된다.
-> 이를 해결하기 위해 프록시를 이해해야 한다.
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
//
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
tx.commit();
find()
만 했음에도 select Query
가 나가는 것을 확인할 수 있다.
em.find()
대신 em.getReference()
를 이용하면 어떤 결과가 나올까?
Member findMember = em.getReference(Member.class, member.getId());
Hibernate:
call next value for hibernate_sequence
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(createdBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID)
values
(?, ?, ?, ?, ?, ?)
em.find()
와 같이 insert Query
만 나간다.
select Query
는 나가지 않는다.
-> em.getReference()
이후
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
코드를 실행하면
findMember.id = 1
/*
/select 쿼리가 나간다.
/*
getReference
코드를 실행할 때 member.getId()
를 파라미터로 이용했기 때문에 Id
는 바로 출력이 된다.
이후 findMember
객체가 가지고있지 않은 정보인 Username
을 호출할 때 select Query
가 나가게 된다.
System.out.println("회원 이름: " + member.getClass());
Class를 조회해보면
findMember = class hellojpa.Member$HibernateProxy$ocdBHpyj
Proxy
타입인 것을 확인할 수 있다.
em.getReference
는 프록시를 사용한다.Question)
getId()를 호출하면 프록시는 target에 있는 getId()를 호출한다.
하지만 처음에는 DB 조회가 되지 않은 상태이므로 target이 비어있을 것이다. 이 때 무슨 일이 벌어질까?
public void getMemberName() {
// 프록시 객체를 가져온다.
Member member = em.getReference(Member.class, "id");
// 1. 프록시에 getName()메서드를 요청한다.
// 2. 프록시가 영속성 컨텍스트에 초기화를 요청한다.
// 3. 영속성 컨텍스트가 DB를 조회한다.
// 4. 영속성 컨텍스트가 실제 Entity를 생성한다.
// 5. target의 getName()메서드가 실행된다.
member.getName();
}
1. 프록시 객체는 처음 사용할 때 한 번만 초기화
public void printUserAndTeam(String memberId) {
Member member = em.getReference(Member.class, memberId);
System.out.println("회원 이름: " + member.getUsername());
System.out.println("회원 이름: " + member.getUsername());
System.out.println("회원 이름: " + member.getUsername());
}
처음 member.getUsername())
코드 실행 시 select Query
가 나가고
그 이후는 Entity에 정보가 담겨있으므로 별도의 Query없이 바로 값이 출력된다.
2. 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능하다.
public void printUserAndTeam(String memberId) {
Member member = em.getReference(Member.class, memberId);
System.out.println("before: " + member.getClass());
System.out.println("회원 이름: " + member.getUsername());
System.out.println("after: " + member.getClass()); // before와 같은 값 출력
}
select Query
가 나가면서 프록시가 실제 Entity로 바뀌는 것은 아니다.
초기화가 되면 프록시 객체를 통해 실제 Entity에 접근 할 수 있는 것 뿐이다.
Class의 값은 Proxy로 동일하다.
3. 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야한다. (==비교 실패, 대신 instance of 사용)
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.find(Member.class, member2.getId());
// find()로 가져왔고 타입을 정확하게 비교하는 것이므로 true를 출력한다.
System.out.println("m1 == m2: " + (m1.getClass() == m2.getClass()));
Member m3 = em.getReference(Member.class, member1.getId());
Member m4 = em.find(Member.class, member2.getId());
// m3는 getReference()이므로 false를 출력한다.
System.out.println("m1 == m2: " + (m1.getClass() == m2.getClass()));
tx.commit();
em.find
는 Entity, em.getReference
는 Proxy를 반환하므로 위 예제는 쉽게 이해할 수 있다.
하지만 비즈니스에서는 비교하는 로직을 Class를 만들어서 이용하기 때문에
logic(member1, member2)
와 같은 코드를 보고 쉽게 판단할 수 없다.
따라서 ==
대신 instance of
를 사용하자.
4. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
public void printUserAndTeam(String memberId) {
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member m1 = em.find(Member.class, member1.getId());
// find()로 진짜 객체를 가져오기 때문에 'Member'라고 출력된다.
System.out.println("m1 = " + m1.getClass());
// find()후 getReference()
Member m2 = em.getReference(Member.class, member1.getId());
// 'Proxy'가 아니라 'Member'로 출력된다.
System.out.println("m2 = " + m2.getClass());
// 프록시든 아니면 한 영속성 컨텍스트에서 가져오고 PK가 같다면 항상 true가 된다.
System.out.println("m1 == reference = " + (m1 == reference));
tx.commit();
}
m1 : member1을 em.find()
를 통해 호출
m2 : 호출되어 영속성 컨텍스트에 존재하즞 member1을 em.getReference()
를 통해 호출
-> getReference()
는 프록시를 반환하지만, 찾는 엔티티가 이미 영속성 컨텍스트에 존재하면 프록시가 아닌 실제 엔티티를 반환한다.
또한 원본과 레퍼런스를 ==
비교 시 항상 true
가 반환된다.
원본과 레퍼런스를 ==
비교 시 항상 true
가 반환되는 것이 매우 중요하다.
em.find()
후 em.find()
-> Member
, Member
반환
em.find()
후 em.getReference()
-> Member
, Member
반환 (영속성 컨텍스트에 찾는 엔티티가 이미 있으므로 실제 엔티티 반환)
em.getReference()
후 em.getReference()
-> Proxy
, Proxy
반환
em.getReference()
후 em.find()
-> Proxy
, Proxy
반환
항상 ==
비교 시 true
를 보장해줘야한다.
따라서, 레퍼런스를 얻고 em.getfind()
코드가 실행될 때 select Query
가 작동하지만 결국 Proxy
를 반환하는 것이다.
5. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
public void printUserAndTeam(String memberId) {
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
// 프록시 생성
Member refMember = em.getReference(Member.class, member1.getId());
// detach(), clear(), close()로 영속성 컨텍스트를 준영속으로 만든다.
em.close();
// 실제 데이터로 초기화 하면서 데이터를 가져와야 하지만
// 영속성 컨텍스트로 관리하지 않게 되면서 exception이 떨어진다.
refMember.getUsername();
System.out.println("refMember = " + refMember.getClass());
tx.commit();
}
예외 발생 시 getstacktrace()
를 통해 예외를 출력하는 코드 또한 작성했다고 가정한다.
위 코드에서 refMember.getUsername()
이 실행될 때 프록시가 초기화 되어야한다.
초기화가 될 때는 영속성 컨텍스트를 통해 실제 Entity를 만들어낸다.
준영속 상태일 때 영속성 컨텍스트의 도움을 받을 수 없기 때문에 예외가 발생한다.
org.hibernate.LazyInitializationException: could not initialize proxy [hellojpa.Member#1]
1. 프록시 인스턴스의 초기화 여부 확인
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory();
...
// 앞에서 초기화 했다면 true, 아니라면 false
System.out.println("isLoaded: " + emf.getPersistenceUnitUtil().isLoaded(refMember));
}
2. 프록시 클래스 확인 방법
entity.getClass().getName()
3. 프록시 강제 초기화
public void printUserAndTeam(String memberId) {
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());
// 이렇게 강제 호출하지말고
refMember.getUsername();
// initialize 메서드를 사용하자
Hibernate.initialize(refMember);
tx.commit();
}
org.hibernate.Hibernate.initialize(entity)
member.getName()
처럼 강제로 호출 해야 한다.참고 :
김영한. 『자바 ORM 표준 JPA 프로그래밍』. 에이콘, 2015.