JPA - 프록시
1. EntityManager.find() vs EntityManager.getReference()
이하 EntityManager = em
JPA에는 em.find 말고도 em.getReference라는 이름 그대로 참조를 가져오는 메서드가 제공된다.
em.find는 데이터베이스를 통해서 실제 엔티티 객체를 바로 조회하고
em.getReference는 데이터베이스 조회를 미루는 가짜 엔티티 객체를 조회한다.
결론적으로 DB의 쿼리가 안나가는데 객체가 조회가 된다.
1) em.find()
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 = " + findMember.getId());
System.out.println("findMember = " + findMember.getUsername());
em.find() 시점에서 실제 데이터를 불러오는 쿼리를 날린다.
Hibernate:
select
m1_0.member_id,
m1_0.created_by,
m1_0.created_date,
m1_0.last_modified_by,
m1_0.last_modified_date,
t1_0.team_id,
t1_0.created_by,
t1_0.created_date,
t1_0.last_modified_by,
t1_0.last_modified_date,
t1_0.name,
m1_0.username
from
member m1_0
left join
team t1_0
on t1_0.team_id=m1_0.team_id
where
m1_0.member_id=?
findMember = 1
findMember = hello
2) em.getReference()
위 코드에서 em.find() 메서드만 em. getReference() 로 변경했다.
Member findMember = em.getReference(Member.class, member.getId());
getId() 부분까지는 이미 엔티티를 조회하는 시점에서 getId() 값을 가지고 있기 때문에 조회하지 않다가,
실제 데이터의 값이 필요하게 되는 getUserName() 시점에 조회쿼리를 날려 값을 조회한다.
findMember.getId() = 1
Hibernate:
select
m1_0.member_id,
m1_0.city,
m1_0.street,
m1_0.zipc5de,
m1_0.username,
m1_0.work_city,
m1_0.work_street,
m1_0.work_zipcode,
m1_0.end_date,
m1_0.start_date
from
member m1_0
where
m1_0.member_id=?
findMember.getUsername() = hello
그렇다면 getReference()의 Member 객체는 뭘까?
아래 쿼리를 추가하여 실행하면,
System.out.println("findMember = " + findMember.getClass());
진짜 Member객체가 아닌, 프록시 객체라는것을 알 수 있다.
findMember = class com.example.jpa.domain.Member$HibernateProxy$gPJPsc5E
이 프록시 객체의 내부에는 target이라는 것이 있는데, 이것이 진짜 엔티티를 가르킨다.
초기에는 껍데기만 있고, ID값만 들고있는 가짜가 반환된다.

2. 프록시 특징

엔티티의 프록시 객체는 어떻게 생겼냐 하면, 프록시 객체는 실제 객체의 taget이라는 참조를 보관한다.
예를들어, Member엔티티 프록시 객체에서 getName을 호출하면,
프록시 객체는 실제 target에 있는 엔티티의 getName을 대신 호출한다.
그런데 처음에는 타겟이 없다.
왜냐하면 이걸 실제 DB에서 조회한 적이 없기 때문이다.
프록시의 특징은 실제 객체 엔티티를 상속받아서 만들어진다.
그래서 실제 클래스와 겉모양이 같다.
이는 하이버네이트가 내부적으로 여러 프록시 라이브러리를 이용해 만든다.
정리
- 실제 클래스를 상속 받아서 만들어짐
- 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)
- 프록시 객체는 실제 객체의 참조(target)을 보관
- 프록시 객체를 호출하면 객체는 실제 객체의 메소드 호출
3. 프록시 객체의 초기화
1) 프록시 객체가 초기화되는 과정

프록시 객체를 getReference로 가져왔을 때, member.GetName을 호출하면,
target에는 Member의 값이 처음에는 없었다가, JPA가 영속성 컨텍스트에 Member 값을 요청한다.
그러면 영속성 컨텍스트가 DB를 조회해서 실제 엔티티 객체를 생성한다. 그리고 프록시 객체의 target에 연결을 시켜준다.
즉, 프록시 내부에는 Member target 이라는 진짜 객체 변수가 있다고 보면 된다.
그래서 프록시 객체에서 getName을 했을 때, target의 getName을 통해 실제 객체에 있는 getName이 반환된다.
2) 한번 초기화된 프록시는 다시 DB를 조회할 필요가 없다.
영속성 컨텍스트에서 초기화를 요청하는 부분이 중요하다.
프록시에 값이 없을 때 진짜 값을 달라고 요청하는 것이다.
DB를 통해서 진짜 값을 가지고 와서 진짜 엔티티를 만드는 과정을 의미한다.
그리고 이것이 초기화를 의미한다.
그리고 한번 초기화 되면 target에 객체 값이 걸리기 때문에 다시 DB를 조회할 일은 없다.
그래서 getName을 2번 요청하더라도 쿼리는 한번만 나간다.
em.getReference()로 userName 조회 시 쿼리
Hibernate:
select
m1_0.member_id,
m1_0.created_by,
m1_0.created_date,
m1_0.last_modified_by,
m1_0.last_modified_date,
t1_0.team_id,
t1_0.created_by,
t1_0.created_date,
t1_0.last_modified_by,
t1_0.last_modified_date,
t1_0.name,
m1_0.username
from
member m1_0
left join
team t1_0
on t1_0.team_id=m1_0.team_id
where
m1_0.member_id=?
findMember.getUsername() = hello
findMember.getUsername() = hello
3) 실제 객체와 프록시 객체의 타입 비교
또한 타입비교를 할 때 == 비교를 사용하지 말고 instance of를 사용해야한다.
왜냐하면 같은 객체끼리의 비교는 괜찮지만, em.find()와, em.getReference()를 == 비교한다면 false가 나올 수 있기 때문이다.
Member member1 = new Member();
member1.setUsername("hello1");
em.persist(member1);
Member member2 = new Member();
member2.setUsername("hello2");
em.persist(member2);
em.flush();
em.clear();
Member findMember1 = em.find(Member.class, member1.getId());
Member findMember2 = em.getReference(Member.class, member2.getId());
System.out.println("findMember1.getClass() = " + findMember1.getClass());
System.out.println("findMember2.getClass() = " + findMember2.getClass());
System.out.println("findMember1 == findMember2 : " + (findMember1.getClass() == findMember2.getClass()));
이 경우엔 실제 객체와 프록시 객체가 비교되므로 false가 나온다.
== 연산자는 객체의 참조를 비교한다. 즉, 두 객체가 메모리 상에서 동일한 위치에 있는지를 확인하는 것이다.
그렇기 때문에, 실제 엔티티 객체와 그 엔티티의 프록시 객체가 서로 다른 메모리 위치에 있다면
== 연산자로 비교했을 때 결과는 false가 된다.
findMember1.getClass() = class com.example.jpa.domain.Member
findMember2.getClass() = class com.example.jpa.domain.Member$HibernateProxy$pNqeG26x
findMember1 == findMember2 : false
4) 영속성 컨텍스트에 엔티티가 이미 있을 때 em.getReference()
같은 id를 참조하는 경우, 이미 find() 하여 1차 캐시에 엔티티가 저장되어있는 상태라면, getReference() 하더라도 프록시가 아닌, 실제 객체를 반환한다.
Member member1 = new Member();
member1.setUsername("hello1");
em.persist(member1);
em.flush();
em.clear();
Member findMember1 = em.find(Member.class, member1.getId());
System.out.println("findMember1.getClass() = " + findMember1.getClass());
Member findMember1Refer = em.getReference(Member.class, member1.getId());
System.out.println("findMember1Refer.getClass() = " + findMember1Refer.getClass());
첫번째 이유는 1차캐시에 이미 실제 엔티티가 존재하기 때문이고,
두번째 이유는 JPA는 같은 엔티티의 같은 id라면 라면 항상 == 비교를 true를 보장하기 때문이다.
findMember1.getClass() = class com.example.jpa.domain.Member
findMember1Refer.getClass() = class com.example.jpa.domain.Member
JPA는 같은 엔티티와 id라면 항상 == 비교가 참이 되도록 보장해주기 때문에, getReference()후 find()를 하게 되면
둘 다 프록시 객체로 조회된다.
Member member1 = new Member();
member1.setUsername("hello1");
em.persist(member1);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember.getClass() = " + refMember.getClass());
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember.getClass() = " + findMember.getClass());
System.out.println("refMember == findMember : " + (refMember == findMember));
그렇기에 재미있게도 refMember.getClass()가 호출되기 직전에 조회쿼리를 호출하지만, 프록시 객체로 조회되는것을 볼 수 있다.
refMember.getClass() = class com.example.jpa.domain.Member$HibernateProxy$a1lJoMHB
Hibernate:
select
m1_0.member_id,
m1_0.created_by,
m1_0.created_date,
m1_0.last_modified_by,
m1_0.last_modified_date,
t1_0.team_id,
t1_0.created_by,
t1_0.created_date,
t1_0.last_modified_by,
t1_0.last_modified_date,
t1_0.name,
m1_0.username
from
member m1_0
left join
team t1_0
on t1_0.team_id=m1_0.team_id
where
m1_0.member_id=?
findMember.getClass() = class com.example.jpa.domain.Member$HibernateProxy$a1lJoMHB
refMember == findMember : true
5) hibernate.LazyInitializationException - 영속성 컨텍스트에 값이 없거나 컨텍스트 자체가 닫힌 경우
프록시 객체에서 username을 호출하면 프록시 객체에서 영속성 컨텍스트에게 실제객체를 호출해달라고 요청하는데,
영속성 컨텍스트에게 호출하기 전에 준영속을 시켜버리면 영속성 컨텍스트에는 캐시가 없기 때문에
JPA에서는 실제 엔티티를 불러올수 없게 된다. close(), clear() 모두 동일하다.
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember.getClass() = " + refMember.getClass());
em.detach(refMember);
refMember.getUsername();
Hibernate:
/* insert for
com.example.jpa.domain.Member */insert
into
member (created_by,created_date,last_modified_by,last_modified_date,username,member_id)
values
(?,?,?,?,?,?)
refMember.getClass() = class com.example.jpa.domain.Member$HibernateProxy$PJu5GIAI
could not initialize proxy [com.example.jpa.domain.Member#1] - no Session
org.hibernate.LazyInitializationException: could not initialize proxy [com.example.jpa.domain.Member#1] - no Session
정리
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
- 프록시 객체는 원본 엔티티를 상속받음. 따라서 타입 체크 시 주의 필요 (== 비교 대신, instance of 사용)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference() 를 호출해도 실제 엔티티를 반환
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시 초기화하면 문제 발생(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)