프록시가 필요한 이유
Member와 Team이 n:1로 매핑되어 있을 때
Member를 조회할 때 팀정보가 조인되어 계속 호출(조인)되어 조회하면
성능이 떨어지지 않을까?
난 Member의 정보만 조회하고 싶은데..
하면 프록시를 활용해서 Member만 조회할 수 있다. 먼저 프록시가 뭔지 알아보자.
프록시란?
실제 클래스를 거의 유사하게 상속받아서 만들어진 객체로 실제 클래스와 겉모양이 같다. 사용하는 입장에선 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다. 프록시 객체는 실제 객체의 참조(target)을 보관한다.
em.find() vs em.getReference()
지금 것 우리는 DB의 실제 엔티티 객체를 조회할 때 em.find() 메소드를 사용했다.
프록시 엔티티 객체를 조회하기 위해선 em.getReference()를 사용하면 된다.
프록시 객체의 초기화
프록시 객체는 getReference 메소드만으로 초기화 되는 것이 아니다. 단지 호출한 상태일 뿐 안에는 텅텅빈 객체이다. 이 객체를 초기화 시켜주기 위해서는 .
-> getName()과 같은 id(key)가 아닌 다른 속성을 조회한다던가
-> Hibernate.initialize(entity) 등의 Hibernate 메소드를 활용하여 초기화 시켜줄 수 있다.
초기화 과정
1. 만약 사용자가 .getName()과 같은 메소드를 호출했을경우
2. proxy 객체가 영속성 컨텍스트에 초기화를 요청한다.
3. 영속성 컨텍스트가 DB에 실제 객체를 조회요청한다.
4. DB가 실제 Entity를 프록시 객체에 제공한다.
5. 프록시 객체는 유지된 상태에서 프록시의 taget에 실제객체의 값들이 다 입혀진 상태가 된다.
프록시의 특징
- 프록시 객체는 처음 사용할 때(단순이 호출하는 것만으론 x) 한 번만 초기화한다.
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다**. 초기화 되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능한 것이지 프록시 객체인 상태는 그대로 유지된다.
- 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 연산자는 안되니까, instance of 사용)
- 하지만 찾고자하는 프록시 객체의 원본이 영속성 컨텍스트에 이미 있을 때는 프록시 객체를 호출해도 실제 원본 엔티티가 반환됨.
예시)
Member m1 = em.find(Member.class, member1.getId()); // 원본 호출
Member reference = em.getReference(Member.class, member1.getId()); // 프록시 호출
이때 m1과 reference 모두 실제 원본객체라는 것이다. (원본이 이미 영속성 컨텍스테 있을 경우에만)
- 반대도 마찬가지이다. 프록시 객체를 먼저 조회하고 그 다음 원본객체를 조회하면, 둘다 동일한 프록시 객체로 조회된다.
- 준영속 상태일 때(detach, close 등으로) 프록시를 초기화하면 문제 발생
org.hibernate.LazyInitializationException이 발생한다면 기억할것
프록시 확인
- 프록시 인스턴스 초기화 여부 확인
=> emf.getPersistenceUnitUtil().isLoaded(refMember)
=> 엔티티 메니저 팩토리에서 PersistenceUnitUtil을 가져올 수 있고 메소드 isLoaded를 이용하여 인스턴스가 초기화 되었는지 확인할 수 있다. 리턴은 boolean형이다.
- 프록시 클래스 확인 방법
refMember.getClass();
refMember.getClass().getName();
- 프록시 강제 초기화
Hibernate.initialize(entity)
ex)
Hibernate.initialize(refMember);
지연 로딩 LAZY를 이용해서 프록시 조회
프록시에 대해 알았으니 어떻게 활용하는지 알아보자. 아까 처음한 질문으로 넘어와서, Member를 조회하는데 팀정보를 Join까지 하면서 굳이 조회하고 싶지 않을 때, Team정보를 프록시객체로 만들어 버리면 사용전까지 초기화하지 않으니까 조회 성능을 높이고 효율적으로 필요할 때만 사용할 수 있지 않을까? 라고 생각하면 된다. !
관계가 설정된 엔티티를 사용전까지 초기화하지 않는 프록시 객체로 설정하는 것을
지연로딩(Lazy Loading) 이라고 한다.
사용예시
@Entity
public class Member{
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team
// ... 생략
}
Member를 초기화 할때 Team의 정보를 워하지 않는 이상 team의 객체에는 텅 빈 프록시 객체로 세팅된다.
사용방법은 위와같이 간단하다. fetch = FetchType.LAZY 로 속성을 설정하면 된다.
@OneToMany @ManyToMany는 기본이 지연 로딩이므로 설정해줄 필요는 없지만
@ManyToOne, @OneToOne은 기본이 즉시 로딩(FetchType.EAGER)이므로 LAZY로 직접 설정해주는 것이 좋다.
프록시 객체로 날아온 Team 객체를 초기화하기 위해서는 팀객체를 사용하면 된다.
Team team = member.getTeam();
team.getName();
처럼 프록시 객체를 사용을 할 때 쿼리를 날려 실제 팀 객체를 가져온다.
=> 비즈니스 로직에서 맴버 조회시 항상 팀정보를 워하지 않는 경우는 쿼리를 아낄 수 있어서 조회 성능이 더 좋아 질 수 있다.
즉시 로딩 EAGER를 이용해서 직접 조회
fetch = FetchType.EAGER로 변경하면 됨
하지만 가급적 지연 로딩만 사용 (실무에서)
- 테이블 여러개가 섞여 있는경우 Join이 몇 개 있을지 못한다.
- 설정은 LAZY로 적용 시키고, 만약 조회 할때 팀정보까지 한번에 다 조회하고 싶은 경우에 JPQL에서 "join fetch 즉시 로딩할 콜럼"으로 활용해서 조회한다.
ex) "select m from Member m join fetch m.team" - 즉시 로딩을 적용하면 예상치 못한 SQL이 발생
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
이 게시물은 '자바 ORM 표준 JPA 프로그래밍' 강의를 수강하고 정리한 내용임을 밝힙니다.
출처: https://www.inflearn.com/course/ORM-JPA-Basic#
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 본 강의는 자바 백엔
www.inflearn.com