객체는 객체 그래프로 연관된 객체들을 탐색한다. 그런데 객체가 데이터베이스에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기는 어렵다. JPA 구현체들은 프록시라는 기술을 사용하여 연관된 객체를 실제 사용하는 시점에 데이터베이스에서 조회할 수 있다. 하지만 자주 함께 사용하는 객체들은 조인을 사용해서 함께 조회하는 것이 효과적이다. JPA는 즉시 로딩과 지연 로딩이라는 방법으로 둘을 모두 지원한다.
프록시 (Proxy)
엔티티를 조회할 떄 연관된 엔티티들이 항상 사용되는 것은 아니다. 엔티티를 조회할 때, 사용하지 않는 엔티티까지 데이터베이스에서 함께 조회해 두는 것은 효율적이지 않다. JPA는 이런 문제를 해결하려고 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연 로딩이라 한다. 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라 한다.
EntityManager.find()를 사용하여 조회할 때, 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회한다.
EntityManager.getReference()를 사용하면, 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미룬다. 대신에 데이터베이스 접근을 위임한 프록시 객체를 반환한다.
//EntityManager.find()
Member member1 = em.find(Member.class, "member1");
//EntityManager.getReference()
Member member2 = em.getReference(Member.class, "member2");
Java
복사
프록시의 특징
프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같아 사용하는 입장에서 진짜 객체인지, 프록시 객체인지 구분하지 않고 사용하면 된다. 프록시 객체는 실제 객체에 대한 참조를 보관한다. 그리고 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
프록시 객체의 초기화 과정
Member member1 = em.find(Member.class, "member1");
member.getName(); //MemberProxy 반환
Java
복사
1.
프록시 객체에 member.getName()을 호출해서 실제 데이터를 조회한다.
2.
실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 초기화라 한다.
3.
영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
4.
프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 멤버변수에 보관한다.
5.
프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다.
프록시의 특징 정리
•
프록시 객체는 처음 사용할 때 한 번만 초기화된다.
•
프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
•
프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
•
영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 프록시가 아닌 실제 엔티티를 반환한다.
•
초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다.
프록시 확인
JPA가 제공하는 PersistenceUnitUtil.isLoaded(Object entity) 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다.
•
초기화되지 않은 프록시 인스턴스 → false
•
이미 초기화되었거나 프록시 인스턴스가 아닐 경우 → true
조회한 엔티티가 진짜 엔티티인지 프록시로 조회한 것인지 확인하려면 클래스명을 직접 출력해보면 된다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
boolean isLoad = emf.getPersistenceUnitUtil().isLoaded(entity);
System.out.println("isLoaded = " + isLoad);
System.out.println("memberProxy = " + member.getClass().getName());
Java
복사
즉시 로딩과 지연 로딩
회원 엔티티와 연관된 팀 엔티티가 있다고 가정해본다. 프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용한다. JPA는 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있도록 두 가지 방법을 제공한다.
1.
즉시 로딩 : 회원 엔티티를 조회할 때 연관된 팀 엔티티도 함께 조회한다.
2.
지연 로딩 : 회원 엔티티만 조회해두고 팀 엔티티는 실제 사용하는 시점에 데이터베이스에서 조회한다.
즉시 로딩
@Entity
public class Member {
//...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
}
public class JpaMain {
public static void main(String[] args) {
//...
Member member1 = em.find(Member.class, "member1");
Team team = member.getTeam(); //이미 로딩된 팀 엔티티 반환
//...
}
}
Java
복사
em.find(Member.class, "member1"); 로 회원을 조회할 때 팀도 함꼐 조회한다. 이때 회원과 팀 두 테이블을 조회하므로 2번의 쿼리가 실행할 것 같지만, 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용하여 쿼리 한 번으로 모두 조회한다.
지연 로딩
@Entity
public class Member {
//...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
public class JpaMain {
public static void main(String[] args) {
//...
Member member1 = em.find(Member.class, "member1");
Team team = member.getTeam(); //프록시 객체
team.teamName(); //팀 객체
//...
}
}
Java
복사
em.find(Member.class, "member1") 를 호출하면 회원만 조회하고 팀은 조회하지 않는다. 대신에 조회된 회원의 team 멤버변수에 프록시 객체를 넣어둔다. 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미룬다. 실제 데이터가 필요할 때, 데이터베이스를 조회해서 프록시 객체를 초기화한다.
[참고 도서] 자바 ORM 표준 JPA 프로그래밍