JPA) 연관관계 관리 - 프록시
Date: Updated:카테고리: java
프록시를 사용해야하는 이유
Member와 Team이라는 테이블이 있다고 가정하자. (Member는 자신이 소속되어 있는 Team이 있다. = N:1의 관계)
이때 Member와 Team 객체를 출력하는 비즈니스 로직이 있다고 할 때 다음과 같이 작성할 수 있다.
private static void printMemberAndTeam(Member member){
System.out.println("username = "+ member.getUsername());
System.out.println("username = "+ member.getTeam().getName());
}
...
Member member = entityManager.find(Member.class, 1L); // member와 team 조회 join 쿼리 수행
printMemberAndTeam(member);
JPA 에서는 member의 이름과 team 이름을 구하기 위해 join 쿼리를 날리게 된다.
printMemberAndTeam
메서드를 수행하기엔 적합한 쿼리였지만 Member의 정보만 필요한 경우에는 join 쿼리는 오히려 낭비가 될 것이다.
어떻게하면 때에 따라 낭비 없이 최적화를 할 수 있을까?
JPA에서는 프록시를 통한 지연로딩이라는 개념으로 해결할 수 있다.
프록시
프록시는 실제 객체의 대리인으로써 사용자의 요청을 대신 받아서 처리해주는 역할을 수행한다.
JPA는 리소스 낭비 최소화를 위해 프록시의 특징을 활용하여 실제 객체가 필요한 시점에만 쿼리를 수행할 수 있게끔 하는 일명 지연로딩 기능을 제공한다.
프록시의 특징은 다음과 같다.
- Hibernate에 의해 프록시 객체가 실제 클래스를 상속 받아서 만들어진다.
- 그렇기 때문에 프록시 객체는 실제 클래스와 메서드 등 겉 모양이 똑같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분할 수 없기 때문에 실제 객체를 사용하듯 사용하면 된다.
- 프록시 객체는 실제 객체의 참조(target)를 보관한다.
- 참조 객체는 실제 엔티티를 가리키고 있으며 초기에는 null 이다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출하게 된다.
- 예를 들면 프록시 객체를 통해
getName()
을 호출 하면 target의getName()
을 대신 호출해주게 된다.
- 예를 들면 프록시 객체를 통해
EntityManager
는 이러한 프록시를 활용한 지연로딩 조회를 위해 getReference()
기능을 제공한다.
EntityManager의 find()와 getReference()
entityManager.find()
- 즉시로딩- 데이터베이스를 통해서 실제 엔티티 객체를 조회한다.
entityManager.getReference()
- 지연로딩- 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다.
- 실제 엔티티 객체를 불러오는게 아닌 Proxy라는 클래스로 감싸진 가짜 객체를 반환한다.
- 실제 엔티티 객체를 불러오는게 아닌 Proxy라는 클래스로 감싸진 가짜 객체를 반환한다.
- 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다.
// 이 시점에는 쿼리를 날리지 않고 가짜(프록시) member 객체를 가져온다
Member findMember = entityManager.getReference(Member.class, 1L);
System.out.println("findMember.id = "+ findMember.getId()); // getId() 값은 이미 알고 있으므로 쿼리가 수행되지 않는다.
System.out.println("findMember.name = "+ findMember.getUsername()); // member의 username은 데이터베이스에서 가져와야 하므로 해당 시점에 쿼리를 수행하여 값을 가져온다.
프록시 객체의 초기화 과정
- Member의 프록시 객체를 가져와서 target을 통해
getName()
을 호출한다. - target의 값이 초기에는 null이므로 Member 객체 초기화를 위해 영속성 컨텍스트에 요청한다.
- 영속성 컨텍스트는 Member 객체를 초기화하기 위해 데이터베이스를 조회한다.
- 이 시점에 쿼리가 수행된다.
- 영속성 컨텍스트를 통해 Member 객체가 초기화된다.
- 이제 Member의 프록시 객체의 target이 실제 Member 객체를 바라보게 되고 target을 통해 실제
getName()
을 호출하게 된다.- 객체 초기화 이후 target은 실제 Member를 바라보게 되므로 실제 엔티티를 보는것과 같다.
정리
-
프록시 객체는 처음 사용할 때 한 번만 초기화 된다.
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.
- 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근을 하는거지 바뀌는게 아님.
- 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근을 하는거지 바뀌는게 아님.
- 프록시 객체는 원본 엔티티를 상속 받는다. 따라서 타입 체크시 주의해야함.
- 원본 엔티티를 상속을 받아서 사용하기 때문에 == 로 비교하면 항상 같다고 나옴.
- 프록시와 엔티티 타입 체크를 하려면 instance of 로 비교해야함.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면
em.getReference()
를 호출해도 실제 엔티티를 반환하게 된다.- 엔티티가 이미 초기화가 되어있으므로 지연로딩이 의미가 없다.
- 그렇기 때문에 프록시로 가져와봤자 아무 이점이 없기 때문이다.
- 그리고 JPA는 트랜잭션 안에서 초기화 한 엔티티에 대해서 동일성을 보장해야 하기 때문에 프록시에서 가져오더라도 실제 엔티티를 반환 한다.
- 반대의 경우도 마찬가지다.
- 예를들어, 처음에
em.getReference()
로 호출을 하고 바로 아래에서em.find()
로 다시 호출하여 영속성 컨텍스트를 통해 초기화를 했다고 가정하자. -
이때 두개의 객체를 비교 하게되면 JPA는 엔티티 동일성을 보장해야하기 때문에 첫번째로 호출한
em.getReference()
에 맞춰em.find()
로 호출한 경우에도 프록시 객체로 나오게 된다.Member refMember = em.getReference(Member.class, 1L); // proxy Member findMember = em.find(Member.class, 1L); // Member System.out.println("refMember == findMember : " + (refMember == findMember)) // true
- 예를들어, 처음에
- 엔티티가 이미 초기화가 되어있으므로 지연로딩이 의미가 없다.
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화 하면 문제 발생
-
아래와 같이 프록시 객체가 영속성 컨텍스트에 의해 초기화 되기전에 영속성 컨텍스트를 닫는 경우 영속상태가 될 수 없다.
Member refMember = em.getReference(Member.class, 1L); // proxy em.close(); // or em.detach(refMember) or em.clear() System.out.println("refMember = " + refMember.getUsername()); // org.hibernate.LazyInitializationException caused!
-
이럴경우 Hibernate는 org.hibernate.LazyInitializationException 예외를 반환하게 된다.
-
프록시 객체를 확인하기 위한 방법들
- 프록시 인스턴스의 초기화 여부 확인하기
-
PersistenceUnitUtil.isLoaded(Object entity)
Member refMember = em.getReference(Member.class, 1L); // proxy PersistenceUnitUtil puu = emf.getPersistenceUnitUtil(); System.out.println("isLoaded = " + puu.isLoaded(refMember)); // false System.out.println("refMember = " + refMember.getUsername()); // 프록시 초기화 System.out.println("isLoaded = " + puu.isLoaded(refMember)); // true
-
- 프록시 클래스 확인 방법
-
entity.getClass() 출력
Member refMember = em.getReference(Member.class, 1L); // proxy System.out.println("refMember = " + refMember.getClass()); // class jpa.Member$HibernateProxy$...
-
- 프록시 강제 초기화
-
org.hibernate.Hibernate.initialize(entity)
Member refMember = em.getReference(Member.class, 1L); // proxy System.out.println("refMember = " + refMember.getClass()); // class jpa.Member$HibernateProxy$... Hibernate.initialize(refMember) // 프록시 강제 초기화
-
- JPA 표준은 강제 초기화가 없음.
- member.getName() 등을 통해 직접 메서드를 실행하여 강제 호출로 초기화 하기
📣 Reference
본 포스팅은 김영한님의 강의를 듣고 스스로 정리 및 추가한 내용입니다.
댓글남기기