일단 한가지 예시를 들어보려한다.
Member 와 Team 엔티티가 있다 둘은 ManyToOne의 관계로 연관관계를 갖고있다.
//Member를 가져옴
Member member = em.find(Member.class, 1L);
//멤버만 필요한 경우
private static void printMember(Member member) {
String name = member.getName();
System.out.println("memberName = " + name);
}
//멤버와 팀이 모두 필요한 경우
private static void printMemberAndTeam(Member member) {
String name = member.getName();
System.out.println("member = " + name);
Team team = member.getTeam();
String teamName = team.getName();
System.out.println("teamName = " + teamName);
}
위의 두 메서드는 각각 멤버만 출력하는 메서드와 둘다 출력하는 메서드이다
printMemberAndTeam의 경우 member 와 team 의 정보가 모두 필요하기 때문에 한번에 가져오는게 유리하다.
반면에 printMember메서드를 사용한다고 했을때 필요한 정보는 member의 정보이다 하지만 조회한 멤버에 팀이 속해 있으니 해당 메서드에서 필요하지 않은 팀의 정보까지 모두 가져오게 되고 낭비가 생긴다 .
이를 해결하기위해 JPA에서는 프록시를 사용한다.
J
PA의 프록시에 대해 알아보자
EntityManager에는 엔티티 조회를 위해 em.find() , em.getReference()가 있다.
em.find()는 실제 객체를 조회하는데 사용하며 DB로 쿼리를 날린다 .
em.getReference()는 데이터베이스 조회를 미루고 가짜 객체(프록시)를 조회한다.
em.find()의 쿼리는 아래와 같다 fetchType의 default가 EAGAR이기 떄문에 find할 때 한방에 다 가져온다
em.getReference()를 한번 호출해 보자
멤버 인서트 쿼리만 나오고 조회 쿼리가 나오지 않는다. 위에 멤버이름을 출력하는 코드를 주석을 풀고 다시 호출해봤다.
조회 쿼리가 날라갔다. 이처럼 getReference메서드는 해당 엔티티에 대해 프록시 객체 생성해를 넣어 놓고 실제 해당 엔티티를 담은 변수가 사용될 때 쿼리를 날려 값을 채워 넣는다.
프록시 초기화 과정
//레퍼런스 조회
Member getReference = em.getReference(Member.class, 1L);
//프록시객체 사용
System.out.println("getReference.getName() = " + getReference.getName());
프록시 객체의 초기화 과정은 위 그림과 같다 순서대로 설명하면
1. 실제 프록시를 사용하는 getName() 메서드가 호출된다.
2. 영속성 컨텍스트로 초기화 요청을 한다 . (이 때 컨텍스트에 값이 있다면 반환하고 아니면 DB에 조회쿼리를 날린다)
3. DB를 조회해서 값을 가져온다
4. 실제 엔티티 객체를 만들어 내고 프록시 객체가 가지고 있는 멤버변수에 넣는다 .
5. 프록시의 target 필드 (실제 엔티티)의 getName()메서드를 호출한다. target.getName();
프록시 특징
- 실제 클래스를 상속 받아서 만들어 진다
- 실제 클래스와 겉 모양이 같다 .
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다 .
- 프록시 객체는 실제 객체의 참조를 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다 .
- 프록시 객체는 처음 사용할 때 한 번만 초기화 된다(한번 초기화되면 영속성 컨텍스트에서 값을 꺼낸다)
- 프록시 객체의 초기화는 실제 엔티티로 바뀌는 것이 아니라 실제 엔티티에 접근 가능하도록 되는 거다
- 프록시는 실제 엔티티를 상속받아 만들어지기 때문에 타입비교에 주의해야한다 프록시객체와 실체객체를 타입비교할때는
"==" 이 아니라 instance of 로 해줘야한다.
- 프록싱은 영속성 컨텍스트의 도움을 받아서 작동하기 때문에 detech(), close()등 영속성 준영속이든 영속성 초기화 상태가 될 경우 예외가 터진다 .
-영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출하면 실제 엔티티를 반환한다 .
- 이미 멤버를 일차 캐시에 올려놨는데 프록시로 가져와 봐야 아무 이점이 없다 .
- JPA에서는 일차 캐시에 올라간 엔티티에 대해서 동일성을 보장하기 때문에 이 경우에 내부적으로 프록시가 아닌 실제 엔티티를
반환해 준다. (일차 캐시에 올라간 같은 엔티티에 대한 == 비교는 항상 true를 반환해야 한다.)
* 두 가지 경우를 생각해보자
1. 레퍼런스로 찾은다음 다시 레퍼런스로 찾을떄
Member getReference = em.getReference(Member.class, 1L);
System.out.println("getReference.getClass() = " + getReference.getClass());
Member getReference2 = em.getReference(Member.class , 1L);
System.out.println("getReference2.getClass() = " + getReference2.getClass());
결과
같은 프록시를 리턴한다.
2. 레퍼런스로 찾은다음 find로 해당 객체를 찾을때
Member getReference = em.getReference(Member.class, 1L);
System.out.println("getReference.getClass() = " + getReference.getClass());
Member findMember = em.find(Member.class, 1L);
System.out.println("findMember.getClass() = " + findMember.getClass());
결과
getReference로 엔티티를 조회했다면 find() 로 찾아도 프록시 객체가 출력된다.
중요한 점은 실제 비지니스 메서드 개발에서 조회한 엔티티를 파라미터로 사용할떄 사용하는 쪽에서는 그것이 프록시 객체인지 실제 엔티티인지 알수가 없다는 것이다 .
예시)
printMember(Member member1 , Member member2 )
//프록시인지 실제 엔티티인지 알 수 없다 .
그렇기 떄문에 엔티티에 대한 타입비교는 "=="이 아닌 instance of를 사용해야한다.
프록시 확인 Util 메서드
Member getReference = em.getReference(Member.class, 1L);
System.out.println("getReference.getClass() = " + getReference.getClass());
//프록시 상태 확인 메서드
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(getReference);
System.out.println("loaded = " + loaded);
//프록시 클래스 확인 방법
String proxyName = getReference.getClass().getName();
//프록시 강제 초기화
Hibernate.initialize(getReference);
프록시를 직접 다뤄서 사용할 일은 많지 않지만 프록시 메커니즘을 이해해야 즉시로딩 지연로딩에 대해서 이해가 잘되니 알아두는 것이 좋다.
'JPA' 카테고리의 다른 글
JPA 공부 10 - 영속성 전이 (cascade) (0) | 2021.06.05 |
---|---|
JPA 공부 9 - 즉시로딩과 지연로딩 (0) | 2021.06.05 |
JPA 공부 7 - 상속관계 매핑 (0) | 2021.06.05 |
JPA 공부 6 - 다대다 연관관계(N:N) (0) | 2021.06.04 |
JPA 공부 5 - 일대일 연관관계(1:1) (0) | 2021.06.04 |