프록시

Mina Park·2022년 9월 10일
0
  • 엔티티 조회시 모든 연관 엔티티들이 항상 사용되는 것은 아니므로, 데이터베이스에서 한꺼번에 조회하는 것은 효율적이지 않다
  • JPA에서 이런 문제 해결을 위해 제공하는 방법이 바로 지연 로딩
    - 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연
  • 지연로딩을 사용하려면, 실제 엔티티 객체 대신 가짜 객체, 즉 프록시 객체가 필요

JPA 표준명세는 지연로딩의 구현방법을 JPA 구현체에 위임


1. 기본개념

  • em.find() vs em.getReference()
    • em.find(): 실제 엔티티 객체 조회(식별자를 사용)

    • em.getReference(): DB 조회를 미루는 가짜(프록시) 엔티티 객체 조회(쿼리 X)


  • em.find() => select 쿼리 실행
            MemberMapping member = new MemberMapping();
            member.setUsername("hello");

            em.persist(member);

            em.flush();
            em.clear();

			MemberMapping findMember = em.find(MemberMapping.class, member.getId());
            System.out.println("findMember.id = " + findMember.getId());
            System.out.println("findMember.username = " + findMember.getUsername());

            tx.commit();
  • em.getReference(): DB 값 사용하는 경우가 없음 => select 쿼리 실행 X
            MemberMapping member = new MemberMapping();
            member.setUsername("hello");

            em.persist(member);

            em.flush();
            em.clear();

			MemberMapping findMember = em.getReference(MemberMapping.class, member.getId());
            System.out.println("findMember.id = " + findMember.getId());
            //getId는 파라미터로 받은 값(DB에 있는 값이 아니라)
            

            tx.commit();
  • em.getReference(): userName의 경우 DB 값 필요 => select 쿼리 실행
    • getClass()값 콘솔로 찍어보면 프록시 확인 가능
            MemberMapping member = new MemberMapping();
            member.setUsername("hello");

            em.persist(member);

            em.flush();
            em.clear();

			MemberMapping findMember = em.getReference(MemberMapping.class, member.getId());
            System.out.println("findMember.class = " + findMember.getClass());
             System.out.println("findMember.username = " + findMember.getUsername());
            

            tx.commit();
findMember.class = class hellojpa.mapping.MemberMapping$HibernateProxy$3nz5GHdh

2. 프록시의 특징

  • (1) 실제 클래스를 상속받아서 겉모양이 동일하게 생성됨

    • (이론상)사용자 입장에서는 진짜 객체인지 프록시 객체인지 구분할 필요가 X
  • (2) 프록시 객체는 실제 객체의 참조(target)를 보관

  • (3) 프록시 객체가 호출되면, 프록시 객체는 실제 객체의 메소드 호출

    • 처음 요청하는 경우는 값이 없으므로 객체 초기화 진행
    • 이후에는 갖고있는 값을 사용, 초기화 미진행

📌프록시 객체의 초기화

  • 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는 것
Member member = em.getReference(Member.class, “id1”);
member.getUsername();

1) 프록시 객체는 처음 사용할 때 한 번만 초기화
2) 초기화할 때 프록시 객체가 실제 엔티티로 바뀌는 것이 X
- 내부에 참조 값이 채워지는 것일 뿐, 여전히 proxy
3) 초기화가 되면 프록시 객체가 실제 엔티티에 접근 가능한 것
4) 프록시 객체는 원본 엔티티를 상속받음(hibernate가 자동으로)
- 따라서 타입 체크시 주의!!!
- == 비교 X, 대신 instance of 사용
📌 JPA에서는 프록시 객체가 사용될 경우가 있음을 염두, 가급적 instance of 사용하여 비교

 			MemberMapping member1 = new MemberMapping();
            member1.setUsername("hello");
            em.persist(member1);

            em.flush();
            em.clear();

            MemberMapping findMember = em.find(MemberMapping.class, member1.getId());
           
			System.out.println("before findMember = " + findMember.getClass()); //proxy
            System.out.println("findMember.username = " + findMember.getUsername()); //select쿼리 실행
            System.out.println("after findMember = " + findMember.getClass()); //여전히 proxy
 			MemberMapping member1 = new MemberMapping();
            member1.setUsername("member1");
            em.persist(member1);

            MemberMapping member2 = new MemberMapping();
            member2.setUsername("member2");
            em.persist(member2);

            em.flush();
            em.clear();

            MemberMapping findMember1 = em.find(MemberMapping.class, member1.getId());
			MemberMapping findMember2 = em.find(MemberMapping.class, member2.getId());
           
			System.out.println("findMember1 == findMember2 " + (findMember1.getClass() == findMember2.getClass())); //true
 			MemberMapping member1 = new MemberMapping();
            member1.setUsername("member1");
            em.persist(member1);

            MemberMapping member2 = new MemberMapping();
            member2.setUsername("member2");
            em.persist(member2);

            em.flush();
            em.clear();

            MemberMapping findMember1 = em.find(MemberMapping.class, member1.getId());
			MemberMapping findMember2 = em.getReference(MemberMapping.class, member2.getId());
           
			System.out.println("findMember1 == findMember2 " + (findMember1.getClass() == findMember2.getClass())); //false
            System.out.println("findMember1 == findMember2 " + (findMember1 instance of Member)); //true
            System.out.println("findMember1 == findMember2 " + (findMember2 instance of Member)); //true
  • (4) 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티가 반환됨
    • 반대의 경우도 마찬가지로, em.getReference()를 호출한 뒤 em.find()를 호출해도 프록시가 반환됨

      📌 같은 영속성 컨텍스트 안(식별자가 동일)에서는 동일성을 보장하기 위한 JPA의 원칙(JPA의 메커니즘)!!!

//case1: find(실제 엔티티) -> reference(실제 엔티티) 

 			MemberMapping member1 = new MemberMapping();
            member1.setUsername("hello");
            em.persist(member1);

            em.flush();
            em.clear();

            MemberMapping findMember1 = em.find(MemberMapping.class, member1.getId()); //영속성 컨텍스트에 올라감
            System.out.println("findMember1 = " + findMember1.getClass()); //MemberMapping

            MemberMapping reference1 = em.getReference(MemberMapping.class, member1.getId());
            System.out.println("reference1 = " + reference1.getClass()); //MemberMapping

            System.out.println("a == a " + (findMember1 == reference1)); //true
            //같은 영속성 컨텍스트 안에 있다면 JPA는 늘 같은 객체를 반환(동일성 보장 원칙)
            //이러한 원칙을 지키기 위해서라도 영속성 컨텍스트 안에 이미 값이 있다면 프록시가 아닌 실제 엔티티를 반환

            tx.commit();
//case2: reference(프록시) -> reference(프록시) 

 			MemberMapping member1 = new MemberMapping();
            member1.setUsername("hello");
            em.persist(member1);

            em.flush();
            em.clear();

            MemberMapping reference1 = em.getReference(MemberMapping.class, member1.getId()); 
            System.out.println("reference1 = " + reference1.getClass()); //proxy

            MemberMapping reference2 = em.getReference(MemberMapping.class, member1.getId());
            System.out.println("reference2= " + reference2.getClass()); //proxy(전자와 동일한 proxy)

            System.out.println("a == a " + (reference1 == reference2)); //true

            tx.commit();
//case3: reference(프록시) -> find(프록시) 

 			MemberMapping member1 = new MemberMapping();
            member1.setUsername("hello");
            em.persist(member1);

            em.flush();
            em.clear();

            MemberMapping reference1 = em.getReference(MemberMapping.class, member1.getId()); 
            System.out.println("reference1 = " + reference1.getClass()); //proxy

            MemberMapping findMember1 = em.find(MemberMapping.class, member1.getId());
            System.out.println("findMember1= " + findMember1.getClass()); //proxy(동일성 보장 때문에 앞서 가져온 proxy를 반환)

            System.out.println("a == a " + (reference1 == findMember1)); //true

            tx.commit();
  • (5) 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태(close(), detach(xxx), clear() 등)인 경우, 프록시를 초기화할 수 없는 문제 발생
    • 하이버네이트는 org.hibernate.LazyInitializationException 예외를 발생시킴

      실무에서 많이 맞닥뜨리게 되는 에러!!!
      (ex. 트랜잭션 종료 후 프록시 객체 조회하는 경우 등)

			MemberMapping member1 = new MemberMapping();
            member1.setUsername("hello");
            em.persist(member1);

            em.flush();
            em.clear();

            MemberMapping refMember = em.getReference(MemberMapping.class, member1.getId()); //영속성 컨텍스트에 올라감
            System.out.println("refMember = " + refMember.getClass()); //proxy

            em.detach(refMember); //영속성 컨텍스트에서 분리
            refMember.getUsername(); //프록시 초기화 불가

            tx.commit();

3. 프록시의 확인

  • (1)프록시 인스턴스의 초기화 여부 확인
    • emf.getPersistenceUnitUtil().isLoaded(Object entity)
			MemberMapping member1 = new MemberMapping();
            member1.setUsername("hello");
            em.persist(member1);

            em.flush();
            em.clear();

            MemberMapping refMember = em.getReference(MemberMapping.class, member1.getId()); //영속성 컨텍스트에 올라감
            System.out.println("refMember = " + refMember.getClass()); //proxy

            System.out.println("isLoaded1 = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); //false
            refMember.getUsername();
            System.out.println("isLoaded2 = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); //true

            tx.commit();
  • (2)프록시 클래스 확인
    • entity.getClass().getName() 출력
System.out.println("refMember = " + refMember.getClass()); //xxx$HibernateProxy$xxx
  • (3)프록시 강제 초기화
    • JPA 표준에는 강제 초기화 X => 하이버네이트가 제공하는 기능 사용
 			//refMember.getUsername(); //방법1. 사실상 강제 초기화
            Hibernate.initialize(refMember); //방법2. 하이버네이트가 제공하는 강제 초기화 메소드

0개의 댓글