728x90
프록시
Member와 Team의 연관 관계에서 Member를 조회하면 항상 Team도 조회하는 것을 원하지 않을 수 있다. 지금 당장 나는 username만 필요하다면 굳이 Team을 조회할 필요가 없는 것이다. 만약 Team이 username을 조회할 때마다 조회된다면 추가적인 비용이 드는 것이니 손해이다.
이런 문제를 해결하기 위해선 먼저 프록시를 이해해야 한다.
프록시 기초
em.find()
- DB를 통해서 실제 Entity 객체를 조회하는 메소드
Member member = em.find(Member.class, memberId);
- member라는 객체 데이터를 사용하지 않고 find()만 해도 SELECT 쿼리를 실행한다.
em.getReference()
- DB 조회를 미루는 프록시(가짜) Entity 객체를 조회한다. 조회 쿼리가 실행되지 않지만 객체가 조회된다.
Member member = em.find(Member.class, memberId);
- find()와 반대로 SELECT 쿼리가 실행되지 않는다. 하지만 아래와 같이 데이터를 실제로 호출하면 SELECT 쿼리를 실행한다.
Member member = em.getReference(Member.class, memberId); System.out.println("findMember = " + member.getClass());
- 클래스를 출력하면 HibernateProxy라고 출력되는데 Hibernate가 강제로 만든 프록시 클래스라는 의미이다.
- 정리하자면 em.getReference()는 프록시를 사용하는데 이때 진짜 값이 있는 객체를 주는 것이 아닌, 프록시라는 속이 텅텅 빈 가짜 객체를 준다.
프록시의 특징
- 프록시는 실제 Entity 클래스를 상속받아서 만들어진다. 그래서 실제 클래스와 겉모습이 같다. 이 상속은 내가 직접 하는 것이 아닌, Hibernate가 내부적으로 라이브러리를 사용하여 상속한다.
- 프록시를 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용한다.
- 프록시 객체는 실제 객체의 참조(target)를 보관한다.
- 프록시 객체에 있는 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다. getId()를 호출하면 target의 getId()를 대신 호출하는 것이다.
- 하지만 맨 처음에는 DB에서 조회되지 않은 상태이고 target은 비어있을 것이고 문제가 될 것이다.
프록시 객체의 초기화
Member member = em.getReference(Member.class, "id");
member.getName();
- 위의 코드에서 em. getReference() 이후 member.getName()을 호출하면 프록시 객체를 가져왔기 때문에 target에 값이 없는 상태이다.
- target에 값이 없다면 영속성 컨텍스트에 데이터를 요청해 그 값을 반환한다.
- 그 과정을 순서대로 나열하면
- getName()으로 데이터를 요청했는데 Member의 target에 데이터가 없다.
- JPA가 진짜 Member객체를 영속성 컨텍스트에 요청한다.
- 영속성 컨텍스트는 DB를 조회해서 실제 Entity객체를 생성하여 보내준다.
- target과 진짜 객체 Entity를 연결한다.
- target의 진짜 getName()을 호출하여 값을 반환한다.
- 즉 위의 코드에서 getName()을 호출하는 시점에 영속성 컨텍스트로 Member를 요청하여 실제 레퍼런스를 가지게 된다. 이후 다시 getName()을 재요청한다면 영속성 컨텍스트에서 값을 받아왔기 때문에 프록시에서 조회한다.
주의 사항
- 프록시 객체는 처음 사용할 때 한 번만 초기화되며 이후엔 그 내용을 그대로 사용한다.
- 프록시 객체를 초기화하면 프록시 객체가 실제 Entity로 바뀌는 것은 아니다. 단지 초기화되면 프록시 객체를 통해 실제 Entity에 접근할 수 있을 뿐이다. 그래서 프록시를 통해 데이터를 가져온 뒤에도 member.getClass()를 출력하면 $HibernateProxy… 이런 문자열이 포함되어 출력될 것이다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
em.persist(member2);
em.flush();
em.clear();
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.find(Member.class, member2.getId());
// true
System.out.println("m1 == m2: " + (m1.getClass() == m2.getClass()));
Member m3 = em.find(Member.class, member1.getId());
Member m4 = em.getReference(Member.class, member2.getId());
// false
System.out.println("m3 == m4: " + (m3.getClass() == m4.getClass()));
//false
System.out.println("m1 == m2: " + (m1 == m2));
// true
System.out.println("m1 == m2: " + (m1 instanceof m2));
System.out.println("m2 == m2: " + (m2 instanceof m2));
- 프록시 객체는 원본 Entity를 상속받기 때문에 프록시인 Member와 아닌 Member의 타입이 동일하지 않을 수 있어 주의해야 한다. 그래서 위와 같이 타입 비교 시 ==를 사용하면 false가, instanceof를 사용하면 true가 나온다.
- 프록시를 사용할지 사용하지 않을지 모르기 때문에 웬만하면 instanceof를 사용하는 것이 좋다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
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());
//true
System.out.println("m1 == reference = " + (m1 == reference));
- 위의 코드에서 m1을 em.find()로 불러왔기 때문에 영속성 컨텍스트 1차 캐시에 정보가 있는 상태이다. 이후 m2에서 getReference를 하면 프록시가 아닌 영속성 컨텍스트에서 가져와서 실제 Entity를 반환한다. 이는 굳이 프록시로 가져올 필요가 없기 때문이다.
- 이후 m1 == reference도 마찬가지로 같은 영속성 컨텍스트에서 객체를 가져오기 때문에 같다고 취급한다. 이는 JPA는 한 트랜잭션 안에서 PK가 같다면 같은 객체임을 보장하기 때문이다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getClass());
//true
System.out.println("a == a: " + (refMember == findMember));
- 프록시가 초기화된 상태에서 find()를 해도 위에서 말한 것처럼 JPA는 한 트랜잭션 안에서 PK가 같다면 같은 객체임을 보장해야 하기 때문에 기본적으로 refMember == findMember의 값이 true임을 보장해야 한다. 따라서 아래와 같이 refMember , findMember 모두 프록시로 출력이 된다.
- find()를 했기 때문에 SELECT쿼리가 발생했다.
- refMember, findMember모두 같은 프록시로 출력된 것을 확인할 수 있다. 프록시를 한 번 조회한 이후에는 쿼리가 나가는 find()를 호출하더라도 해당 객체는 프록시로 반환한다. 그래야 JPA가 같은 객체임을 보장하는 룰을 보장할 수 있기 때문이다.
- 즉 처음에 Entity로 반환했다면 Entity, 프록시로 반환했다면 계속 프록시로 반환한다.
- 사실 이게 어떤 값이 나오지는 중요하지는 않지만 instanceof를 사용해서 타입 비교를 해야 하는 것을 기억해야한다.
준영속 상태의 프록시
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
// 프록시 생성
Member refMember = em.getReference(Member.class, member1.getId());
// 프록시로 출력
System.out.println("refMember = " + refMember.getClass());
// 준영속화
em.close();
refMember.getUsername();
System.out.println("refMember = " + refMember.getClass());
tx.commit();
- refMember.getUsername()을 호출하면 실제 데이터로 초기화하면서 데이터를 가져와야 하지만 close() 때문에 영속성 컨텍스트로 관리하지 않으면서 Exception이 발생한다.
- 위와 같이 detach(), clear(), close()로 준영속 상태가 되면 LazyInitializationException을 던진다. 이는 프록시를 초기화 할 때 영속성 컨텍스트를 통해 하기 때문이다.
- 트랜잭션이 끝났는데 조회를 시도할 때 많이 발생하는 예외이다.
프록시 유틸리티 메소드
- PersistenceUnitUtil.isLoaded(Object entity)
- 해당 프록시가 초기화 됐는지 확인하는 메소드
- entity.getClass().getName()
- 프록시 클래스를 확인하는 메소드
- org.hibernate.Hibernate.initialize(entity)
- Hibernate에서 제공하는 프록시 강제 초기화 메소드로 JPA 표준은 강제 초기화가 없다.
- member.getName()과 같이 강제로 호출해야 한다.
728x90
'Java > JPA' 카테고리의 다른 글
[JPA] 영속성 전이와 고아 객체 (0) | 2023.06.25 |
---|---|
[JPA] 즉시 로딩과 지연 로딩 (0) | 2023.06.25 |
[JPA] 다양한 연관 관계 매핑(다대일, 일대다, 일대일, 다대다) (0) | 2023.06.25 |
[JPA] 연관 관계 (0) | 2023.06.24 |
[JPA] Entity 매핑 (0) | 2023.06.24 |