반응형
05-14 05:47
Today
Total
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
관리 메뉴

개발하는 고라니

[JPA] Proxy 본문

Framework/JPA (Hibernate)

[JPA] Proxy

조용한고라니 2021. 7. 23. 19:16
반응형

Proxy

JPA에서 Proxy란, 실제 데이터를 갖고있는 Entity가 아니라 Entity의 class를 상속받는 객체이고, Entity를 가리킨다. 그래서 Proxy의 내부는 텅 비어있고, 실제 Entity를 가리키는 Entity target를 갖는다. 결과적으로 Proxy와 실제 class의 모습은 동일하며, 사용자 입장에서는 뭐가 class고 Proxy인지 구분하여 사용할 필요는 없다. 하지만 몇 가지 주의해야할 점은 존재한다.

 

Proxy 객체는 실제 객체의 참조를 보관한다.

그리고 Proxy객체를 호출하면 Proxy객체는 실제 객체의 메서드를 호출하게 된다.

 

실제 객체를 조회하면 DB에 쿼리(SELECT)를 날려 데이터를 가져오지만,

참조를 조회하면 DB에 쿼리를 날리지 않고 비어있는 Proxy 객체를 반환 받는다.

 

 Proxy 객체는 주로 추후 지연 로딩(Lazy Loading)으로 객체를 가져올 때 사용된다. 

Team team = new Team();
team.setName("A");
em.persist(team);

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

Long id = team.getId();

Team findTeam = em.find(Team.class, id); //실제 Team 객체 -> DB에 쿼리 날아감
System.out.println("findTeam.getClass() = " + findTeam.getClass());
//findTeam.getClass() = class domain.Team

//--------
em.clear();
//--------

Team refTeam = em.getReference(Team.class, id); //Proxy 객체 -> DB에 쿼리 X
System.out.println("refTeam.getClass() = " + refTeam.getClass());
//refTeam.getClass() = class domain.Team$HibernateProxy$oZKS5PD9

Proxy가 SQL을 날리는 시점

그렇다면, Proxy는 언제 DB에 쿼리를 날려 조회를 하게되는가? 바로 실제 Entity의 value가 사용되는 시점이다. 예를 들어 위의 코드에서, System.out.println(refTeam.getName()); 코드가 호출되면 그 때 Select 쿼리가 날아가게 된다.

Proxy 객체 초기화

Proxy에 값이 없을 때, DB에 조회해 실제 Entity 값을 만드는 과정

 

위에서 언급한 SQL을 날리는 것이 Proxy를 초기화하는 것이다. 사실 간단하지만 이에 대한 매커니즘은 조금 복잡할 수 있다.

 

  1. Team refTeam = em.getReference(Team.class, id); //Proxy 반환
  2. refTeam.getName(); //Proxy 초기화
  3. Proxy가 비어있으므로 초기화 요청
  4. 영속성 ContextDB를 조회해서 실제 Team 객체를 준비
  5. Proxy의 target과 실제 Team 객체를 연결해서, 실제 Team 객체의 getName()을 호출

 

Proxy 특징

Proxy의 특징을 알아보기 전 JPA가 반드시 보장해주는 특징하나를 먼저 보도록 하자.

 

JPA는 같은 트랜잭션, 같은 영속성 컨텍스트에서 동일한 엔티티를 2개 조회했다면, 그 2개는 항상 동일한 객체이다.

Team team1 = em.find(Team.class, 1L);
Team team2 = em.find(Team.class, 1L);

boolean same = team1 == team2; //항상 true

이를 잘 숙지하고 Proxy의 특징을 보자.

 

Proxy는 처음 1회만 초기화된다.

Long id = team.getId();

Team refTeam = em.getReference(Team.class, id);

//Proxy 초기화 -> DB에 쿼리 날아감
System.out.println("refTeam.getName() = " + refTeam.getName());

//이미 값이 있으므로 DB에 쿼리 날아가지 않음
System.out.println("refTeam.getName() = " + refTeam.getName());
    select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.id=?

refTeam.getName() = A
refTeam.getName() = A

 

Proxy를 초기화 할 때, Proxy -> Entity로 변경되는 것은 아니며 Proxy를 통해 Entity에 접근 가능

Long id = team.getId();

Team refTeam = em.getReference(Team.class, id);
System.out.println("refTeam = " + refTeam.getClass());

//Proxy 초기화
System.out.println("refTeam.getName() = " + refTeam.getName());

System.out.println("refTeam = " + refTeam.getClass());
refTeam = class domain.Team$HibernateProxy$mmW03jOz

    select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        Team team0_ 
    where
        team0_.id=?

refTeam.getName() = A
refTeam = class domain.Team$HibernateProxy$mmW03jOz

 

Proxy는 원본 Entity(여기선 Team)를 상속받으므로 Type Check시 ' == ' 대신 instanceof를 사용

Team team = new Team();
team.setName("A");
em.persist(team);

Team team1 = new Team();
team1.setName("B");
em.persist(team1);

//------ Team 2개 생성
em.flush();
em.clear();

Long id = team.getId();
Long id1 = team1.getId();

Team refTeam = em.getReference(Team.class, id); //Proxy
Team findTeam = em.find(Team.class, id1); //Entity

System.out.println("refTeam.getClass() = " + refTeam.getClass());
System.out.println("findTeam.getClass() = " + findTeam.getClass());
System.out.println("-----------------------------------------------");
System.out.println("(findTeam.getClass() == refTeam.getClass()) = " + (findTeam.getClass() == refTeam.getClass()));
System.out.println("(findTeam instanceof Team) = " + (findTeam instanceof Team));
System.out.println("(refTeam instanceof Team) = " + (refTeam instanceof Team));
refTeam.getClass() = class domain.Team$HibernateProxy$GqdWTzMK
findTeam.getClass() = class domain.Team
-----------------------------------------------
(findTeam.getClass() == refTeam.getClass()) = false
(findTeam instanceof Team) = true
(refTeam instanceof Team) = true

 

영속성 컨텍스트에 이미 Entity가 있으면 getReference() 해도 Entity가 반환,

영속성 컨텍스트에 이미 Proxy가 있으면 find() 해도 Proxy가 반환

 

- 이는 위에서 언급한 JPA의 매커니즘 특징을 지키기 위함으로 이해하면 편하다.

 

Long id = team.getId();

Team refTeam = em.getReference(Team.class, id);
Team findTeam = em.find(Team.class, id);

System.out.println("refTeam.getClass() = " + refTeam.getClass());
System.out.println("findTeam.getClass() = " + findTeam.getClass());

/*
refTeam.getClass() = class domain.Team$HibernateProxy$rJWs3JZk
findTeam.getClass() = class domain.Team$HibernateProxy$rJWs3JZk
*/

 

※ 나는 여기서 강한 궁금증이 생겼다. 영속성 컨텍스트에 Proxy가 이미 잇으면 find() 해도 Proxy를 반환한다. 이 명제에서.. 영속성 컨텍스트는 어떻게 Proxy를 반환한줄 알고 find() 해도 Proxy를 반환해주는거지..?

 

먼저 나는 영속성 컨텍스트에 대해 잘못 알고있었다. 간단히 엔티티 매니저- 영속성 컨텍스트 - (1차캐시 및 쓰기지연 SQL 저장소) 이렇게 있는 줄 알았는데, 영속성 컨텍스트에는 Proxy를 담고있는 Map이 존재했다. Key는 Entity의 Key로 하는 map. 즉, 엔티티를 저장하는 1차캐시가 있듯이, Proxy를 따로 관리하는 공간이 존재했던 것이다. 따라서 Proxy를 반환받고, 동일한 데이터에 대해 em.find()를 하면 Entity를 반환하기 전에 Proxy가 있는지 먼저 찾고, 있으면 Proxy를 반환하는 것이다.

 

그럼 반대로 영속성 컨텍스트에 Entity가 있을 때 getReference() 하면 Entity를 반환 받는 것 또한, em.getReference()를 하면 Proxy가 있는지 먼저 찾는게 아니라, 1차 캐시(Entity)를 먼저 찾고, 있으면 Entity를 반환하고, 없으면 Proxy를 생성하는 것으로 이해하였다.

 

인프런 질문 및 답변 링크 : https://www.inflearn.com/questions/264414

 

영속성 컨텍스트를 쓸 수 없는 준영속 상태이거나, EntityManager가 clear되었을 때 Proxy를 초기화 하려고 하면 예외가 발생한다.

 

- 이는 웹 개발 시 자주 만날 수 있는 예외라고 한다. LazyInitializationException이 발생하는데, 에러 메세지로 "no Session" 이라고 출력되며, 여기서 Session은 EntityManager를 말한다.

 

Long id = team.getId();

Team refTeam = em.getReference(Team.class, id);

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

refTeam.getName();
Long id = team.getId();

Team refTeam = em.getReference(Team.class, id);

em.detach(refTeam);

refTeam.getName();
org.hibernate.LazyInitializationException: could not initialize proxy [domain.Team#1] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:170)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:310)
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
at domain.Team$HibernateProxy$DaXN2Xzp.getName(Unknown Source)
at JpaTest.main(JpaTest.java:41)

 

#Reference

인프런 김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 본 강의는 자바 백엔

www.inflearn.com

 

반응형

'Framework > JPA (Hibernate)' 카테고리의 다른 글

[JPA] 벌크 연산 (Bulk Operation)  (0) 2021.08.05
[JPA] 페치 조인 (FETCH JOIN)  (0) 2021.08.05
[JPA] Cascade / OrphanRemoval  (0) 2021.07.25
[JPA] 상속관계 매핑  (0) 2021.07.23
[JPA] JPA란?  (0) 2021.07.15
Comments