1. 프록시
프록시는 실제 엔티티 대신에 사용되는 객체로서, 원본 엔티티를 상속받습니다. 이제부터 프록시를 왜 사용하는지에 대해서 알아보도록 하겠습니다.
다음과 같은 Member Entity와 Team Entity가 있을때 Member를 조회할때 Team도 같이 가지고 올수도 있고, Member만 가지고 올 수 있습니다.
- em.find() vs em.getReference()
- em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
em.find는 진짜 객체를 찾아줍니다. 결론적으로는 em.getReference는 DB에 쿼리가 안나가는데 객체가 조회가 됩니다.
getReference를 하는 시점에는 데이터베이스를 호출하지 않습니다. 그런데 이 값이 실제로 사용되는 시점에 데이터베이스에 쿼리를 날립니다.
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("findMember.username = " + findMember.getUserName());
class를 출력해보면 class hellojpa.Member$HibernateProxy$odcVHpjy 이런식으로 나오는걸 확인할 수 있습니다. 바로 하이버네이트가 만든 가짜 클래스 라는 것 입니다. 이게 프록시 클래스 라는 것 입니다.
껍데기는 똑같은데 안에는 비어있습니다. 그리고 내부에는 target이 있는데 이게 진짜 레퍼런스를 가르킵니다. 초기에는 비어있습니다. getName을 호출하면 실제 target에 있는 getName을 대신 호출합니다.
- 실제 클래스를 상속 받아서 만들어짐
- 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)
- 프록시 객체는 실제 객체의 참조(target)를 보관
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
2. 프록시 객체의 초기화
Member member = em.getReference(Member.class, “id1”);
member.getName();
프록시 객체를 가지고 온 후, member.getName()을 호출하면 Member target에 값이 없기 때문에 jpa가 영속성 컨텍스트에 요청을 합니다. 그러면 영속성 컨텍스트가 DB를 조회해서 실제 Entity객체를 생성해서 줍니다. 그리고 프록시 내부의 Member target이라는 멤버변수와 연결을 해줍니다. 영속성 컨텍스트를 통해서 초기화라는걸 요청하는게 중요합니다.
3.프록시 확인
- 프록시 인스턴스의 초기화 여부 확인
- PersistenceUnitUtil.isLoaded(Object entity)
- 프록시 클래스 확인 방법
- entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)
- 프록시 강제 초기화
- org.hibernate.Hibernate.initialize(entity);
- 참고: JPA 표준은 강제 초기화 없음
- 강제 호출: member.getName()
-> 프록시 인스턴스 초기화 여부 확인
-> 프록시 강제 초기화 예제입니다. getUsername으로 강제초기화하면 이상하니까 저런 초기화 방법을 제공합니다. 쿼리가 나가는걸 확인할 수 있습니다. getReference를 실제로 많이 사용하진 않지만, 프록시를 알아야 즉시로딩과 지연로딩을 깊이 이해할 수 있습니다.
4. 프록시의 특징
프록시 객체는 처음 사용할 때 한번만 초기화됩니다(중요). 한번 초기화 되면 여러번 호출해도 그 초기화된 객체를 계속 사용하게 됩니다. 또한 프록시 객체가 실제 엔티티로 바뀌는 것은 아닙니다.(중요) 프록시 객체를 통해서 실제 엔티티에 접근가능한 것 입니다. 이게 실제 엔티티로 바뀌는 것이 아닙니다.
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("before findMember = " + findMember.getClass());
System.out.println("findMember.username = " + findMember.getUserName());
System.out.println("after findMember = " + findMember.getClass());
before,after에 hellojpa.Member$HibernateProxy$odcVHpjy 이런식으로 같게 나옵니다. 프록시는 유지가 되고 내부의 target에만 값이 채워집니다.
타입을 체크할일이 실무에서는 많지 않지만 jpa에서 엔티티 타입 체크시에는 instance of를 사용해야합니다. 하나 헷갈릴만한걸로는 jpa에서는 어떻게든 == 비교를 맞춥니다.
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()); //proxy
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getClass()); //Member
System.out.println("refMember == findMember: " + (refMember == findMember));
find를 했지만 결과적으로 '=='을 맞추기 위해 findMember에 프록시 객체가 들어갑니다.
다음으로는 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가발생합니다. 실무에서는 이 에러를 진짜 많이 만나게 될 것 입니다.
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()); //proxy
em.detach(refMember);
System.out.println("refMember == findMember: " + (refMember == findMember));
detach (or em.close() or em.clear()) 후 조회를 하면, 영속성 컨텍스트에서 더이상 관리를 안하기 때문에LazyInitializationException이 발생합니다. 웹어플리케이션 개발 시 보통 트랜잭션과 프록시를 맞추는데, 트랜잭션이 끝나고 프록시를 조회할 때 보면 에러가 납니다. 그때 이 내용을 기억하시면 됩니다.
REFERENCE
https://www.inflearn.com/course/ORM-JPA-Basic
'JPA' 카테고리의 다른 글
[JPA] 영속성전이(CASCADE)와 고아 객체 (0) | 2020.04.22 |
---|---|
[JPA] 즉시로딩과 지연로딩 (0) | 2020.04.22 |
[JPA] @MappedSuperclass 애노테이션 (0) | 2020.04.19 |
[JPA] 상속 관계 매핑(고급매핑) (0) | 2020.04.19 |
[JPA] 연관 관계 매핑의 종류 (0) | 2020.04.19 |