개발자꿈나무
프록시 본문
Member -> Team 단방향 연관관계를 가지고 있다고 가정했을 때, 과연 Member를 조회할 때마다 매번 Team을 조회하는 것이 옳을까?
//회원과 팀 정보를 출력하는 메소드
public void printUserAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("member.username : " + member.getName());
System.out.println("member.team : " + team.getName());
}
//회원 정보만 출력하는 메소드
public void printUserName(String memberId) {
Member member = em.find(Member.class, memberId);
System.out.println("member.username : " + member.getName());
}
- 위 로직처럼 회원과 팀을 함께 출력하는 것이 아니라 회원 정보만 출력한다고 할때 Team 객체는 전혀 사용하지 않는 것을 알 수 있다. 그렇다면 과연 Member 객체를 조회할 때마다 Team 객체를 조회하는 것은 효율적이지 않을 수 있다. JPA는 이를 해결하기 위해 실제로 엔티티가 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이를 지연 로딩이라고 한다.
★ 프록시 기초
실제 데이터베이스를 조회하여 엔티티를 반환하는 메소드는 em.find(); 이다. 그러나 실제 엔티티를 사용할 때까지 데이터베이스 조회를 미루고자 한다면 em.getReference() 메소드를 사용하면 된다. 이 메소드를 호출하면 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체가 아니라 프록시 객체를 반환한다.
Member member = em.find(Member.class, "id1");
Member member = em.getReference(Member.class, "id1");
★ 프록시 특징
- 프록시는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉모습이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체에 대한 참조(target)을 보관한다.
- 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
★ 프록시 객체의 초기화
- 프록시 객체는 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라고 한다.
Member member = new Member();
member.setName("test1");
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()); //실제 쿼리가 여기서 날아감
- 쿼리가 보내진 결과를 보면 실제 데이터베이스를 조회해서 결과가 필요한 getName()이 실행된 순간 select 쿼리를 이용해서 데이터베이스를 조회하는 모습을 확인할 수 있다.
식별자의 경우 getReference() 를 실행할 때 식별자 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.
따라서 getId()를 호출하더라도 초기화 되는 것이 아니라 프록시 객체가 가지고 있는 식별자 값이 반환된다.
- 프록시 객체에 member.getName() 메소드를 호출해서 실제 데이터를 조회한다.
- 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청한다. -> 초기화
- 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
- 프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 멤버변수에 보관한다.
- 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다.
★ 정리
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니며, 프록시 객체가 초기화되면 프록시 객체를 통해 실제 엔티티에 접근할 수 있다.
- 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다. (== 비교를 하는 것이 아니라 instance of를 사용해야 한다.)
Member member1 = new Member();
member1.setName("test1");
em.persist(member1);
Member member2 = new Member();
member2.setName("test2");
em.persist(member2);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
Member findMember = em.find(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (refMember.getClass() == findMember.getClass()));
- member1과 member2를 ==으로 비교하게 되면 member1은 프록시 객체, member2는 실제 엔티티이므로 false가 나오게 된다. 이 상황에선 당연한 결과라고 예상하지만 만약 비즈니스 로직이 이렇게 구성되어 있으면 어떨까?
method(member1, member2);
private static void method(Member m1, Member m2) {
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass()));
} // -> false
- 매개변수를 받을 때 프록시 타입이 아니라 Member 타입을 받아오는데 언제 프록시 객체가 넘어올지 모르므로 타입 비교는 ==를 써서는 안된다. 대신에 instance of를 이용해서 타입 비교를 해줘야한다.
method(member1, member2);
private static void method(Member m1, Member m2) {
System.out.println("m1 == m2 : " + (m1 instanceof Member));
System.out.println("m1 == m2 : " + (m2 instanceof Member));
} // -> true , true
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
Member member = new Member();
member.setName("test1");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.class : " + findMember.getClass());
// class hellojpa.model.entity.Member
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember.class : " + refMember.getClass());
// class hellojpa.model.entity.Member
System.out.println("m1 == m2 : " + (refMember == findMember)); // true
- 영속성 컨텍스트에 이미 찾는 엔티티가 있으면 굳이 지연로딩을 사용해도 아무런 효과가 없으므로 실제 엔티티를 반환하지만, 또다른 이유로는 JPA는 같은 레벨의 트랜잭션(영속성 컨텍스트) 안에서 ==는 항상 true가 나와야 하기 때문이다. refMember가 프록시 객체일 경우 m1 == m2가 false이므로 실제 엔티티를 반환하는 것이다.
Member member = new Member();
member.setName("test1");
em.persist(member);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember.class : " + refMember.getClass());
// class hellojpa.model.entity.Member$HibernateProxy$H3OKX4fk
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.class : " + findMember.getClass());
// class hellojpa.model.entity.Member$HibernateProxy$H3OKX4fk
System.out.println("m1 == m2 : " + (refMember == findMember)); // true
- 그런데 만약 프록시를 호출하고 난 이후에 em.find를 호출하면 어떻게 될까? JPA는 위 로직에서 m1 == m2 : true가 나와야 하므로 findMember 역시 프록시 객체를 반환한다. 실제 실무에서 이렇게까지 복잡하게 쓰일 일은 잘 없지만 JPA의 기본 매커니즘에 대해서 이해를 하고 있어야 하는 부분이다. 프록시 역시도 실제 엔티티를 상속받은 객체이므로 실제 개발할 때는 엔티티던 프록시던 크게 상관은 없지만 타입 비교를 할 때만 주의해주면 될 것 같다.
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 예외가 발생한다. -> em.close() 혹은 em.detach()
★ 프록시 확인
- 프록시 인스턴스 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity) - 프록시 클래스 확인
entity.getClass().getName() -> ..javasist.. or HibernateProxy .. - 프록시 강제 초기화
org.hibernate.Hibernate.intialize(entity);
'자바 > JPA' 카테고리의 다른 글
영속성 전이, 고아 객체 (0) | 2023.08.22 |
---|---|
지연 로딩과 즉시 로딩 (0) | 2023.08.22 |
실전 예제 - 상속관계 매핑 (0) | 2023.08.21 |
복합 키 매핑 (0) | 2023.08.20 |
@MappedSuperclass (0) | 2023.08.18 |