지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라고 한다.
프록시 특징 프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같다.
프록시 객체의 초기화 프록시 객체는 member.getName()처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라 한다.
프록시의 특징
- 프록시 객체는 처음 사용할 때 한번만 초기화된다.
- 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
- 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
- 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다.
준영속 상태와 초기화
Member meber = em.getReference(Member.class, "id1");
transaction.commit;
em.close(); //영속성 컨텍스트 종료
member.getName() // 준영속 상태 초기화 시도
//org.hibernate.LazyInitializationException 예외 발생
member.getName()을 호출하면 프록시를 초기화 해야하는데 영속성 컨텍스트가 없으므로 실제 엔티티를 조회할 수 없다. 따라서 예외가 발생
프록시와 식별자
엔티티를 프록시로 조회할 때 식별자값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.
Team team = em.getReference(Team.class, "team1");
team.getId(); // 초기화 되지 않음
프록시 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회하는 team.getId()를 호출해도 프록시를 초기화 하지 않는다. 단 엔티티 접근 방식을 프로퍼티로 설정한 경우에만 초기화하지 않는다.
엔티티 접근 방식을 필드(@Access(AccessType.Field))로 설정하면 JPA는 getId()메소드가 id만 조회하는 메소드인지 다른 필드까지 활용해서 어떤 일을 하는 메소드인지 알지 못하므로 프록시 객체를 초기화 한다.
프록시 확인
PersistenceUnitUtil.isLoaded(Object entity) 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다. 아직 초기화 하지 않는 프록시 인스턴스는 false를 반환한다.
boolean isLoad = em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(entity);
or
boolean isLoad = emf.getPersistenceUnitUtil().isLoaded(entity);
NULL 제약 조건과 JPA 조인전략
즉시 로딩 실행 SQL에서 JPA가 내부 조인(INNER JOIN)이 아닌 외부 조인(LEFT OUTER JOIN)을 사용하는데, 이는 외래키 필드의 값이 NULL을 허용하느냐 하지 않느냐에 따라 달라진다.
만약 회원이 팀을 가질 수도 있지만, 팀이 없을 수도 있을때 내부 조인을 사용하게 되면 에러가 발생한다. 그래서 JPA는 기본적으로 외부조인을 통해 조인을 가져온다.
만약 외래키를 무조건 갖는다면 성능과 최적화를 위해 내부조인을 사용하는게 좋다. 설정방법은 아래와 같다.
@Entity
public class Member{
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID", nullable = false)
private Team team;
}
- JoinColumn(nullable = true): NULL 허용(기본값), 외부조인 사용
- JoinColumn(nullable = false): NULL 허용하지 않음, 내부조인 사용
또는
public class Member{
@ManytoOne(fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
프록시와 컬렉션 래퍼
지연로딩으로 설정하면 실제 엔티티 대신에 프록시 객체를 사용한다. 하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리 할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데 이것을 컬렉션 래퍼라고 한다.
Member member = em.find(Member.class, "member1");
List<Order> orders = member.getOrders();
System.out.println("orders = " + orders.getClass().getName());
//결과: orders = org.hibernate.collection.internal.PersistentBag
엔티티를 지연 로딩하면 프록시 객체를 사용해서 지연 로딩을 수행하지만 주문 내역 같은 컬렉션은 컬렉션 래퍼가 지연 로딩을 처리해준다.
참고로 member.getOrders()를 호출해도 컬렉션은 초기화 되지 않는다. 컬렉션은 member.getOrders().get(0)처럼 컬렉션에서 실제 데이터를 조회할 때 데이터베이스를 조회해서 초기화 한다.
컬렉션에 FetchType.EAGER 사용시 주의점
- 컬렉션을 하나 이상 즉시 로딩하는것은 권장하지 않음
- 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
'SPRING > JPA' 카테고리의 다른 글
[JPA]스프링 데이터 Common:커스텀 리포지토리 (0) | 2022.01.21 |
---|---|
[JPA]MappedSuperClass (0) | 2022.01.21 |
[JPA]객체지향 쿼리 심화 (0) | 2022.01.21 |
[JPA]NamedQuery (0) | 2022.01.21 |
[JPA] 엔티티 직접사용 (0) | 2022.01.21 |