🍃이 글은 inflearn에서 김영한의 스프링 부트와 JPA 실무 완전 정복 로드맵을 학습하고 작성한 것입니다.🍃
Member를 조회할 때 Team도 함께 조회해야 할까?
Team 정보가 필요할 땐 함께 조회해도 된다.
Team 정보가 필요하지 않을 때 함께 조회하면 낭비가 발생한다.
JPA는 이런 낭비를 하지 않기 위해, 프록시와 지연 로딩으로 해결한다.
JPA에서 em.find() 말고, em.getReference()라는 메서드도 제공된다.
em.find()는 DB를 통해서 실제 엔티티 객체를 조회하는 메서드이다.
Member findMember = em.find(Member.class, member.getId());
em.getReference() 는 DB의 조회를 미루는 가짜(프록시) 엔티티 객체를 조회하는 메서드이다.
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.username = " + findMember.getUsername());
호출하면 조회 쿼리가 발생하지 않고 프록시 객체(HibernateProxy)를 조회한다.
조회된 프록시 객체를 실제 사용할 때 조회 쿼리가 발생한다.
실제 클래스를 상속받아서 만들어진다.
실제 클래스와 겉모양이 같다.
사용하는 입장에서는 진짜인지 가짜(프록시)인지 구별하지 않고 사용한다.
프록시 객체는 실제 객체의 참조(target)를 보관한다.
프록시 객체를 통해 메소드를 호출하면, 프록시 객체는 target을 통해 실제 객체의 메소드를 호출한다.
프록시 객체의 초기화 메커니즘
프록시 객체는 처음 사용할 때 한 번만 초기화한다.
프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근하는 것이다.
프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크 시 주의해야 한다.
== 타입 비교는 실패한다.
instance of를 사용해야 한다.
JPA에서 사용하는 객체가 진짜인지 가짜(프록시)인지 판단하기 애매하기 때문에 타입 비교는 == 보단 instance of를 사용하자.
영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해
도 실제 엔티티 반환한다.
프록시 객체의 초기화는 영속성 컨텍스트를 통해 진행되기 때문에, 준영속 상태일 때는 예외가 발생한다.
하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트린다.
보통 트랜젹션의 시작과 끝을 영속성 컨텍스트의 시작과 끝으로 맞추기 때문에 트랜잭션 종료 후 프록시를 조회하면 예외가 발생한다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
@ManyToOne(fetch = "FetchType.Lazy")
팀의 이름을 조회해 보면 이때 실제 팀 객체 조회 쿼리가 나간다.
반대로 즉시 로딩은 Member를 조회할 때 Team을 같이 조회한다.
FetchType을 EAGER로 설정하면 된다.
대부분의 JPA 구현체는 가능하면 조인을 사용해서 SQL 한 번에 함께 조회하려고 한다.
지연 로딩만 사용해야 한다.
즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
@ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 LAZY로 설정한다.
@OneToMany, @ManyToMany는 기본이 지연 로딩이다.
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
문제 상황
개발자는 분명 한 번 members를 조회 했지만, n번의 쿼리가 더 발생하는 심각한 N+1 문제가 생겼다.
이러한 문제 때문에 지연 로딩을 사용해야 한다.
for (Member member : members) {
member.getTeam().getName();
}
물론 각 Member와 관련된 Team을 사용하기 위해, 위의 코드와 같이 사용한다면 똑같이 N+1문제가 발생한다.
이러한 지연 로딩에서 N+1 문제는 fetct join , entity graph , batch size로 해결한다.
참고: JPA는 내부적으로 리플랙션을 사용해서 동적으로 객체를 생성하기 때문에 기본생성자가 꼭 필요하다. 접근제어자를 public 말고 protected로 해서 보호하자. (기본 생성자의 접근 제어자를 private으로 걸면, 다음에 Lazy Loading 사용 시 Proxy 관련 예외가 발생할 수 있다.)