🌱 SPRING/JPA

[JPA] 프록시

1HOON 2020. 7. 29. 15:51

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
public class Member
{
    @Id @GeneratedValue
    private Long id;
 
    private Team team;
}
 
 
@Entity
public class Team
{
    @Id
    private Long id;
}
cs

 

이런 연관관계의 엔티티가 존재할 때, 아래 상황을 가정해보겠습니다.

 

Case 1. Member와 Team을 함께 출력하는 비즈니스 로직이 추가되었다.

이 때는 아래와 같이 로직을 구현할 수 있겠습니다.

JPA에서 Member 조회 시, 자동으로 Team까지 함께 조회해오므로, EntityManager.find 호출 시에 Member와 Team을 조인해서 가져오는 SELECT 쿼리를 수행하게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
/**
 * Member를 Team과 함께 출력한다.
 */
public void printMemberWithTeam()
{
    Member findMember = EntityManager.find(Member.class, 1L);
    Team team = findMember.getTeam();
 
    System.out.println(findMember.toString());
    System.out.println(team.toString());
}
cs

 

그렇다면, 아래 경우에는 어떨까요?

Case2. Member만 출력하는 비즈니스 로직이 추가되었다.

Case2의
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

 

경우에는 Member 엔티티만을 활용하지만, JPA에서 연관관계 매핑되어있는 Team도 같이 조회하므로 불필요한 데이터도 함께 조회됩니다. 예시의 경우 단순히 Member-Team으로 구성되어 괜찮아 보일 수도 있지만, 엔티티 간 연관관계가 늘어날수록 문제는 커집니다.

 

JPA에서는 이러한 문제를 프록시와 지연 로딩(Lazy Loading)으로 해결할 수 있는데, 이번 포스팅에서는 프록시에 대해 알아봅니다.

 

 

프록시


1
EntityManager.getReference(Member.class, 1L);
cs

프록시는 기존의 find 메서드가 아닌 getReference 메서드를 통해 사용할 수 있습니다.

 

getReference 메서드를 호출하면 JPA에서는 데이터베이스에 SQL을 질의하는 대신, 가짜 엔티티 객체를 반환해줍니다. 그리고 이 가짜 엔티티 객체를 참조할 때, SQL을 질의하게 됩니다.

 

  • EntityManager.find(Member.class, 1L)
    Member를 조회하면서 Team도 같이 조회합니다.
  • EntityManager.getReference(Member.class, 1L)
    메서드 호출 시점에는 SQL이 질의되지 않고, 반환된 객체를 사용(참조)할 때 질의됩니다.
    이때, 반환된 객체는 실제 엔티티 클래스(Member)가 아니고 Hibernate에서 만든 프록시(가짜) 클래스입니다.

 

앞선 사례 중 Case2로 예를 들면, 아래와 같이 동작하게 됩니다.

1
2
3
4
5
6
7
8
public void printOnlyMember()
{
    // SQL 질의 X
    Member findMember = EntityManager.getReference(Member.class, 1L);
    
    // SQL 질의 O
    System.out.println(findMember.toString());
}
cs

 

그런데, 코드를 자세히 보시면 이상한 부분이 있을 겁니다.

바로 getReference 메서드에서 반환하는 객체는 엔티티가 아니고 프록시 객체일 텐데 Member 변수로 담고 있는 부분입니다.

그 이유는 아래 프록시의 특징을 보시면 이해되실 겁니다.

 

 

프록시의 특징


  • 프록시는 실제 클래스를 상속받아 만들어집니다.
  • 프록시는 실제 클래스와 겉모양이 같습니다.
  • 따라서, 사용할 때는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됩니다.

프록시 객체 참조 시 SQL 질의 프로세스

  • 프록시 객체는 실제 객체의 참조(target)를 보관합니다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메서드를 호출합니다.
    -> 프록시.getName() 을 호출하면 프록시에서 target.getName() 을 호출합니다.
  • 프록시 객체는 처음 사용할 때 한 번만 초기화됩니다.
  • 프록시 객체를 초기화한다고 해서 프록시 객체가 실제 엔티티 객체로 바뀌는 것은 아닙니다.
    초기화가 되면, 프록시 객체를 통해 실제 엔티티에 접근 가능합니다.
  • 프록시 객체는 원본 엔티티를 상속받기 때문에, 타입 체크 시 주의해야 합니다.
    == 비교는 실패합니다. 대신 instance of를 사용해야 합니다.
    ▶ 로직에서 프록시를 쓸지 안 쓸지 확신할 수 없기 때문에 타입 비교 시에는 반드시 instace of를 사용하는 것을 추천합니다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 getReference 메서드를 호출했더라도 실제 엔티티를 반환합니다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 예외가 발생합니다.
    예) Hibernate의 경우 org.hibernate.LazyInitializationException 발생

출처 :: 인프런 강의(자바 ORM 표준 JPA 프로그래밍 - 기본편)

반응형