프록시
jpa에서 프록시는 DB로부터 데이터를 조회할 때 사용된다.
일반적으로 DB에서 데이터를 조회하고, 해당 데이터에 대한 엔티티를 얻는 메소드는 EntityManger.find()이다.
EntityManger.find()는 DB를 통해 실제 엔티티 객체를 얻을 수 있는 것이다.
그에 반해 EntityManager.getReference()는 DB에서 데이터를 조회하지 않으며, 가짜(프록시) 엔티티 객체를 얻을 수 있다.
프록시 엔티티 객체 특징
- 프록시 엔티티 객체는 실제 엔티티 클래스를 상속 받아서 생성된다. 따라서 실제 엔티티 객체와 형태가 동일하다.
- 실제 엔티티 객체와 프록시 엔티티 객체의 차이점이 있다면, 프록시 엔티티 객체는 id를 제외한 속성의 값들이 비워져 있다. 그리고 실제 엔티티 객체(target)를 참조하고 있다.
- 프록시 객체에서 속성에 대해 접근하는 메소드를 호출하게 되면, 프록시 객체는 참조하고 있는 실제 엔티티 객체의 메소드를 호출한다. 이것이 프록시 엔티티 객체의 초기화 이다.
- 프록시 객체는 한번만 초기화된다.
- 초기화를 통해 프록시 객체가 실제 객체로 변하는 것이 아니다. 프록시 객체는 초기화된 후 실제 객체에 참조로 접근 가능하게 된다.
- 프록시 객체는 실제 객체, 즉 엔티티와 같은 타입이 아니다. 따라서 타입 체크 시 주의 해야한다. == 대신 instance of를 사용하자.
- 영속성 컨텍스트에 실제 객체가 이미 있으면, getReference()를 호출해도 실제 객체를 반환한다.
- 반대로 이미 특정 id에 대한 객체로 프록시 객체가 먼저 사용되었다면, find()도 프록시 객체를 반환한다.
- 준영속 상태에서 프록시를 초기화하면 예외가 발생한다. 영속성 컨텍스트의 기능을 사용할 수 없기 때문이다.
프록시 엔티티 객체의 초기화
프록시 엔티티 객체의 초기화 과정에 대해 좀 더 자세히 알아보자.
id, name을 속성으로 가진 Member 엔티티가 있고, id가 1인 Member 데이터가 DB에 저장되어있다고 가정하자.
- Member member = em.getReference(Member.class, 1L);을 통해 프록시 객체인 member를 얻는다.
- member.getName()을 통해 실제 속성 값들에 접근 할 때, 프록시 객체는 영속성 컨텍스트에 초기화 요청을 보낸다.
- 영속성 컨텍스트는 select 쿼리를 통해 DB에서 프록시 객체의 id에 해당하는 실제 데이터를 조회하고, 엔티티화 하여 실제 객체를 영속성 컨텍스트에 저장한다.
- member.target.getName()을 통해 실제 데이터를 얻는다.
지연 로딩
앞에서 프록시를 설명한 이유는 지연 로딩을 이해하기 위해서이다.
프록시를 이해했다면, 지연 로딩도 쉽게 이해할 수 있다.
지연 로딩은 특정 엔티티를 조회할 때, 연관 관계를 가진 엔티티의 데이터는 즉시 조회하지 않도록 하는 기능이다.
지연 로딩은 왜 필요한 기능일까?
Member와 Team이라는 연관 관계를 가진 엔티티가 존재한다고 가정하자.
Member에 대한 데이터만 필요하고 Team에 대한 데이터는 필요 없다고 하더라도, 즉시 로딩을 할 경우 join을 통해 Team에 대한 데이터까지 함께 조회하게 된다. 불필요한 join 쿼리가 함께 실행되는 것이다.
지연 로딩은 이러한 불필요한 join 쿼리 실행을 배제하기 위해 사용된다.
예시 코드
지연 로딩
Member 엔티티는 Team 엔티티와 @ManyToOne으로 연관 관계가 매핑되어있다.
@ManyToOne의 속성으로 fetch = FetchType.LAZY가 설정된 것을 볼 수 있다. 이것이 지연 로딩을 위한 설정이다.
LAZY에 해당하는 try ~ finally 내부를 살펴보자.
Member 데이터를 조회할 때, 지연 로딩이 설정된 연관 엔티티 Team은 실제 객체가 아닌 프록시 객체로 조회된다.
그리고 실제 Team의 값을 조회할 때, Team에 대한 select 쿼리가 따로 실행되면서 프록시 객체가 초기화된다.
즉시 로딩
Member 엔티티의 Team 참조 객체에 대해 fetch =FetchType.EAGER로 설정될 경우, 즉시 로딩이 설정된다.
EAGER에 대한 try ~ finally 내부를 살펴보자.
Member의 Team 참조 객체에 대해 즉시 로딩이 설정되어있으므로, Member를 조회할 때 join을 통해 한번에 Team의 데이터까지 조회된다. 즉 Team도 실제 객체로 조회된다.
@Entity
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "member_id")
private Long id;
@Column(name = "username")
private String name;
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩, Team을 프록시 객체로 조회한다. Member만 DB에서 조회한다.
// @ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
@JoinColumn(name = "team_id")
private Team team;
...
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
@Entity
public class Team extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>(); // 관례대로 ArrayList로 초기화 해둔다.
...
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
// LAZY
try {
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("memberA");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
// Team에 지연 로딩을 설정해주었다.
// Member만 DB에서 조회하고, Team은 프록시 객체로 생성한다.
Member m = em.find(Member.class, member.getId());
System.out.println("m.getTeam().getClass() = " + m.getTeam().getClass()); // 프록시 Team
// 실제 Team의 값을 조회할 때 Team을 조회하는 쿼리가 따로 실행된다. 즉 초기화된다.
m.getTeam().getName();
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
// EAGER
try {
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("memberA");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
// Team에 즉시 로딩을 설정해주었다.
// join을 통해 Member와 Team을 DB에서 함께 조회한다.
Member m = em.find(Member.class, member.getId());
System.out.println("m.getTeam().getClass() = " + m.getTeam().getClass()); // Team
tx2.commit();
} catch (Exception e) {
tx2.rollback();
} finally {
em2.close();
}
실무에서의 지연 로딩
실무에서는 가급적 모든 연관 관계에 대해 지연 로딩만 사용하는 것이 매우 권장된다.
- 즉시 로딩은 예상치 못한 쿼리를 발생시킬 수 있다.
- 즉시 로딩은 JPQL에서 N+1 문제를 유발한다.
- @ManyToOne, @OneToOne은 기본 값이 EAGER이므로 LAZY로 반드시 설정해주자.
- join을 통해 연관된 엔티티의 데이터도 함께 조회하고 싶으면, fetch join 또는 엔티티 그래프 기능을 사용하자.