-
프록시JPA/JPA 기본 2023. 7. 11. 18:05
public class JpaMain { public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory(); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); try { Member member = em.find(Member.class, 1L); // printMember(member); printMemberAndTeam(member); tx.commit(); } catch (Exception e) { tx.rollback(); } finally { em.close(); } emf.close(); } private static void printMember(Member member) { System.out.println("username = " + member.getName()); } private static void printMemberAndTeam(Member member) { String username = member.getName(); System.out.println("username = " + username); Team team = member.getTeam(); System.out.println("team = " + team.getName); } }
printMember와 printMemberAndTeam이라는 함수가 있다고 가정을 하자. Member와 Team을 같이 호출해야 할 때와 Member만 호출하고 싶을 때가 있는데 연관관계가 맺어있다고 해서 쿼리가 계속 같이해서 나가면 성능 상에 문제가 있을 것이다.
프록시 기초
JPA에서는 em.find() 말고도 em.getReference()라는 메서드가 있다.
- em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
em.getReference()는 쿼리가 나가지 않지만 객체가 조회되는 것이다.
Member findMember = em.getReference(Member.class, member.getId()); System.out.println("findMember.id = " + findMember.getId()); System.out.println("findMember.name = " + findMember.getName());
맨 윗줄의 findMember를 가져오는 데에는 쿼리가 호출되지 않는다. em.getReference()이기 때문이다.
두 번째 줄에서 getId()를 출력할 때에도 쿼리가 호출되지 않는데 세 번째 줄인 getName()을 출력할 때에는 쿼리가 호출된다. 그 이유는 em.getReference()를 할 때 member.getId()는 파라미터로 넘겨줬지만 name은 넘겨주지 않았기 때문이다.
아래 사진과 같이 영속성 컨텍스트에 초기화를 요청하고, 영속성 컨텍스트가 DB 조회를 통해 실제 Entity를 생성하며, MemberProxy가 실제 Entity의 name을 target.getName()으로 불러오는 것이다.
프록시의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니고, 초기화가 되면서 프록시 객체를 통해서 실제 엔티티에 접근 가능해짐
- 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시 초기화 시 문제 발생
네 번째 줄의 말이 잘 이해가 안가는데
Member member1 = new Member(); member1.setName("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()); System.out.println("a == a: " + (refMember == findMember));
이렇게 출력을 해 보면 m1은 Member 객체가 당연히 나오는데, reference도 프록시가 아니라 Member 객체가 나온다.
(프록시라면 class.hellojpa.Member가 아니라 class hellojpa.Member$HibernateProxy$~~~ 등으로 나옴)
이렇게 나오는 이유는 첫 번째는 이미 1차 캐시에 있는 데이터를 굳이 프록시로 가져오는 데 이점이 없기 때문에 원본을 가져오는 것이다.
두 번째 이유가 중요한데, JPA에서는 트랜잭션 안에서 한 영속성 컨텍스트에서 가져 온 데이터는 항상 똑같음을 보장해주기 때문에 이미 원본을 사용 중인 트랜잭션에서는 em.getReference()를 쓰더라도 원본을 가져온다는 것이다.
맨 마지막 줄을 JPA는 항상 충족시켜야한다. (a == a)
Member member1 = new Member(); member1.setName("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()); System.out.println("refMember == findMember: " + (refMember == findMember));
위에서 설명 했듯이 == 비교를 충족시켜야 하기 때문에 em.getReference()로 호출을 하면 em.find()로 호출을 하더라도 프록시가 호출된다.
다섯 번째 줄의 내용은 실무에서 많이 만나는 문제라고 한다.
Member member1 = new Member(); member1.setName("member1"); em.persist(member1); em.flush(); em.clear(); Member refMember = em.getReference(Member.class, member1.getId()); System.out.println("refMember = " + refMember.getClass()); em.clear(); // 여기서 영속성 컨텍스트가 종료되어서 아래에서 초기화 불가능 refMember.getUsername();
영속성 컨텍스트 상태여야지만 DB에서 조회를 해서 초기화를 시켜주는데, 만약에 영속성 컨텍스트 상태가 아니게 된다면 초기화를 할 수 없게 된다.
LazyInitializationException 예외가 터지게 된다면 영속성 컨텍스트를 다시 확인해보자.
'JPA > JPA 기본' 카테고리의 다른 글
영속성 전이(CASCADE) (0) 2023.07.11 즉시로딩과 지연로딩 (0) 2023.07.11 상속관계 매핑 (0) 2023.07.11 다양한 연관관계 매핑 (0) 2023.07.11 연관관계 매핑 - 단방향, 양방향 연관관계 (0) 2023.07.11