나를 기록하다
article thumbnail
Published 2023. 6. 24. 15:23
[TIL-8 / 230623] JPA 프록시 TIL
반응형

1. 프록시

Member를 조회할 때 Team도 함께 조회해야 할까?

member, team

  • 회원과 팀 함께 출력
public void printUserAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
System.out.println("소속팀: " + team.getName()); }
  • 회원만 출력
public void printUser(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
}

회원과 팀을 함께 출력하기 위해서는 Member와 Team을 모두 조회해야 하지만, 회원만 출력할 때도 Team을 조회해야 할까?

프록시 기초

  • em.find() vs em.getReference()
  • em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회

em.getReference()

em.find() 예시

  • 코드
try {
            Member member = new Member();
            member.setUsername("hello");

            em.persist(member);

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

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

            tx.commit();
        }
  • 결과
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.INSERT_MEMBER as INSERT_M2_3_0_,
        member0_.createdDate as createdD3_3_0_,
        member0_.UPDATE_MEMBER as UPDATE_M4_3_0_,
        member0_.lastModifiedDate as lastModi5_3_0_,
        member0_.TEAM_ID as TEAM_ID7_3_0_,
        member0_.USERNAME as USERNAME6_3_0_,
        team1_.TEAM_ID as TEAM_ID1_7_1_,
        team1_.INSERT_MEMBER as INSERT_M2_7_1_,
        team1_.createdDate as createdD3_7_1_,
        team1_.UPDATE_MEMBER as UPDATE_M4_7_1_,
        team1_.lastModifiedDate as lastModi5_7_1_,
        team1_.name as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
findMember.id = 1
findMember.username = hello

em.getReference() 예시 1

  • 코드
try {
            Member member = new Member();
            member.setUsername("hello");

            em.persist(member);

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

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

            tx.commit();
        }
  • 결과
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (INSERT_MEMBER, createdDate, UPDATE_MEMBER, lastModifiedDate, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?)

em.getReference() 예시 2

  • 코드
Member member = new Member();
            member.setUsername("hello");

            em.persist(member);

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

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

            tx.commit();
        }
  • 결과
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.INSERT_MEMBER as INSERT_M2_3_0_,
        member0_.createdDate as createdD3_3_0_,
        member0_.UPDATE_MEMBER as UPDATE_M4_3_0_,
        member0_.lastModifiedDate as lastModi5_3_0_,
        member0_.TEAM_ID as TEAM_ID7_3_0_,
        member0_.USERNAME as USERNAME6_3_0_,
        team1_.TEAM_ID as TEAM_ID1_7_1_,
        team1_.INSERT_MEMBER as INSERT_M2_7_1_,
        team1_.createdDate as createdD3_7_1_,
        team1_.UPDATE_MEMBER as UPDATE_M4_7_1_,
        team1_.lastModifiedDate as lastModi5_7_1_,
        team1_.name as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
findMember.username = hello

프록시 특징

  1. 실제 클래스를 상속받아서 만들어진다.
  2. 실제 클래스와 겉 모양이 같다.
  3. 사용하는 입장에서는 진짜 객체인지, 프록시 객체인지 구분하지 않고 사용하면 된다.(이론상)
  4. 프록시 객체는 실제 객체의 참조(target)를 보관한다.
  5. 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

프록시의 상속, 위임

 

프록시 객체의 초기화

Member member = em.getReference(Member.class, “id1”);
member.getName();

프록시 객체의 초기화 과정

 

프록시의 특징 정리

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

2. 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능

  • 2번 추가 설명 - 프록시는 교체되는 것이 아니고 내부의 타겟에 값이 채워지는 것
try {
           Member member = new Member();
            member.setUsername("hello");

            em.persist(member);

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

            //
//            Member findMember = em.find(Member.class, member.getId());
            Member findMember = em.getReference(Member.class, member.getId());
            System.out.println("before findMember = " + findMember.getClass());
            System.out.println("findMember.username = " + findMember.getUsername());
            System.out.println("after findMember = " + findMember.getClass());

            tx.commit();
        }
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (INSERT_MEMBER, createdDate, UPDATE_MEMBER, lastModifiedDate, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?)
before findMember = class hellojpa.Member$HibernateProxy$0q4UoZqm
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.INSERT_MEMBER as INSERT_M2_3_0_,
        member0_.createdDate as createdD3_3_0_,
        member0_.UPDATE_MEMBER as UPDATE_M4_3_0_,
        member0_.lastModifiedDate as lastModi5_3_0_,
        member0_.TEAM_ID as TEAM_ID7_3_0_,
        member0_.USERNAME as USERNAME6_3_0_,
        team1_.TEAM_ID as TEAM_ID1_7_1_,
        team1_.INSERT_MEMBER as INSERT_M2_7_1_,
        team1_.createdDate as createdD3_7_1_,
        team1_.UPDATE_MEMBER as UPDATE_M4_7_1_,
        team1_.lastModifiedDate as lastModi5_7_1_,
        team1_.name as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
findMember.username = hello
after findMember = class hellojpa.Member$HibernateProxy$0q4UoZqm

3. 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야한다.(== 비교 실패, 대신 instance of 사용)

  • 3번 추가 설명 - em.find()와 em.find()
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.find(Member.class, member2.getId());

System.out.println("m1 == m2 -> " + (m1.getClass() == m2.getClass()));
m1 == m2 -> true
  • 3번 추가 설명 - em.find()와 em.getReference()
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());

System.out.println("m1 == m2 -> " + (m1.getClass() == m2.getClass()));
m1 == m2 -> False

→ 실제로 업무를 할 때는 객체가 실제로 넘어오는지, 프록시로 넘어오는지 알 수 없다. 따라서 절대로 == 비교는 해서는 안된다. instance of를 사용하라.

try {
            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.getReference(Member.class, member2.getId());

            logic(m1, m2);

            tx.commit();
        }
...
private static void logic(Member m1, Member m2) {
        System.out.println("m1 == m2 -> " + (m1 instanceof Member));
        System.out.println("m1 == m2 -> " + (m2 instanceof Member));
    }
m1 == m2 -> true
m1 == m2 -> true

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

  • 이유 1 - 영속성 컨텍스트에 있는데 굳이 프록시로 가져와봐야 아무 이점이 없다. 원본을 반환하는 것이 성능 최적화에서도 더 이점이 있다.
  • 이유 2 - JPA에서는 자바 컬렉션에서 가져온 것을 == 비교하듯이 ==비교가 한 영속성 컨텍스트에서 가져오고 pk가 같으면 true를 반환한다.
  • 4번 추가 설명 1
try {
            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());

            tx.commit();
        }
                        System.out.println("a == a -> " + (m1 == reference));
m1 = class hellojpa.Member
reference = class hellojpa.Member
a == a -> true
  • 4번 추가 설명 2 - m1과 reference 모두 getReference를 사용해도 같은 프록시로 반환 → m1 == reference가 성립해야 하기 때문
try {
            Member member1 = new Member();
            member1.setUsername("member1");
            em.persist(member1);

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

            Member m1 = em.getReference(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 -> " + (m1 == reference));

            tx.commit();
        }
m1 = class hellojpa.Member$HibernateProxy$tv7w3yGh
reference = class hellojpa.Member$HibernateProxy$tv7w3yGh
a == a -> true
  • 4번 추가 설명 3 - 프록시로 한번 조회가 되면 em.find()에서도 프록시로 반환을 한다. == 비교를 맞추기 위해서.
  • 핵심은 프록시든 아니든 개발에 문제가 없게 하는 것이 중요함. 어떻게든 JPA에서는 refMember == findMember 비교를 true로 맞추려 한다.
try {
            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());

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

            tx.commit();
        }
refMember = class hellojpa.Member$HibernateProxy$LMOvPJAY
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.INSERT_MEMBER as INSERT_M2_3_0_,
        member0_.createdDate as createdD3_3_0_,
        member0_.UPDATE_MEMBER as UPDATE_M4_3_0_,
        member0_.lastModifiedDate as lastModi5_3_0_,
        member0_.TEAM_ID as TEAM_ID7_3_0_,
        member0_.USERNAME as USERNAME6_3_0_,
        team1_.TEAM_ID as TEAM_ID1_7_1_,
        team1_.INSERT_MEMBER as INSERT_M2_7_1_,
        team1_.createdDate as createdD3_7_1_,
        team1_.UPDATE_MEMBER as UPDATE_M4_7_1_,
        team1_.lastModifiedDate as lastModi5_7_1_,
        team1_.name as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
findMember = class hellojpa.Member$HibernateProxy$LMOvPJAY
a == a -> true

5. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.

(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터뜨린다.)

  • 5번 추가 설명 - detach(), close(), clear()를 사용할 시 영속성 컨텍스트가 날라감 → refMember는 영속성 컨텍스트의 도움을 받지 못함 → LazyInitializationException: could not initialize proxy 오류
try {
            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()); // Proxy

            em.detach(refMember); // 준영속
//            em.close();

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

            tx.commit();
        }
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (INSERT_MEMBER, createdDate, UPDATE_MEMBER, lastModifiedDate, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?)
refMember = class hellojpa.Member$HibernateProxy$1XjTqsMB
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$1XjTqsMB.getUsername(Unknown Source)
    at hellojpa.JpaMain.main(JpaMain.java:30)
Jun 23, 2023 9:46:22 PM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/test]

프록시 확인

1. 프록시 인스턴스의 초기화 여부 확인 - emf.getPersistenceUnitUtil().isLoaded(refMember));

  • 초기화 전 isLoaded = false
try {
            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()); // Proxy
            System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));

            tx.commit();
        }
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (INSERT_MEMBER, createdDate, UPDATE_MEMBER, lastModifiedDate, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?)
refMember = class hellojpa.Member$HibernateProxy$HQDEJLw0
isLoaded = false
  • 초기화 후 isLoaded = true
try {
            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()); // Proxy
            refMember.getUsername();
            System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));

            tx.commit();
        }
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (INSERT_MEMBER, createdDate, UPDATE_MEMBER, lastModifiedDate, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?)
refMember = class hellojpa.Member$HibernateProxy$KSgjPxUU
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.INSERT_MEMBER as INSERT_M2_3_0_,
        member0_.createdDate as createdD3_3_0_,
        member0_.UPDATE_MEMBER as UPDATE_M4_3_0_,
        member0_.lastModifiedDate as lastModi5_3_0_,
        member0_.TEAM_ID as TEAM_ID7_3_0_,
        member0_.USERNAME as USERNAME6_3_0_,
        team1_.TEAM_ID as TEAM_ID1_7_1_,
        team1_.INSERT_MEMBER as INSERT_M2_7_1_,
        team1_.createdDate as createdD3_7_1_,
        team1_.UPDATE_MEMBER as UPDATE_M4_7_1_,
        team1_.lastModifiedDate as lastModi5_7_1_,
        team1_.name as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
isLoaded = true

2. 프록시 클래스 확인 방법 - refMember.getClass().getName() 출력

System.out.println("refMember = " + refMember.getClass().getName()); // Proxy
System.out.println("refMember = " + refMember.getClass().getName()); // Proxy

3. 프록시 강제 초기화 - JPA 표준은 강제 초기화가 없다. Hibernate.initialize(refMember); 이렇게 사용

Member refMember = em.getReference(Member.class, member1.getId());
            System.out.println("refMember = " + refMember.getClass().getName()); // Proxy
            Hibernate.initialize(refMember); // 강제 초기화
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.INSERT_MEMBER as INSERT_M2_3_0_,
        member0_.createdDate as createdD3_3_0_,
        member0_.UPDATE_MEMBER as UPDATE_M4_3_0_,
        member0_.lastModifiedDate as lastModi5_3_0_,
        member0_.TEAM_ID as TEAM_ID7_3_0_,
        member0_.USERNAME as USERNAME6_3_0_,
        team1_.TEAM_ID as TEAM_ID1_7_1_,
        team1_.INSERT_MEMBER as INSERT_M2_7_1_,
        team1_.createdDate as createdD3_7_1_,
        team1_.UPDATE_MEMBER as UPDATE_M4_7_1_,
        team1_.lastModifiedDate as lastModi5_7_1_,
        team1_.name as name6_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?
반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...