본문 바로가기

Spring/JPA & Hibernate

proxy

반응형

Why proxy?

Member를 조회할 때 team도 데이터베이스에서 함께 조회해야 할까?

 

만약, 비즈니스 로직 상, 맴버를 조회할 때, 그 맴버와 연관된 팀 정보도 함께 조회해야한다면, 팀 데이터까지 조회해야하지만,

그것이 아니라면, 굳이 팀 정보를 가져오는 것은 성능 상 불리할 수 있다.

 

비즈니스 로직에 의해 유동적일 수 있는 이러한 사항을 JPA는 프록시지연로딩을 통해 해결한다.

JPA에서 제공해주는 두가지 메소드

1. em.find()

 데이터베이스를 통해서 실제 엔티티 객체 조회 ( quary가 실행됨 )

2. em.getReference()
 데이터베이스 조회를 미루는 가짜(프록시)엔티티 객체를 조회( 디비에 quary를 실행하지 않음 )

getReference

메서드는 조회가 필수적으로 필요한 경우가 아니라면, (그 값이 실제 사용되는 시점이 아니라면) 조회 쿼리를 실행하지 않는다.

 


Member member = em.getReference(Member.class, member.getId()); 
System.out.println("findMember.id = " + member.getId());
System.out.println("findMember.userName = " + member.getUserName());

실행 결과에서 알 수 있듯이, 데이터베이스에 조회하지 않아도 알고 있는 id 값을 사용할 때는 SELECT 쿼리가 실행되지 않고, user name 데이터를 사용할 때가 되서야 SELECT 쿼리가 발생한다.

 

getReference()를 통해 얻은 객체의 클래스를 출력해보자


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

이는, Member.class 타입이 아닌 하이버네이트가 만든 가짜 프록시 클래스 타입인 것을 확인할 수 있다.

 

가짜 proxy 객체

하이버네이트가 만든 가짜 프록시 객체는 구조만 진짜 클래스와 같을 뿐 id만 가지고 있고 아무런 데이터를 가지고 있지 않다.

내부에는 target을 가지고 있어 이 값이 진짜 객체의 reference를 가리키고 있다.

 

 

  • 프록시 클래스는 실제 클래스를 상속받아서 만들어짐 (하이버네이트 내부 프록시 라이브러리를 통해)
  • 그래서 실제 클래스와 겉모양이 같다. 
  • 사용하는 입장에서는 진짜 객체인지
 프록시 객체인지 구분하지 않고, 사용할 수 있다.(이론상)

 

 

 

 

  • 프록시 객체는 실제 객체의 참조(target)를 보관하고 있다.
  • 프록시 객체의 getter 메서드를 호출하면 프록시 객체는 실제 객체(target)의 해당 getter 메서드 호출한다.

프록시 객체의 초기화

만약, 최초로 객체의 값을 요청할 경우에는 DB에서 조회한 적이 없기에, target이 존재하지 않는다.

이럴 경우 다음과 같은 방식으로 프록시 객체를 초기화 시킨다.

  1. 먼저, getName()을 호출하면, proxy객체의 target객체의 getName()을 호출하려 하지만, target이 존재하지 않음.
  2. JPA는 이 상황에서 영속성 컨텍스트에 진짜 member 객체를 요청한다. ( 영속성 컨텍스트를 통해서 초기화를 요청함 )
  3. 영속성 컨텍스트는 DB를 조회하여 실제 member entity를 생성하여 proxy 객체의 target에 연결한다.
  4. 프록시 객체의 getName()을 호출했을 때, target의 getName()을 반환하므로써, 실제 데이터가 봔환되도록 한다.
    즉, getName()을 호출하기 전까지는 DB에 member를 가져오는 query가 나가지 않을 것.

 🌟 프록시 특징 🌟

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시 객체 초기화를 통해 프록시 객체를 실제 엔티티로 바꾸는 것이 아니라, 해당 프록시 객체의 target을 통해서 실제 엔티티에 접근 가능하게 하는 것이다.
  • 프록시 객체는 실제 클래스 타입이 아닌 원본 엔티티를 상속 받아 생성한 클래스 타입이다. 따라서 타입 체크 시 == 비교가 아닌 instance of 사용해야한다.
  • 만약, 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
 (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
F

🔎   다음은 프록시 객체의 타입 비교하는 코드이다.


Member memberA = new Member();
memberA.setUsername( "memberA" );

Member memberB = new Member();
memberB.setUsername( "memberB" );

Member memberC = new Member();
memberB.setUsername( "memberC" );

em.persist( memberA );
em.persist( memberB );
em.persist( memberC );

em.flush(); // 영속성 컨텍스트 내용 DB에 반영하기
em.clear(); // 영속성 컨텍스트 초기화 (SELECT 쿼리를 보기 위함)

Member findMemberA = em.find( Member.class, memberA.getId() );

Member findMemberB = em.find( Member.class, memberB.getId() );
System.out.println( "findMemberA == findMemberB " + ( findMemberA.getClass() == findMemberB.getClass() ) );

Member referenceMemberC = em.getReference( Member.class, memberC.getId() );
System.out.println( "findMemberA == referenceMemberC " + ( findMemberA.getClass() == referenceMemberC.getClass() ) );
System.out.println( "referenceMemberC instanceof Member " + ( referenceMemberC instanceof Member ) );

System.out.println( "findMemberA Class: " + ( findMemberA.getClass() ) );
System.out.println( "referenceMemberC Class: " + ( referenceMemberC.getClass() ) );

tx.commit();

reference Member proxy..

실행 결과를 통해 알 수 있듯이, 프록시 객체의 경우 instance of 로 클래스를 비교해야 한다.

 

🔎  다음은 동일한 트랜잭션 레벨에서 영속성 컨택스트에 이미 존재하는 엔티티에 대해 em.getReference(entity)를 호출하는 코드이다.


Member findMemberC = em.find( Member.class, memberC.getId() );
Member referenceMemberC = em.getReference( Member.class, memberC.getId() );

System.out.println( "referenceMemberC == findMemberC " + ( referenceMemberC == findMemberC ) );

reference Member proxy == member class

위의 실행 결과에서 알 수 있듯이, JPA에서는 영속성 컨텍스트에서 find()로 가져온 객체와 getReference()로 가져온 proxy 객체의 클래스가 같다. 어차피 영속성 컨택스트에 존재하는 객체인데, 굳이 별도의 라이브러리를 통해 프록시 객체를 생성할 이유가 없기 때문이다. 또한, JPA에서는 한 트렌젝션 안에서 같은 엔티티를 조회할 경우, 그 두 객체의 동일성을 보장해줘야한다. 이러한 이유로 이미 영속성 컨택스트에 존재하는 엔티티의 경우, 프록시 객체가 아닌 실제 엔티티 객체를 준다.

 

🔎  다음은 동일한 트랜잭션 레벨에서 em.getReference(entity)를 호출한 후 em.find(entity)를 호출하는 코드이다.(아까와 반대)


Member referenceMemberC = em.getReference( Member.class, memberC.getId() );
Member findMemberC = em.find( Member.class, memberC.getId() );

System.out.println( "referenceMemberC == findMemberC " + ( referenceMemberC == findMemberC ) );

proxy가 한번 생성되어 조회되면 em.find() 호출 시에도 proxy를 반환한다.

JPA는 proxy이든 아니든 동일성을 보장하여 개발에 영향을 주지 않기 위해서이다. (== 비교 시 true를 보장하기 위해)

 

🔎  다음은 엔티티가 준영속 상태(detch)일 경우, 엔티티의 데이터를 조회하는 코드이다.


Member memberA = new Member();
memberA.setUsername( "memberA" );
em.persist( memberA );

em.flush(); // 영속성 컨텍스트 내용 DB에 반영하기
em.clear(); // 영속성 컨텍스트 초기화 (SELECT 쿼리를 보기 위함)

Member referenceMember = em.getReference( Member.class, memberA.getId() );

em.detach( referenceMember );
System.out.println( "referenceMember name : " + referenceMember.getUsername() );

tx.commit();

위의 실행 결과에서 볼 수 있듯이, 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, getter 메소드를 호출하면
 하이버네이트는 org.hibernate.LazyInitializationException 예외 터트린다.

준영속 상태를 만드는 다음 세가지 메소드 실행 후 엔티티에 대한 조회를 할 경우, 모두 "could not initialize proxy" 예외를 만나게 될 것
1. em.detach( referenceMember ); 

2. em.close();
3. em.clear();
=> 영속성 컨텍스트에 관리를 하지 않겠다.. 

트렌젝션이 끝나고 나서 영속성 컨텍스트를 조회하려고 할 때도 no Session Error 가 발생함.

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인
 

    PersistenceUnitUtil.isLoaded(Object entity)  

Member referenceMember = em.getReference( Member.class, memberA.getId() );

System.out.println( "referenceMember name : " + referenceMember.getUsername() ); // 강제 초기화 효과
Hibernate.initialize(referenceMember); // 강제 초기화
System.out.println( "referenceMember isLoaded : " + emf.getPersistenceUnitUtil().isLoaded( referenceMember ) );

  • 프록시 클래스 확인 방법
 

    entity.getClass().getName() 출력 ( ..javasist.. or  HibernateProxy…)  
  • 프록시 강제 초기화
 (하이버네이트가 제공하는 것)

    org.hibernate.Hibernate.initialize(entity);  
  • 참고: JPA 표준은 강제 초기화 없음
 

    대신 강제 호출: member.getName()

다음

juns-lee.tistory.com/123

반응형

'Spring > JPA & Hibernate' 카테고리의 다른 글

JPA 데이터 타입  (0) 2020.01.15
영속성 전이 CASCADE  (0) 2020.01.10
@MappedSuperclass  (0) 2020.01.09
연관관계 맵핑  (0) 2019.12.19
Entity Mapping  (0) 2019.12.19