본문 바로가기
[IT]/JPA

[JPA] JPA 프록시

by dop 2021. 3. 22.

JPA 프록시

다음과 같이 Team 엔티티와 Member엔티티가 1:N 연관관계에 있다고 생각해보자.

 위와 같이 Member 엔티티에는 Team이라는 타입의 필드 값이 존재하게 된다면, Member 엔티티를 조회할 때마다 매핑된 Team에 대한 정보도 같이 조회될 것이다. 즉, 관계된 모든 테이블에 쿼리가 발생하고 연관관계가 많아지면 상상할 수 도 없을 만큼의 select 쿼리가 발생하게 될 것이다.

추가로 Member 객체를 조회했지만 당장 Team에 대한 정보를 필요로 하지 않는 경우도 있을 것이다. 이때도 Team에 대한 쿼리가 발생하는데, JPA는 프록시 개념을 이용해 쿼리가 발생하지 않도록 처리한다.

 

EntityManager의 getReference(class, Object)라는 메소드를 호출하면 해당 클래스를 상속받은 프록시 객체가 할당된다.

Proxy 객체는 겉으로 봤을 때 원본 객체와 동일하게 보인다. Proxy 객체를 통해 원본 객체의 필드, 메소드 등을 직접 호출것처럼 보이지만, 사실 Proxy 객체 내부의 target이라는 값을 통해 원본 객체에 접근한다.

쉽게 말해 호출 권한을 갖고 있어서, 우리는 원본 객체처럼 사용하면 된다는 이야기다. (하지만...)

EntityManager em;

User user1 = em.getReference(User.class, userId); // user 쿼리 발생 전
logger.info("getReference 호출");
String username = user1.getName(); // DB에 쿼리가 날아간다.
logger.info("user의 getName() 호출");

호출결과

보이는 바와 같이 user1.getName()을 호출한 이후에 쿼리가 발생하는 것을 볼 수 있다. 즉, Proxy 객체의 값을 접근하기 전까지는 DB의 리소스를 사용하지 않아도 된다는 말이다. 그리고 이렇게 Reference로 할당받은 객체를 getClass() 값을 받아보면 User$HibernateProxy$FaEhuhGe라는 값을 반환한다. 뒤에 붙은 FaEhuhGe는 할당받을 때마다 다른 값으로 변경된다. 여기서 주의할 점은 원본 객체인 User와 ( == ) 비교했을 때 false 값이 반환되는 것에 주의해야 한다. 앞서 언급한 바와 같이 원본 객체를 상속받았기 때문에 객체 비교를 원한다면 instance of 연산을 활용하는 습관을 들이는 것이 좋다.

 

Proxy객체의 get메소드가 호출되었을 때 다음과같이 5단계의 과정을 거쳐 값을 얻게 된다.

  1. proxy 객체의 get메소드 호출로 실제 데이터 조회
  2. 아직 실제 엔티티가 생성되지 않은 경우 영속성 컨텍스트에 엔티티 생성을 요청 (초기화 요청)
  3. 영속성 컨텍스트는 DB에서 해당하는 엔티티를 조회(Proxy 객체로 저장)
  4. Proxy 객체는 실제 엔티티의 참조를 target 멤버 변수에 보관
  5. Proxy 객체가 실제 엔티티의 get메소드 호출해서 결과 반환

+) getReference와 find 호출 순서에 따른 결과

 

Proxy의 get메소드를 호출하면 영속성 컨텍스트에 해당 엔티티가 저장되게 된다. 그런데, get메소드 호출전에 find로 해당객체를 요청받으면 이 객체는 Proxy 객체일까? 원본 객체일까? 

 

답은 find로 요청했지만 proxy 객체를 할당받게 된다. 이미 영속성 컨텍스트에 Proxy 객체 상태로 존재하기 때문에 새로운 원본 객체를 보관하게 되면, 영속성 관리가 어려워지게 된다.

 

반대로 find를 먼저 호출해서 영속성 컨텍스트에 원본객체가 존재한다고 하면, getReference로 할당받을지언정 Proxy 객체가 아닌 원본객체를 할당받게 된다! (영속성 컨텍스트 선에서 해결되었으니 DB 쿼리도 발생하지 않는다)

 

 

getReference() 이후 find()

User user1 = em.getReference(User.class, userId);
User user2 = em.find(User.class, userId);

logger.warn(user1.getClass() + "");
logger.warn(user2.getClass() + "");
logger.warn("geReference() 를 먼저 한 경우 둘다 프록시 객체로 통일 된다");

getReference / find

 

find() 이후 getReference()

User user1 = em.find(User.class, userId);
User user2 = em.getReference(User.class, userId);
logger.warn(user1.getClass() + "");
logger.warn(user2.getClass() + "");
logger.warn("find() 를 먼저 한 경우 둘다 원본 객체로 통일 된다");

find / getReference

 

getReference메소드를 직접 활용하는 경우는 거의 없지만 다음에 등장하는 즉시 로딩/지연 로딩 개념에서 반드시 알고 있어야 하는 내용이다. 

728x90