개발자꿈나무

프록시 본문

자바/JPA

프록시

망재이 2023. 8. 22. 15:30
Member -> Team 단방향 연관관계를 가지고 있다고 가정했을 때, 과연 Member를 조회할 때마다 매번 Team을 조회하는 것이 옳을까?
//회원과 팀 정보를 출력하는 메소드
public void printUserAndTeam(String memberId) {
    Member member = em.find(Member.class, memberId);
    Team team = member.getTeam();
    System.out.println("member.username : " + member.getName());
    System.out.println("member.team : " + team.getName());
}

//회원 정보만 출력하는 메소드
public void printUserName(String memberId) {
    Member member = em.find(Member.class, memberId);
    System.out.println("member.username : " + member.getName());
}

- 위 로직처럼 회원과 팀을 함께 출력하는 것이 아니라 회원 정보만 출력한다고 할때 Team 객체는 전혀 사용하지 않는 것을 알 수 있다. 그렇다면 과연 Member 객체를 조회할 때마다 Team 객체를 조회하는 것은 효율적이지 않을 수 있다. JPA는 이를 해결하기 위해 실제로 엔티티가 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이를 지연 로딩이라고 한다.

 

 

★ 프록시 기초

실제 데이터베이스를 조회하여 엔티티를 반환하는 메소드는 em.find(); 이다. 그러나 실제 엔티티를 사용할 때까지 데이터베이스 조회를 미루고자 한다면 em.getReference() 메소드를 사용하면 된다. 이 메소드를 호출하면 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체가 아니라 프록시 객체를 반환한다.

Member member = em.find(Member.class, "id1");
Member member = em.getReference(Member.class, "id1");

 

 

★ 프록시 특징

- 프록시는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉모습이 같다.

- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.

- 프록시 객체는 실제 객체에 대한 참조(target)을 보관한다.

- 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

 

 

★ 프록시 객체의 초기화

- 프록시 객체는 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라고 한다.

Member member = new Member();
member.setName("test1");
em.persist(member);

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

Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.name = " + findMember.getName()); //실제 쿼리가 여기서 날아감

- 쿼리가 보내진 결과를 보면 실제 데이터베이스를 조회해서 결과가 필요한 getName()이 실행된 순간 select 쿼리를 이용해서 데이터베이스를 조회하는 모습을 확인할 수 있다.

더보기

식별자의 경우 getReference() 를 실행할 때 식별자 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.

따라서 getId()를 호출하더라도 초기화 되는 것이 아니라 프록시 객체가 가지고 있는 식별자 값이 반환된다.

  1. 프록시 객체에 member.getName() 메소드를 호출해서 실제 데이터를 조회한다.
  2. 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청한다. -> 초기화
  3. 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
  4. 프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 멤버변수에 보관한다.
  5. 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다.

 

 

★ 정리

  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
  • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니며, 프록시 객체가 초기화되면 프록시 객체를 통해 실제 엔티티에 접근할 수 있다.
  • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다. (== 비교를 하는 것이 아니라 instance of를 사용해야 한다.)
Member member1 = new Member();
member1.setName("test1");
em.persist(member1);

Member member2 = new Member();
member2.setName("test2");
em.persist(member2);

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

Member refMember = em.getReference(Member.class, member1.getId());
Member findMember = em.find(Member.class, member2.getId());

System.out.println("m1 == m2 : " + (refMember.getClass() == findMember.getClass()));

- member1과 member2를 ==으로 비교하게 되면 member1은 프록시 객체, member2는 실제 엔티티이므로 false가 나오게 된다. 이 상황에선 당연한 결과라고 예상하지만 만약 비즈니스 로직이 이렇게 구성되어 있으면 어떨까?

method(member1, member2);


private static void method(Member m1, Member m2) {
	System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass()));
} // -> false

- 매개변수를 받을 때 프록시 타입이 아니라 Member 타입을 받아오는데 언제 프록시 객체가 넘어올지 모르므로 타입 비교는 ==를 써서는 안된다. 대신에 instance of를 이용해서 타입 비교를 해줘야한다.

method(member1, member2);


private static void method(Member m1, Member m2) {
	System.out.println("m1 == m2 : " + (m1 instanceof Member));
    System.out.println("m1 == m2 : " + (m2 instanceof Member));
} // -> true , true
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
Member member = new Member();
member.setName("test1");
em.persist(member);

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

Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.class : " + findMember.getClass());
// class hellojpa.model.entity.Member

Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember.class : " + refMember.getClass());
// class hellojpa.model.entity.Member

System.out.println("m1 == m2 : " + (refMember == findMember)); // true

- 영속성 컨텍스트에 이미 찾는 엔티티가 있으면 굳이 지연로딩을 사용해도 아무런 효과가 없으므로 실제 엔티티를 반환하지만, 또다른 이유로는 JPA는 같은 레벨의 트랜잭션(영속성 컨텍스트) 안에서 ==는 항상 true가 나와야 하기 때문이다. refMember가 프록시 객체일 경우 m1 == m2가 false이므로 실제 엔티티를 반환하는 것이다.

Member member = new Member();
member.setName("test1");
em.persist(member);

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

Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember.class : " + refMember.getClass());
// class hellojpa.model.entity.Member$HibernateProxy$H3OKX4fk

Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.class : " + findMember.getClass());
// class hellojpa.model.entity.Member$HibernateProxy$H3OKX4fk

System.out.println("m1 == m2 : " + (refMember == findMember)); // true

- 그런데 만약 프록시를 호출하고 난 이후에 em.find를 호출하면 어떻게 될까? JPA는 위 로직에서 m1 == m2 : true가 나와야 하므로 findMember 역시 프록시 객체를 반환한다. 실제 실무에서 이렇게까지 복잡하게 쓰일 일은 잘 없지만 JPA의 기본 매커니즘에 대해서 이해를 하고 있어야 하는 부분이다. 프록시 역시도 실제 엔티티를 상속받은 객체이므로 실제 개발할 때는 엔티티던 프록시던 크게 상관은 없지만 타입 비교를 할 때만 주의해주면 될 것 같다.

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 예외가 발생한다. -> em.close() 혹은 em.detach()

 

 

★ 프록시 확인

  • 프록시 인스턴스 초기화 여부 확인
    PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인
    entity.getClass().getName() -> ..javasist.. or HibernateProxy ..
  • 프록시 강제 초기화
    org.hibernate.Hibernate.intialize(entity);
728x90

'자바 > JPA' 카테고리의 다른 글

영속성 전이, 고아 객체  (0) 2023.08.22
지연 로딩과 즉시 로딩  (0) 2023.08.22
실전 예제 - 상속관계 매핑  (0) 2023.08.21
복합 키 매핑  (0) 2023.08.20
@MappedSuperclass  (0) 2023.08.18