-
#JPA-13 프록시JPA 2021. 2. 19. 17:32
[출처] 인프런 김영한 강사님 -자바 ORM 표준 JPA 프로그래밍 기본
프록시 학습전 의문? Member를 조회할 때 Team도 함께 조회해야 할까?
회원과 팀 함께 출력
public void printUserAndTeam(String memberId) { Member member = em.find(Member.class, memberId); Team team = member.getTeam(); System.out.println("회원 이름: " + member.getUsername()); System.out.println("소속팀: " + team.getName()); }
회원만 출력
public void printUser(String memberId) { Member member = em.find(Member.class, memberId); Team team = member.getTeam(); System.out.println("회원 이름: " + member.getUsername()); }
프록시 기초
• em.find() vs em.getReference()
• 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.getId() = " + findMember.getId()); System.out.println("findMember.getUsername() = " + findMember.getUsername()); tx.commit();
• em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
(DB에 쿼리가 안나가는데 조회가 되는것)
Member member = new Member(); member.setUsername("hello"); em.persist(member); em.flush(); em.clear(); Member findMember = em.getReference(Member.class, member.getId()); System.out.println("findMember.getId() = " + findMember.getId()); System.out.println("findMember.getUsername() = " + findMember.getUsername());
• em.getReference할때까지는 쿼리를 호출안하고 이 값을 사용하는 시점에 쿼리가 날라간다.
• findMember.getId() 같은 경우는 위에서 member.getId()로 collection에 있는 값을 넣었기때문에 DB에 안다녀와도 알수 있지만.
• findMember.getUsername같은 경우에는 DB에 있는거 이고 findMember가 가짜이기때문에 findMember.getUsername을 호출 하는 시점에 JPA가 DB에 쿼리를 날리고 findMember에 값을 채운다음에 결과를 보여준다.
Proxy~ hibernate가 만들어낸 가짜라고 볼 수 있다.
hibernate가 가짜 entity를준다. 껍데기는 똑같은데 내부가 텅텅빈것이다 .
안에잇는 target이 진짜 reference를 가르킨다. target은 초기에는 없고 초기에는 껍데기에 아이디 값만 가지고 있는 애를 반환한다.
프록시 특징
• 실제 클래스를 상속 받아서 만들어짐
• 실제 클래스와 겉 모양이 같다.
• 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고
사용하면 됨(이론상)
-> 상속관계는 상속해도 부모타입 보고 쓰면 되기 때문에.
• 프록시 객체는 실제 객체의 참조(target)를 보관
• 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
getName을호출하면 실제 타겟에있는 getName을 대신 조회해준다. 하지만 처음엔 target이 없음.
프록시 객체의 초기화
Member member = em.getReference(Member.class, "id1"); //프록시 객체를 가져옴
member.getName(); //진짜 객체를 가져옴
1. getName을 했을때 taget에 진짜 entity가 없으면
2. JPA가 영속성 컨텍스트에 요청을 한다.
3~4. 영속성 컨테르가 실제 entity객체를 가져오고
5. 프록시의 target에 진짜 Entity를 연결해준다.
프록시의 특징
• 프록시 객체는 처음 사용할 때 한 번만 초기화
• 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초 기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
->프록시는 교체되는 것이 아니라 내부에 타겟에만 갑싱 채워지는 것.
• 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비 교 실패, 대신 instance of 사용)
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 m1 = em.find(Member.class, member1.getId()); Member m2 = em.find(Member.class, member2.getId()); System.out.println("m1 == m2 " + (m1.getClass() == m2.getClass())); //type 비교 tx.commit();
type이 같기때문에 Member m1 = em.find(Member.class, member1.getId()); Member m2 = em.getReference(Member.class, member2.getId()); System.out.println("m1 == m2 " + (m1.getClass() == m2.getClass())); //type 비교
type비교이기때문에 상속관계 이런것은 안된다. ->실제 비지니스 로직에서는 type을 비교할때 이게 proxy로 들어올지 실제로 들어올지 모르기때문에 type비교는 ==으로 하면 안되고 instanceof를 사용해야한다.
private static void logic(Member m1, Member m2){ System.out.println("m1 = Member : " + (m1 instanceof Member)); System.out.println("m2 = Member : " + (m2 instanceof Member)); }
instanceof로 비교 • 속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해 도 실제 엔티티 반환
Member member1 = new Member(); member1.setUsername("hello1"); em.persist(member1); em.flush(); em.clear(); Member m1 = em.find(Member.class, member1.getId()); System.out.println("m1.getClass() = " + m1.getClass()); Member reference = em.getReference(Member.class, m1.getId()); System.out.println("reference.getClass() = " + reference.getClass()); tx.commit();
reference가 proxy가아니라 실제 entity인 이유
1. 이미 Member를 영속성 컨텍스트 1차캐시에 올려놧는데 proxy를 가져와서 얻을 이점이없다.
원본을 바로 반환하는게 성능 최적화에 적합하다.
2. JPA에서는 m1 == reference는 항상 true가나와야한다. m1이 실제든 proxy든 상관없이
JPA에서는 마치 자바 컬렌션에서 가져온값을 == 비교 하듯이 ==비교가 한 영속성 컨텍스트 안에서 가져온놈이고 PK가 똑같으면 JPA는 항상 TRUE를 반환해야한다. (JPA는 한 트렌젝션 안에서 같음을 보장해준다.)
Member reference = em.getReference(Member.class, member1.getId()); System.out.println("reference.getClass() = " + reference.getClass()); //proxy Member m1 = em.find(Member.class, member1.getId()); System.out.println("m1.getClass() = " + m1.getClass()); //Member System.out.println("reference = m1 " + (reference == m1));
프록시가 한번 조회되면 그다음 FIND도 PROXY로 맞춰버린다 그래야 == 비교를 할 수 있기 때문이다. • 속성 컨텍스트의 도움을 받을 수 없는 준속 상태일 때, 프록시를 초기화하면 문제 발생 (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
Member member1 = new Member(); member1.setUsername("hello1"); em.persist(member1); em.flush(); em.clear(); //1. proxy Member reference = em.getReference(Member.class, member1.getId()); //2. query가 날라가면서 proxy 초기화 System.out.println("reference.getClass() = " + reference.getClass()); //proxy //3. 그런데? 프록시의 초기화 요청이 영속성 컨텍스트를 통해서 하는데, 영속성 컨텍스트를 꺼버리면? 또는 em.detach로 관리안하게 해버리면? em.close(); //or em.detach(reference); tx.commit();
clear도 똑같다. clear는 끈건아니지만 다지우고 새로만드는 것이기 떄문에 이런것을 마나는 순간 reference는 영속성 컨텍스트의 도움을 못받는 것이다. could not initialize proxy에러를 만난다.
프록시 확인
• 프록시 인스턴스의 초기화 여부 확인 PersistenceUnitUtil.isLoaded(Object entity)
- 프록시가 초기화 됐는지 안됐는지.
emf.getPersistenceUnitUtil().isLoaded(reference));
• 프록시 클래스 확인 방법 entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)
• 프록시 강제 초기화 org.hibernate.Hibernate.initialize(entity); or Hibernate.initialize(entity);
• 참고: JPA 표준은 강제 초기화 없음 강제 호출: member.getName()
'JPA' 카테고리의 다른 글
#JPA-15 속성 전이: CASCADE (0) 2021.02.19 #JPA-14 즉시 로딩과 지연 로딩 (0) 2021.02.19 #JPA-12 상속관계 매핑 (0) 2021.02.19 #JPA-11 다양한 연관관계 매핑 (0) 2021.02.18 #JPA-10 연관관계 매핑 기초 (0) 2021.02.17