먼저 생각을 해보자.
실제로 필요한 비즈니스 로직에 따라 다르다.
비즈니스 로직에서 필요하지 않을 때가 있는데, 항상 Team을 함께 가져와서 사용할 필요는 없다. 낭비가 발생하게 된다.
JPA는 이 낭비를 하지 않기 위해, 지연로딩과 프록시라는 개념으로 해결한다.
지연 로딩을 이해하려면, 프록시의 개념에 대해서 명확하게 이해해야 한다.
em.find()
vs em.getReference()
em.find(): DB를 통해서 실제 엔티티 객체 조회
em.getReference(): DB의 조회를 미루는 가짜(프록시) 엔티티 객체를 조회
가짜 엔티티 객체를 조회한다고?
이게 무슨 얘기냐면 DB의 쿼리는 안나가는데 객체가 조회가 되는 것이다.
아래의 코드로 이해하자.
getReference로 가져온 엔티티의 클래스 확인
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush(); //영속성 컨텍스트의 1차 캐시에 있는 정보를 db로 강제 전송
em.clear(); //영속성 컨텍스트 초기화
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember = " + refMember.getClass());
tx.commit()
member 객체를 하나 만들고 저장한후 플러쉬, 클리어로 영속성 컨텍스트를 깨끗하게 만들었다.
em.getReference(Member.class, member.getId());
를 하면 쿼리가 어떻게 나갈까? 아래 사진으로 결과를 확인해보자.
쿼리를 보니까 인서트 쿼리만 나가고 셀렉트 쿼리는 안나갔다.
이때 refMember.getUsername()을 하면 셀렉트 쿼리가 나간다.
GetReference를 호출하는 시점에는 DB에 쿼리를 안보낸다. 그런데 이 값이 실제 사용되는 시점에 DB에 쿼리가 나간다.
클래스를 확인해보니 $HibernateProxy~~
라고 나온다. 이게 뭐냐면 Hibernate가 강제로 만든 가짜 클래스라는 얘기다.
em.getReference(Member.class, member.getId());
라고 하면 진짜 멤버 객체를 반환해주는 것이 아니다. Hibernate가 자기 내부에 라이브러리를 사용해서 가짜(프록시) 엔티티 객체를 만들어서 주는 것이다.
프록시 엔티티 객체를 위 사진처럼 생겼다. 껍데기는 똑같은데 안이 텅텅 빈 것이다.
그리고 내부에는 Target이라는 게 있는데 이게 진짜 Reference를 가르킨다.
프록시 특징
1.em.getReference()
로 프록시 객체를 가져온 다음에, getName()
메서드를 호출 하면
2. MemberProxy 객체에 처음에 target 값이 존재하지 않는다. JPA가 영속성 컨텍스트에 초기화 요청을 한다.
3. 영속성 컨텍스트가 DB에서 조회해서
4. 실제 Entity를 생성해준다.
5. 그리고 프록시 객체가 가지고 있는 target(실제 Member)의 getName()
을 호출해서 결국 member.getName()
을 호출한 결과를 받을 수 있다.
6. 프록시 객체에 target이 할당 되고 나면, 더이상 프록시 객체의 초기화 동작은 없어도 된다.
프록시의 특징
프록시 객체는 처음 사용할 때 한 번만 초기화
프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님
em.getReference()
로 조회한 클래스를 getClass()로 보면, HibernateProxy 클래스였던 것을 위에서 학습했다.프록시 객체는 원본 엔티티를 상속받는다. 프록시 객체와 원본 객체의 타입이 다르다.
항상 같다
라고 나와야 한다.Member find = em.find(Member.class, member1.getId());
Member reference = em.getReference(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1 == m2));
--------------
## 결과 ##
m1 == m2 : False
System.out.println("find : " + (find instanceof Member));
System.out.println("reference : " + (reference instanceof Member));
--------------
## 결과 ##
find : True
reference : True
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush(); //영속성 컨텍스트의 1차 캐시에 있는 정보를 db로 강제 전송
em.clear(); //영속성 컨텍스트 초기화
Member m1 = em.find(Member.class, member.getId());
System.out.println("m1.getClass() = " + m1.getClass());
Member reference = em.getReference(Member.class, member.getId());
System.out.println("reference.getClass() = " + reference.getClass());
tx.commit(); // 트랜잭션을 커밋하는 시점에서 영속성 컨텍스트에 있는 DB의 쿼리가 날라감
참고
Member m1 = em.getReference(Member.class, member.getId()); System.out.println("m1.getClass() = " + m1.getClass()); Member reference = em.find(Member.class, member.getId()); System.out.println("reference.getClass() = " + reference.getClass()); ------------------------- ## 결과 ## m1.getClass() = class hellojpa.Member$HibernateProxy$Cl8uGtEy reference.getClass() = class hellojpa.Member$HibernateProxy$Cl8uGtEy
이전에 말했듯이 JPA에서는 같은 인스턴스의 == 비교에 대해서 같은 영속성 컨텍스트 안에서 조회하면
항상 같다
라고 나와야 한다. m1에서 getReference로 Hibernate Proxy로 조회가 됐다. 이때 == 비교에 대해서 같다고 보장해야하기 때문에 em.find한 결과도 실제 member 객체가 아닌 Hibernate Proxy가 반환된다.
결론적으로, 내부적으로 복잡한 처리 등은 JPA가 알아서 해주니 우리가 개발할 때 프록시인지 진짜 객체인지는 중요하지 않다.
Member member = new Member();
member.setUsername("hello");
em.persist(member);
em.flush(); //영속성 컨텍스트의 1차 캐시에 있는 정보를 db로 강제 전송
em.clear(); //영속성 컨텍스트 초기화
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("ref.getClass() = " + refMember.getClass());
em.detach(refMember); //준영속 상태로 만듦
System.out.println("refMember = " + refMember.getUsername()); //준영속 상태이기 때문에 데이터를 못가져옴 (오류 발생)
tx.commit(); // 트랜잭션을 커밋하는 시점에서 영속성 컨텍스트에 있는 DB의 쿼리가 날라감
오류
getReference로 프록시 객체를 가져왔다. 프록시에 대한 초기화 요청은 영속성 컨텍스트를 통해서 일어나는데 만약 영속성 컨텍스트를 꺼버린다면 어떻게 될까?
em.detach(refmember)
를 해서 프록시 객체를 준영속 상태로 만들었다. 준영속 상태로 만들고 refMember.getUsername()
을 하니까 위 사진처럼 프록시를 초기화 할 수 없다는 오류가 발생한다.
이전에 설명한 것처럼 프록시 객체는 refMember.getUsername()
을 하면 영속성 컨텍스트에 초기화 요청을 한다. 그런데 em.detach(refmember)로 준영속 상태로 만들었기에 영속성 컨텍스트에 초기화 요청을 하지 못하게 되었고 오류가 발생한다.
프록시 확인을 도와주는 Util성 메소드들이 있다.
출처
자바 ORM 표준 JPA 프로그래밍 강의
게시글 속 자료는 모두 위 강의 속 자료를 사용했습니다.
https://ict-nroo.tistory.com/131