비즈니스 상황마다 다르지만
회원과 팀을 함께 출력하는printUserAndTeam
의 경우 Member, Team을 따로 가져오는 것 보다 한번에 같이 가져오는게 성능상 이득이다
회원만 출력하는printUser
의 경우 Team 데이터까지 같이 가져올 필요 없다. (낭비)
👉 JPA는 이런 상황을프록시
와지연로딩
으로 해결한다.
📌em.find()
vs em.getReference()
em.find()
- 데이터베이스를 통해서 실제 엔티티 객체 조회
em.getReference()
- 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
- 데이터베이스에 쿼리를 날리지 않고 조회하는 방법이다.
Member member = new Member();
member.setUserName("hello");
em.persist(member);
em.flush();
em.clear();
// select 쿼리가 나간다
Member findMember = em.find(Member.class, member.getId());
System.out.println("=== em.find(Member.class, member.getId()) ===");
System.out.println("findMember.getUserName() = " + findMember.getUserName());
System.out.println("findMember.getId() = " + findMember.getId());
System.out.println("=== commit ===");
tx.commit();
em.find(Member.class, member.getId());
호출 시점에 select 쿼리가 나간뒤findMember
변수에 데이터를 담는다.
Member member = new Member();
member.setUserName("hello");
em.persist(member);
em.flush();
em.clear();
// getReference 시점에 select 쿼리가 나가지 않는다
Member findMemberRef = em.getReference(Member.class, member.getId());
System.out.println("=== em.getReference(Member.class, member.getId()) ===");
System.out.println("=== commit ===");
tx.commit();
em.getReference(Member.class, member.getId());
호출 시점에 select 쿼리가 나가지 않는다.
Member member = new Member();
member.setUserName("hello");
em.persist(member);
em.flush();
em.clear();
// getReference 시점에 select 쿼리가 나가지 않는다
Member findMemberRef = em.getReference(Member.class, member.getId());
System.out.println("=== em.getReference(Member.class, member.getId()) ===");
System.out.println("BEFORE findMemberRef = " + findMemberRef.getClass());
System.out.println("findMemberRef.getId() = " + findMemberRef.getId());
// findMemberRef의 데이터를 참조하는 시점에 select 쿼리가 나간다
System.out.println("findMemberRef.getUserName() = " + findMemberRef.getUserName());
System.out.println("findMemberRef.getUserName() = " + findMemberRef.getUserName());
System.out.println("AFTER findMemberRef = " + findMemberRef.getClass());
System.out.println("=== commit ===");
tx.commit();
findMemberRef.getUserName()
호출시점에 select 쿼리가 나간다
findMemberRef.getId()
호출 시점에는 이미findMemberRef
변수에id
값이 담겨져 있기 때문에 쿼리가 나가지 않는다.
findMemberRef
는 id 값만 가지고 있는 텅 빈 껍데기 객체이다.findMemberRef
객체의userName
속성 데이터가 없기 때문에 데이터베이스에서 가져오기 위해 select 쿼리를 날린다.
- 두번째
findMemberRef.getUserName()
호출 시점에는 쿼리가 나가지 않고findMemberRef
객체의 값을 참조한다.findMemberRef.getClass()
를 출력하면Member$HibernateProxy$LehDcP3W
이다.
- 하이버네이트가 만든 가짜 클래스 (프록시 클래스) 라는 의미이다
- 실제 클래스를 상속 받아서 만들어진다 (하이버네이트가 내부적으로 프록시 라이브러리를 이용하여 만든다)
- 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨 (이론상)
- 프록시 객체는 실제 객체의
target
이라는 참조를 보관한다.- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출한다
- 즉, Proxy 객체의
getName()
이 호출되면target
이라는 실제 객체인Entity
의getName()
을 대신 호출한다.- 데이터베이스에 조회하지 않는 이상 처음에는
target
이 없다
처음에
getName()
을 호출한다
👉MemberProxy의 getName()
이 호출되면target (Member)의 getName()
이 대신 호출된다
👉 하지만 target (Member)는 null이므로 JPA가 영속성 컨택스트에 target 초기화를 요청한다.
👉 그제서야 영속성 컨택스트는 데이터베이스에 조회하고 실제 Entity 객체를 생성하여 반환한다
👉 그 다음 Member Proxy의Member target
에 실제 Entity 객체를 연결한다
👉target.getName()
이 호출되면 실제 Entity 객체의getName()
이 호출된다.
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.
초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능하게 되는 것이다.
- 프록시 객체는 유지가 되고 프록시 객체 내부의 target값이 채워지는 것이다.
- 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야한다.
(==
비교 대신instance of
사용해야한다.)- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면
em.getReference()
를 호출해도 실제 엔티티 반환한다.
- 이 반대 상황도 마찬가지다. (프록시 객체로 조회하면
em.find()
를 호출해도 프록시 객체를 반환한다.)- 왜냐하면, JPA는 동일 트랜잭션안에서 동일 영속성 컨택스트 속에 조회되는 Entity의 동일성을 보장해줘야하기 떄문이다.
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.
(하이버네이트는org.hibernate.LazyInitializationException
예외를 터트림)
Member member1 = new Member();
member1.setUserName("hello1");
em.persist(member1);
Member member2 = new Member();
member2.setUserName("hello2");
em.persist(member2);
em.flush();
em.clear();
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.find(Member.class, member2.getId());
logic(m1, m2);
---
private static void logic(Member m1, Member m2) {
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass()));
}
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());
logic(m1, m2);
---
private static void logic(Member m1, Member m2) {
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass()));
}
find
와getReference
으로 얻은 객체 타입은 다르다.
- 실제 비즈니스 로직에서 타입 비교시
객체가 프록시 객체인지 실제 객체인지 확인하기 어렵기 때문에==
으로 타입 비교하면 안된다.
👉 타입비교는m1 instance of Member
로 해야한다.
em.getReference()
를 호출해도 실제 엔티티 반환한다.Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = " + m1.getClass());
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass());
System.out.println("m1 == reference : " + (m1 == reference));
// 프록시 객체 refMember
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());
// 실제 객체 findMember ? => 프록시 객체 findMember
Member findMember = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + findMember.getClass());
// 프록시 refMember == 실제 findMember : true
System.out.println("refMember == reference : " + (refMember == findMember));
Member member1 = new Member();
member1.setUserName("hello1");
em.persist(member1);
em.flush();
em.clear();
// 프록시 객체 refMember
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());
// refMember는 영속성 컨택스트에서 더이상 관리하지 않는다.
// em.clear() 도 마찬가지
em.detach(refMember);
// 에러 발생
// could not initialize proxy [hellojpa.Member#1] - no Session
System.out.println("refMember = " + refMember.getUserName());
org.hibernate.LazyInitializationException
예외
프록시 객체가 영속성 컨택스트의 도움을 받지 못하면
초기화 요청
을 할 수 없게 된다.
👉 실제 객체를 가져오지 못하기 때문에 프록시 객체를 초기화할 수 없다.
- 프록시 인스턴스의 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity)- 프록시 클래스 확인 방법
entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)- 프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity);- 참고: JPA 표준은 강제 초기화 없음
강제 호출: member.getName()
PersistenceUnitUtil.isLoaded(Object entity)
Member refMember = em.getReference(Member.class, member1.getId());
// 프록시 객체 refMember
System.out.println("refMember = " + refMember.getClass());
// 아직 프록시 초기화 하지 않았으므로 false
System.out.println("1 isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
String userName = refMember.getUserName();
// 프록시가 초기화 되었으므로 true
System.out.println("2 isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
단순히 Member 정보만 사용하는 비즈니스 로직에서 연관관계 매핑되었다는 이유로 Team 정보를 조인해서 가져오면 손해다.
그래서 JPA는지연 로딩
이라는 옵션을 제공한다.
@Entity
public class Member extends BaseEntity {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String userName;
// LAZY : 지연로딩 전략을 따른다
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Team team;
…
}
@ManyToOne(fetch = FetchType.LAZY)
- 지연 로딩 전략을 따른다.
Team team
객체는 프록시 객체로 조회한다.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member1 = new Member();
member1.setUserName("hello1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
System.out.println("=== BEFORE em.find(Member.class, member1.getId()) ===");
Member m = em.find(Member.class, member1.getId());
System.out.println("=== AFTER em.find(Member.class, member1.getId()) ===");
// Team 객체는 프록시 객체이다.
System.out.println("m.getTeam().getClass() = " + m.getTeam().getClass());
// 프록시 객체인 Team을 조회하는 시점에 select 쿼리가 나간다
System.out.println("=== BEFORE m.getTeam().getName() ===");
System.out.println("m.getTeam().getName() = " + m.getTeam().getName());
System.out.println("=== AFTER m.getTeam().getName() ===");
System.out.println("=== commit ===");
tx.commit();
member1
로딩시team1
객체는 DB에서 값을 가져오지 않고 프록시 객체로 세팅되어있다.
Team 객체는 지연 로딩(
@ManyToOne(fetch = FetchType.LAZY)
)으로 설정 되어있다.
member 조회시 Team 객체는 프록시 객체로 초기화 된다.
프록시 객체인 team의 데이터를 참조하는 시점에 프록시 객체 team은 실제 엔티티 객체를 가리키도록 초기화된다.
비즈니스 로직에서 Member와 Team을 같이 사용하는 경우엔 함께 조회하는게 좋다
@Entity
public class Member extends BaseEntity {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String userName;
// EAGER: 즉시 로딩 전략을 따른다
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn
private Team team;
…
}
Member 객체를 조회할 때, Team 객체 정보까지 한번에 조회한다. (조인)
- 즉시 로딩이므로 Team 객체는 프록시가 아닌 실제 엔티티 객체이다.
member1 로딩시 team1 까지 조인하여 조회한다.
- JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회한다.
- 즉시 로딩하는 2가지 방법
- member를 조회할 때, 한번에 team 데이터까지 조인하여 가져오는 방법 👉 대부분의 하이버네이트 구현체는 이 방식을 사용한다.
- member를 조회한 뒤, team을 나중에 조회 (
em.find()
호출 시점에 쿼리가 2번 나간다)
- 가급적 지연 로딩만 사용해야 한다.(특히 실무에서)
- 실무에서는 즉시 로딩을 사용하면 안된다. NEVER EVER
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
- 하나의 테이블에 여러 개의 연관관계가 물려있을 경우, 즉시 로딩을 따르면 여러 테이블 조인된다
👉 성능문제 발생- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
- @ManyToOne, @OneToOne은 기본이 즉시 로딩
EAGER
👉LAZY
로 설정해야 한다.- @OneToMany, @ManyToMany는 기본이 지연 로딩
LAZY
System.out.println("=== em.createQuery ===");
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
em.find()
를 사용하면 JPA가 조인으로 최적화하여 가져올 수 있지만,
JPQL은 SQL으로 번역되어 나가기 때문에 조인하여 가져오지 않는다.
select m from Member m
를 sql로 번역하면 👉 그냥 Member에 대한 select쿼리가 나가게 된다.- Member 정보를 가져왔더니, Team 참조 필드가 즉시 로딩 (
EAGER
) 으로 설정 되어있기 때문에 👉 Team에 대한 select 쿼리가 나간다.- 즉, 값을 채워야할 데이터만큼 추가 쿼리가 나간다.
👉 처음 나가는 쿼리1
+ 추가 쿼리N
:N + 1
문제를 일으킨다.
- 처음에 연관관계 매핑시 지연 로딩 (
LAZY
)으로 설정한다.- 3가지 방법을 사용한다.
fetch join
- 런타임시 동적으로 원하는 연관관계를 선택해서 조인하여 가져온다.
- Member만 가져올 때는 기본적으로 Member 만 select한다 (Team은 지연로딩으로 설정되어있기 떄문).
Member와 Team을 가져올 때는 fetch join을 사용하여 함께 가져온다.
-em.createQuery("select m from Member m join fetch m.team", Member.class)
이론적인 활용이기 때문에 실무에서 예시처럼 활용하면 큰일난다. (실무에선 무조건 지연 로딩만)
- Member와 Team은 자주 함께 사용 👉 즉시 로딩
- Member와 Order는 가끔 사용 👉 지연 로딩
- Order와 Product는 자주 함께 사용 👉 즉시 로딩
member1 조회시
- team 은 조인하고
- orders는 프록시 객체로 초기화한다.
- 모든 연관관계에 지연 로딩을 사용해라!
- 실무에서 즉시 로딩을 사용하지 마라!
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라!
- 즉시 로딩은 상상하지 못한 쿼리가 나간다.