오봉이와 함께하는 개발 블로그
JPA - 프록시 본문
프록시
@Entity
@Getter @Setter
public class Member extends BaseEntity{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
Member에 대한 정보만 필요할 때, Member와 Team 모두 필요할 때가 있을텐데, Member를 조회할 때 무조건 Team도 같이 조회가 되면 불필요한 쿼리가 생겨 성능상 큰 문제는 아닐지언정 좋지는 않을 것이다.
어떻게 하면 Member만 조회할 수 있을까?
프록시 기초
JPA에서는 em.find()말고도 em.getReference()라는 메소드를 제공한다.
- em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
- 껍데기는 똑같지만 실제 내용은 텅텅 비었다.
- target은 진짜 객체를 가르키는데 초기 값은 null이다
Member member = new Member();
member.setName("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.name = " + findMember.getName());
위 코드를 실행하면 em.find로 값을 찾아오는 순간, 즉 Member findMember = em.find(Member.class, member.getId());가 실행될 때 조회 쿼리가 전송된다.
하지만, 아래 코드를 실행하면 달라진다.
Member member = new Member();
member.setName("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.name = " + findMember.getName());
위 코드를 실행하면 em.getReference()가 실행될 때는 조회 쿼리가 전송되지 않다가 System.out.println("findMember.name = " + findMember.getName());가 실행될 때, 즉 실제 값을 사용할 때 조회 쿼리가 전송된다.
System.out.println("findMember.id = " + findMember.getId());는 이미 em.getReference()를 할 때 파라미터로 넣었기 때문에 찾아오지 않아도 된다.
em.getReference()로 조회한 findMember를 getClass() 메소드를 통해 조회하면 hellojpa.Member$HibernateProxy$XXX라고 나온다. 이는 Hibernate가 강제로 만든 가짜 클래스라는 뜻이다.
프록시 특징 1
- 실제 클래스를 상속 받아서 만들어지기 때문에 실제 클래스와 겉 모양이 같다.
- 사용자 입장에서는 이론상 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체의 참조(target)을 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
- 프록시 객체 생성 당시 target은 null이다.
- 아직 실제 객체를 조회하지 않았기 때문
프록시 객체의 초기화
Member member = em.getReference(Member.class, "id1");
member.getName();
초기화라는 것은 프록시에 값이 없기 때문에 영속성 컨텍스트에 요청하는 것이다.
요청 메소드를 두 번 호출하면 값을 가져왔기 때문에 조회 쿼리를 또 날리진 않는다.
프록시 특징 2
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 한 번 초기화 하면 초기화 했던 값을 계속 사용.
- 프록시 객체를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다!
- 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능해지는 것 뿐이다.
- 프록시는 유지가 되고, 값이 채워지는 것
- 프록시 객체는 원본 엔티티를 상속받는다.
- 따라서 타입 체크시 주의해야한다.
- ==비교 대신, instance of를 사용해야 한다.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환시킨다.
- 영속성 컨텍스트에 이미 있다는 뜻은 1차 캐시에 있기 때문에 조회 쿼리를 날리지 않아도 된다는 뜻.
- == 비교할 때 true를 반환하기 위해서 같은 영속성 컨텍스트에 있으면 실제 엔티티를 반환함.
- 영속성 컨텍스트에 이미 있다는 뜻은 1차 캐시에 있기 때문에 조회 쿼리를 날리지 않아도 된다는 뜻.
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = ", m1.getClass());
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = ", reference.getClass());
// 결과
m1 = class hellojpa.Member
reference = class hellojpa.Member
- 반대로 em.getReference()를 통해 호출한 객체가 있을때 em.find()로 같은 객체를 호출하면 진짜 객체가 아닌 프록시 객체를 반환해 준다.
- JPA는 == 비교할 때 어떻게든 true를 반환하기 위함
Member reference = em.fingetReference(Member.class, member1.getId());
System.out.println("reference = ", reference.getClass());
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = ", m1.getClass());
// 결과
reference = class hellojpa.Member$HibernateProxy$XXX
m1 = class hellojpa.Member$HibernateProxy$XXX
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화 하면 문제가 발생한다.
- 하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림
- 프록시는 영속성 컨텍스트의 도움을 받아 원본 객체를 상속받기 때문이다.
Member refMember = em.reference(Member.class, member1.getId());
System.out.println("refMember = ", refMember.getClass());
em.detach(refMember); // 영속성 컨텍스트에서 제거
// 외에 em.clear(), em.close() 모두 포함.
refMember.getName();
// 결과는 org.hibernate.LazyInitializationException 발생
프록시 확인
- 프록시 인스턴스의 초기화 여부 확인(실제 객체로부터 값을 가져왔는지 안 가져왔는지 여부)
- PersistenceUnitUtil.isLoaded(Object entity)
- emf.PersistenceUnitUtil.isLoaded(객체)
- PersistenceUnitUtil.isLoaded(Object entity)
- 프록시 클래스 확인 방법
- 객체명.getClass()
- 프록시 강제 초기화
- Hibernate.initialize(객체)
- 참고로 JPA 표준은 강제 초기화가 없기 때문에 객체.get메소드()(member.getName())를 사용해야 함
출처 : 인프런 김영한 지식공유자님의 스프링 부트와 JPA 실무 완전 정복 로드맵 강의
'BE > JPA' 카테고리의 다른 글
JPA - 영속성 전이(CASCADE)와 고아 객체 (0) | 2022.06.28 |
---|---|
JPA - 즉시 로딩, 지연 로딩 (0) | 2022.06.28 |
JPA - 실전 예제 4 - 상속관계 매핑 (0) | 2022.06.28 |
JPA - @Mapped Superclass (0) | 2022.06.28 |
JPA - 상속관계 매핑 (0) | 2022.06.28 |