JPA/JPA 기본

27. 프록시

sdafdq 2023. 10. 27. 12:24

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

뭐, 말 그대로 상황에 따라 다르다.

Member와 Team을 짝꿍처럼 쓴다면 함께 가져와야 하고,

Team을 사용하는 경우가 있다, 정도라면 Member만 조회해서 가져오는 게 낫다.

 

말 그대로 어떤 경우엔 Member만 가져오고(사용하지도 않는 Team을 가져오는 것은 성능 낭비니까)

어떤 경우엔 둘 다 가져오고 싶은 것이다.

 

EntityManager는 조회할 때,

em.find 뿐 아니라 em.getReference라는 것도 있다.

사용방법은 똑같다. 무슨 차이일까?

코드상에서 설명 하겠다.

 

먼저 em.find()

Member member = new Member();
member.setName("memberA");

em.persist(member);

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

em.find(Member.class, member.getId());
tx.commit();

뭐 원래 하던 대로이다.

persist로 영속성 컨텍스트에 저장해 뒀다가,

flush하면 쿼리를 db에 날린다.

그렇게 insert 쿼리가 나가고,

em.find()해서 찾는 쿼리 select를 해서 가져온다.

Hibernate: 
    /* insert for
        jpabook.jpashop.domain.Member */insert 
    into
        Member (city,createdDate,lastModifiedDate,name,street,zipcode,MEMBER_ID) 
    values
        (?,?,?,?,?,?,?)
Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.city,
        m1_0.createdDate,
        m1_0.lastModifiedDate,
        m1_0.name,
        m1_0.street,
        m1_0.zipcode 
    from
        Member m1_0 
    where
        m1_0.MEMBER_ID=?

 

다음은 getReference로 해 보겠다.

Member member = new Member();
member.setName("memberA");

em.persist(member);

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

em.getReference(Member.class, member.getId());
tx.commit();

em.find()를 getReference로만 바꾼 것이다.

해 보면

Hibernate: 
    /* insert for
        jpabook.jpashop.domain.Member */insert 
    into
        Member (city,createdDate,lastModifiedDate,name,street,zipcode,MEMBER_ID) 
    values
        (?,?,?,?,?,?,?)

insert 쿼리만 나간다.

그런데,

Member member = new Member();
member.setName("memberA");

em.persist(member);

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

Member findedMember = em.getReference(Member.class, member.getId());
System.out.println("===== before use find member");
System.out.println("member name = " + findedMember.getName());
System.out.println("===== after use find member");
tx.commit();
Hibernate: 
    /* insert for
        jpabook.jpashop.domain.Member */insert 
    into
        Member (city,createdDate,lastModifiedDate,name,street,zipcode,MEMBER_ID) 
    values
        (?,?,?,?,?,?,?)
===== before use find member
Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.city,
        m1_0.createdDate,
        m1_0.lastModifiedDate,
        m1_0.name,
        m1_0.street,
        m1_0.zipcode 
    from
        Member m1_0 
    where
        m1_0.MEMBER_ID=?
member name = memberA
===== after use find member

이렇게 조회해온 값에 접근하는 순간,

그제서야 select 쿼리가 나간다. (물론 id접근은 우리가 이미 인자로 줬기 때문에 select 쿼리가 나가지 않는다. 우리가 준 걸 참조하면 되니까.)

 

정말 편리하다. 접근하기 전까지, 조회를 미뤄두는 것이다.

이러면 접근하지 않는다면 조회해오지도 않는다.

즉, 사용하지 않을 데이터를 조회해 오는 것을 방지할 수 있다.

 

저 가져온 findedMember의 클래스를 한번 출력해 보면

System.out.println("findedMember = " + findedMember.getClass());
findedMember = class jpabook.jpashop.domain.Member$HibernateProxy$lZie2jsB

뭔가 로그에 프록시라고 찍힌다.

 

하이버네이트의 프록시이다.

그냥 Member 객체가 아니라, 그 Member를 상속받은 프록시 객체를 준다.

 

쉽게 생각하면 Member에 프록시를 씌워 만들었다고 보면 된다. (상속 받았다고 보면된다.)

em.getReference() 하면 저 프록시로 감싼 객체를 주는 것이다.

빈 껍데기를 준다고 생각하면 된다.

 

 

프록시의 특징

실제 클래스를 상속받아 만들어 진다.

실제 클래스와 겉모습이 같다.

사용하는 입장에선 진짜와 프록시 객체와 구분하지 않고 사용하면 된다. 그런데 좀 주의할 점이 있다.

프록시는 실제 객체를 참조로써 보관한다.

저렇게 가지고 있다.

Entity target 해서 내부에서 필드로 실제 객체를 가지고 있는 모양이다.

프록시에서 getName()을 호출하면,

target.getName()을 호출해 준다.

 

근데, 아무래도 처음에는 위처럼 null인 상태이다.

왜냐하면 조회한 적 없기 때문.

 

과정을 보면 이렇다.

getReference() 해서 빈 Entity가 있는 프록시 객체를 가지고 있다가,

접근하려 하면,

영속성 컨텍스트에 target에 대한 초기화를 요청한다. 영속성 컨텍스트에서 쓰기지연 SQL 저장소도 있으니 이것 또한 그런 식으로 어디 저장소에 저장해 뒀다가 지연시킬 수 있는 것 같다.

 

그러면 DB에서 조회해서 영속성 컨텍스트에 넣고,

그걸 Entity로 만들어 프록시의 target에 집어넣어 주는 것 같다.

 

그 이후는 이제 target에 값이 있으니 영속성 컨텍스트에 초기화를 요청할 필요가 없다.

Member member = new Member();
member.setName("memberA");

em.persist(member);

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

Member findedMember = em.getReference(Member.class, member.getId());
System.out.println("===== before use find member");
System.out.println("member name = " + findedMember.getName());
System.out.println("===== after use find member");
System.out.println("member name = " + findedMember.getName());

select 쿼리 한번만 나간다.

 

프록시 특징 2

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

프록시 객체가 초기화 후 실제 Entity로 바뀌는 것이 아니고, 프록시 내부에 있는 target에 실제 Entity가 넣어지는 거임.

프록시 객체는 원본 엔티티를 상속받는 것 이기 때문에, == 비교 말고 instance of 를 사용해야 한다. 우리가 받은 건 원본 클래스를 상속받아 만든 프록시 객체 이므로.

그래서 사실 JPA 자체가 타입 비교할 때 == 대신 instance of를 사용하는 것이 좋다. (아니 근데 == 자체가 타입 비교가 아니고 레퍼런스 비교 아니었나? 그래서 맞지 않을텐데.. 아, getClass() 말하는 건가 보다. 그러면 == 비교말고 instance of 해야 함.)

member instanceof Member

이렇게

 

그리고, 이미 영속성 컨텍스트에 Entity가 있으면, getReference()를 해도 실제 엔티티를 반환한다.

무슨 말이냐 하면,

Member member = new Member();
member.setName("memberA");

em.persist(member);

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

Member findMember = em.find(Member.class, member.getId());
Member referenceMember = em.getReference(Member.class, member.getId());

System.out.println("findMember = " + findMember.getClass());
System.out.println("referenceMember = " + referenceMember.getClass());

tx.commit();

이렇게 하면,

둘다 프록시가 아니고 그냥 Member이다.

왜냐하면, 이미 em.find()로 조회해서 영속성 컨텍스트에 넣어놨기 때문이다.

그럼 굳이 다시한번 조회해올 필요가 없다.

@Id 덕분에 id도 무결성이라 

그러니까, find나 getReference나 순서가

먼저 영속성 컨텍스트에서 인자로 준 id로 조회해 보고, 거기에 있으면 반환해 준다.

그런데 데이터가 없으면 그제서야 조회를 하는 것이다. 

조회하는 타이밍도 지연처리 시켜서 commit 시점이나 접근하는 시점에.

영속성 컨텍스트는

이런 식으로 구성되어 있다.

 

또 그럼 반대로

Member member = new Member();
member.setName("memberA");

em.persist(member);

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

Member referenceMember = em.getReference(Member.class, member.getId());
System.out.println("====== after reference =======");
Member findMember = em.find(Member.class, member.getId());

System.out.println("findMember = " + findMember.getClass());
System.out.println("referenceMember = " + referenceMember.getClass());

tx.commit();

이렇게 getReference() 먼저 하면 find() 도 같은 프록시 객체로 가져온다.

 

이렇게 되게끔 한 이유는, (아마 영속성 컨텍스트에 프록시 그 객체가 엔티티로써 저장되지 않을까?) 그 왜 DB를 자바 객체처럼 쓰고 싶다고 == 하면 같은 객체 나오게끔 하도록 그거 맞출려고 이렇게 한 것 같다.

 

뭐 프록시를 쓰든 프록시가 아닌 객체를 쓰든, 프록시가 원래 객체를 상속받아 만든거기 때문에, 상관은 없다.

근데 주의할 점이 있다고는 했다.

그게 instance of를 사용해야 한다는 것.

 

그리고, 

여기서 보다시피, 프록시의 초기화는 영속성 컨텍스트 -> DB에 걸쳐 이루어 진다.

근데 만약, 

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.detach(refMember);
System.out.println("refMember = " + refMember.getName());

tx.commit();

이렇게 한다면.

프록시의 초기화는 영속성 컨텍스트를 통해 이루어 지는데, 프록시의 값을 참조하여 지연로딩을 통해 프록시를 초기화 하기 전에, 그 중간단계인 영속성 컨텍스트에서 퇴출시켜 버리면,

org.hibernate.LazyInitializationException: could not initialize proxy [hellojpa.Member#202] - no Session
	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:164)
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:310)
	at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:44)
	at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:102)
	at hellojpa.Member$HibernateProxy$qGMU9l6D.getName(Unknown Source)
	at hellojpa.JpaMain.main(JpaMain.java:32)
10월 28, 2023 4:42:07 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PoolState stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/./test]

이렇게 프록시를 초기화할 수 없다고 오류가 난다.

아, 이거는 e.printStackTrace() 해서 catch에서 에러가 났을 때 잡아서 쌓인 오류 스택을 본 것이다.

 

이를 위해서, 프록시 확인을 위한 여러 유틸리티가 있다.

 

emf.PersistenceUnitUtil.isLoaded(프록시)

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("is Loaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));

tx.commit();

EntityManagerFactory에서 얻을 수 있고, 그 유틸에서 isLoaded는 말 그대로 로드 되었냐 안되었냐, 프록시 입장에서는 불러오는게 초기화니 

위의 코드에서는 아직 프록시의 값에 접근하지 않았으므로 초기화 되지 않았다.

즉, false가 나온다.

 

Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember name= " + refMember.getName());
System.out.println("is Loaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));

이렇게 하면 true 나온다.

 

또, 아예 프록시를 강제초기화 하는 방법도 있다.

이건 JPA 표준이 아니라 하이버네이트에서 제공해주는 기술이다.

Member member1 = new Member();
member1.setName("member1");
em.persist(member1);

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

Member refMember = em.getReference(Member.class, member1.getId());
Hibernate.initialize(refMember);

tx.commit();

이렇게 하면 select 쿼리가 나간다.

 

JPA 강제 초기화는 그냥

member.getName() 이런 식으로 하면 된다 그냥.

 

참고로 이 강제초기화도, 당연히 영속성 컨텍스트에 없으면 안된다.

퇴출되어 있으면 마찬가지로 초기화 불가 오류가 나온다.

 

 

그리고, 사실 getReference()는 잘 안쓴다. 그냥 이해를 위해.

'JPA > JPA 기본' 카테고리의 다른 글

29. 영속성 전이와 고아 객체  (0) 2023.10.28
28. 즉시로딩, 지연로딩  (0) 2023.10.28
25. Mapped Superclass 매핑 정보 상속  (0) 2023.10.27
24. 상속관계 매핑  (0) 2023.10.26
23. 실전3 다양한 연관관계 매핑  (0) 2023.10.25