Proxy

@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
 }
위와 같이 두 개의 Entity가 연관관계로 형성되어 있으며, DB에서도 2개의 테이블이 생성되어 있는 경우를 가정해보자

 

 // 1L으로 Member를 조회
 Member findMember = em.find(Member.class, 1L);
 
 // Member, Team을 함께 사용해야 할 때조회 시 함께 조회해야하는 경우
 private static void printMemberAndTeam(Member member) {
        String username = member.getUsername();
        System.out.println("username = " + username);

        Team team = member.getTeam();
        System.out.println("team.getName() = " + team.getName());
  }
    
  // 단순하게 Member에 있는 필드만 사용하는 경우 굳이 Team을 함께 조회할 필요가 없는 경우
  private static void printMember(Member member) {
        System.out.println("member = " + member.getUsername());
  }

 

위와 같이 단순 Member Entity에 있는 필드만 사용할 경우 / Team을 참조하고 있는 참조변수를 통해서 Team Entity에 있는 필드도 사용할 경우가 있다. 이때, 단순 Member에 있는 필드만 사용할 경우 Join을 통해서 Team Entity 값을 불러올 필요는 없을 것이다.

프록시 기초


getReference(entity type, pk): 
- 조회 시 실제로 DB에서 조회하는 것이 아닌 조회 할 TYPE과 같은
  Proxy Object를 생성하고 내부적으로 target이라는 참조변수가 있다.       
- Proxy 객체는 실제 객체를 상속받고 있다.
- 실제 데이터가 필요할 시점에 영속성 컨텍스트를 통해서 DB에서 조회하고 
  실제 Entity 객체를 만들어 target을 통해 참조한다.
- 초기 조회 이후에는 실제 객체를 참조하고 있기에 조회가 필요하지 않게 된다.

// find()를 통해서 조회 시 영속성 컨텍스트에 없을 시 조회 Query가 나간다.
Member findMember = em.find(Member.class, member.getId());
// getReference()를 통해서 조회시 프록시 객체를 만들고 조회 Query가 나가지 않는다.
Member findMember = em.getReference(Member.class, member.getId());
// class hellojpa.Member$HibernateProxy$uXjNnDJ9 << Proxy Class
System.out.println("findMember.getClass() = " + findMember.getClass()); 
// 조회 시 이미 id를 주었기에 DB에 조회 할 필요 x
System.out.println("findMember.getId() = " + findMember.getId()); 
// 실제 데이터를 가져와야 하기에 DB에서 조회 후 데이터를 가져온다.(초기)
System.out.println("findMember.getUsername() = " + findMember.getUsername()); 
// 이미 조회를 한번 했기에 DB에서 조회 x
System.out.println("findMember.getUsername() = " + findMember.getUsername());

프록시 객체 초기화 과정



프록시 특징


프록시 객체는 처음 사용할 때 한 번만 초기화된다.

Member findMember = em.getReference(Member.class, member1.getId());
// 초기화
System.out.println("findMember.getUsername() = " + findMember.getUsername()); 
// 초기화 이후
System.out.println("findMember.getUsername() = " + findMember.getUsername()); 

/* 결과 : 초기화 시 조회 Query가 1회 나가고 
이후에는 DB에서 조회하지 않고 영속성 컨텍스트에서 조회한다.

Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.TEAM_ID as TEAM_ID3_3_0_,
        member0_.USERNAME as USERNAME2_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
findMember.getUsername() = member1
findMember.getUsername() = member1
*/

프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아닌 초기화 시

프록시 객체를 통해서 실제 엔티티에 접근이 가능한 것이다.

// proxy 객체 생성
Member reference = em.getReference(Member.class, member1.getId());
// 초기화 전
System.out.println("before reference.getClass() = " + reference.getClass());
// proxy 초기화
reference.getUsername();
// 초기화 후
System.out.println("after reference.getClass() = " + reference.getClass());


/* 결과 : 초기화 시 실제 entity로 변경되는 것이 아니다.
before reference.getClass() = class hellojpa.Member$HibernateProxy$YXc20kMc
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.TEAM_ID as TEAM_ID3_3_0_,
        member0_.USERNAME as USERNAME2_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
after reference.getClass() = class hellojpa.Member$HibernateProxy$YXc20kMc
*/

 

프록시 객체는 원본 엔티티를 상속받기에 타입 체크시 (==) 비교 대신 instance of를 사용해야한다.

/* 
 타입 비교 시 instance of 를 사용해야한다 -> 실제 객체일지 proxy 객체일지 알 수 가 없기에
 하나의 Tranaction안에 있을 경우이다.
*/

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 m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());

// false
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass()));

영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티가 반환

// 하나의 트랜젝션 안에서 JPA는 a == a 비교 시 항상 true를 보장해야한다.

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());


/* 결과 : 이미 영속성 컨텍스트에 있을 경우 실제 entity를 반환한다.
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.TEAM_ID as TEAM_ID3_3_0_,
        member0_.USERNAME as USERNAME2_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
        
m1 = class hellojpa.Member
reference = class hellojpa.Member
*/

// 이미 프록시 객체를 생성한 시점에서 find를 해도 프록시 객체를 반환한다.
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("m1 = " + refMember.getClass());

Member findMember = em.find(Member.class, member1.getId());
System.out.println("reference = " + findMember.getClass());

System.out.println("findMember == refMember " + ( findMember == refMember ) );

/* 결과 : 반대의 경우로 프록시 객체를 생성한 이후 find하면 프록시 객체를 반환
m1 = class hellojpa.Member$HibernateProxy$Im8NAvKP
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.TEAM_ID as TEAM_ID3_3_0_,
        member0_.USERNAME as USERNAME2_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
reference = class hellojpa.Member$HibernateProxy$Im8NAvKP
findMember == refMember true

*/

준영속 상태일때, 프록시를 초기화하면 문제 발생

(org.hibernate.LazyInitializationException)

Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("m1 = " + refMember.getClass());

// 준영속 상태로 만들기
 em.detach(refMember);
// em.clear();
// em.close();

System.out.println("refMember.getUsername() = " + refMember.getUsername());

/* 결과 
org.hibernate.LazyInitializationException: could not initialize proxy [hellojpa.Member#1] - no Session
	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:169)
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:309)
	at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
	at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
	at hellojpa.Member$HibernateProxy$u8iZnGjl.getUsername(Unknown Source)
	at hellojpa.JpaMain.main(JpaMain.java:55)
*/

 


프록시 확인


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

Member refMember = em.getReference(Member.class, member1.getId());
// 프록시 객체 초기화 여부 확인
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); 

/* 결과
isLoaded = false
*/

 

프록시 강제 초기화

Member refMember = em.getReference(Member.class, member1.getId());
//강제로 초기화 -> Hibernate에 있는 것이지 JPA가 제공하는 것은 아니다
Hibernate.initialize(refMember);

/*
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.TEAM_ID as TEAM_ID3_3_0_,
        member0_.USERNAME as USERNAME2_3_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
3월 22, 2023 11:09:56 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/test]

Process finished with exit code 0

*/

'DB > JPA' 카테고리의 다른 글

영속성 컨텍스트  (0) 2023.05.10
즉시 로딩, 지연 로딩  (0) 2023.03.23