프록시
프록시 사용하는 이유
비즈니스 로직에서 Member와 Team을 같이 조회해야할 지는 상황에 따라 달라진다.
만약 회원과 팀을 함께 출력해야하면 회원 정보를 조회할 때 Team 정보도 같이 조회하는게 좋지만,
회원 정보만 필요한 경우 Team 정보까지 필요하지 않다.
em.find(Member.class, 1L)에서 team도 조회할지 회원 정보만 조회할지 상황에 따라 달라진다
이런 문제를 지연 로딩과 프록시로 해결할 수 있다.
프록시 기초
em.find() vs get.Refernece()
- em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조
try {
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
System.out.pintln("findMember.id = " + findMember.getId());
System.out.pintln("findMember.id = " + findMember.getUsername());
tx.commit();
}
결과를 보면 getUsername() 조회할 때만 쿼리가 날라갔다.
→ em.getReference 호출하는 시점에는 쿼리를 안날리고 findMember가 실제로 사용되는 시점에 쿼리가 나가는데 member.getId()할 때 쿼리가 안나가는 이유는 em.getReference(Member.class, member.getId())에서 파라미터로 getId() 해서 findMember에 값이 있기 때문에 findMember.getId() 할 때는 쿼리가 날라가지 않고, Username은 아직 DB에 값이 있다.
따라서 findMember.getUsername()에서 쿼리가 날라가서 데이터를 가져온다.
em.getReference해서 가져온 객체
결과를 보면 em.getReference(Member.class, member.getId())로 가져온 클래스는 하이버네이트가 만들어낸 가짜 클래스이다.
프록시 특징
- 실제 클래스를 상속 받아서 만들어졌다
- 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용됨 (이론상).
- 프록시 객체는 실제 객체의 참고(target)을 보관
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메서드 호출(프록시의 getName()을 호추라면 실제 엔티티의 getName() 대신 호출해준다.)
- 엔티티를 DB에서 조회하기 전까지 target에 엔티티가 없다.
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니라 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능한 것이다.
- 프록시 객체는 원본 엔티티를 상속 받으므로 프록시 타입 체크할 때 주의해야한다
- == 비교하면 안되고 instance of 사용
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생한다.
하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림
프록시 초기화 요청이 영속성 컨텍스트를 통해서 일어나므로 em.detach(reMember) (refMember를 영속성 컨텍스트가 관리하지 않게..) 또는 em.close() (영속성 컨텍스트 닫으면..) 또는 em.clear() refMember는 영속성 컨텍스트의 도움을 받지 못하므로 refMember.getUsername()에서 프록시를 초기화 할 수 없다고 에러가 난다.
프록시 객체는 처음 사용할 때 한번만 초기화 되므로 refMember.getUseraname()에서는 초기화되지 않고 영속성 컨텍스트가 clear되니까 실제 엔티티에 접근하지 못하므로 에러 발생
프록시 객체 초기화
Member member = em.getReference(Member.class, member.getId());
member.getName();
프록시 객체 초기화 과정
em.getReference하고 껍데기만 있는 프록시가 만들어지고 member.getName해서 프록시의 getName()을 보는데 맨 처음에는 Member target에 데이터가 없다.
JPA가 영속성 컨텍스트에 Member 객체요청 → 영속성 컨텍스트가 DB 조회 → DB는 실제 entity 생성해서 MemberProxy의 target에 entity 연결 → target.getName()을 통해서 Member의 getName()이 반환
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.username = " + findMember.getUsername());
위 코드에서는 getUsername()할 때 초기화 과정이 일어난다. 따라서 getUsername()을 두번 요청하면 초기화 과정이 필요하지 않다.
프록시 확인 방법
프록시 인스턴스 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity)
//entity: 프록시 객체
//초기화된 프록시 객체 -> true
//초기화 되지 않은 프록시 객체 -> false
프록시 클래스 확인 방법
entity.getClass();
//entity: 프록시 객체
출력: (..javasist.. or HibernateProxy…)
프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity);
//Hibernate.initialize(proxyEntity);
참고: 하이버네이트가 강제 초기화를 제공하는 거고 JPA 표준은 강제 초기화 없다.
QnA
Q. 프록시 객체도 영속성 컨텍스트에 저장이 되나?
프록시 객체 초기화할 때 영속성 컨텍스트에 초기화 요청하고 영속성 컨텍스트가 DB에서 조회한다고 나왔는데 .getReference 했을 때 생성되는 프록시 객체도 영속성 컨텍스트에서 관리하나?
A. 맞다. 참고할 자료: https://tecoble.techcourse.co.kr/post/2022-10-17-jpa-hibernate-proxy/
'JPA' 카테고리의 다른 글
영속성 전이: CASCAED (0) | 2023.11.09 |
---|---|
즉시 로딩과 지연 로딩 (2) | 2023.11.09 |
고급 매핑 (0) | 2023.11.09 |
양방향 연관관계와 연관관계의 주인 (0) | 2023.11.02 |
연관관계 매핑 기초 (0) | 2023.11.02 |