[JPA] 프록시, 즉시 로딩, 지연 로딩
프록시
- 연관된 객체를 자유롭게 탐색하기 위해 사용하는 기술입니다.
- 엔티티를 조회할 때 연관된 엔티티들이 항상 같이 조회되는 것은 아닙니다.
- 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 지연 로딩이라고 하는데 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 그것을 프록시 객체라고 합니다.
- 프록시를 사용하면 연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라 실제 사용하는 시점에서 데이터 베이스를 조회할 수 있습니다.
※ 공통 예제 코드 ※
@Entity
@Getter @Setter
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name ="GROUP_ID")
private Group group;
}
@Entity
@Getter @Setter
@Table(name = "GROUPS")
public class Group {
@Id
@GeneratedValue
private Long id;
private String groupName;
}
- Group 엔티티에 @Table해주는 이유는 group라는 예약어가 있기 때문입니다.
프록시 기초
- JPA에서 식별자로 엔티티를 조회할 때 EntityManager.find(반환 객체, 식별자(PK))를 사용하는데 영속성 컨텍스트에 엔티티가 없으면 데이터베이스에서 조회합니다.
- find() 메서드를 사용하면 조회한 엔티티를 실제 사용하든 사용하지 않든 데이터베이스에서 조회를 하는데 조회를 미루고 싶으면 EntityManager.getReference(반환 객체, 식별자(PK))를 사용합니다.
- getReference() 메서드를 이용하면 JPA는 데이터베이스를 조회하지 않고 실제 엔티티를 생성하지 않는 대신 데이터베이스 접근을 위임한 프록시 객체를 반환합니다.
프록시 초기화
- 프록시 초기화는 프록시 객체를 실제 데이터 값을 사용할 때 조회해서 실제 엔티티 객체를 생성합니다.
※ 프록시 기초와 프록시 초기화 ※
import javax.persistence.*;
public class Jpa {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
try {
transaction.begin();
//영속성 컨텍스트에 저장
Member member = new Member();
member.setName("hoestory");
em.persist(member);
em.flush(); // 데이터베이스에 저장
em.clear(); // 영속성 컨텍스트를 비우기
// 조회한 엔티티를 실제 사용하든 않든 데이터베이스에서 조회하게됨
Member findMember = em.find(Member.class,member.getId();
//결과
//findMember.getClass() = class Member
System.out.println("findMember.getClass() = " + findMember.getClass());
// 프록시 객체 : 사용하지 않는 객체는 데이터베이스에서 조회를 미룸.
Member proxyMember = em.getReference(Member.class, member.getId();
// 결과
//proxyMember.getClass() = class Member$HibernateProxy$cbNUX9Ay
System.out.println("proxyMember.getClass() = " + proxyMember.getClass());
// 초기화 : 실제값을 조회
proxyMember.getName();
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
transaction.rollback();
} finally {
em.close();
}
emf.close();
}
}
- find() : 사용하고 싶든 사용하고 싶지 않든 데이터베이스에서 조회를 합니다.
- getReference() : 데이터베이스의 조회를 미루고 실제 엔티티의 객체를 생성하고 싶으면 프록시를 초기화해주면 됩니다
- proxyMember.getName() : 프록시 초기화
프록시 특징
- 처음 사용할 때 한 번만 초기화됩니다.
- 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것이 아니고 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있습니다.
- 영속성 컨텍스트에 찾는 엔티티가 있으면 getReference()를 호출해도 프록시 객체가 아닌 실제 엔티티를 반환합니다.
- 프록시 초기화는 영속성 컨텍스트의 도움을 받아야 되기 때문에 준영속 상태에서 프록시를 초기화할 경우 org.hibernate.LazyInitalizationException 예외가 발생합니다.
프록시 초기화 확인 및 강제 초기화
- 프록시 초기화를 확인하는 방법은 JPA에서 제공하는 PersistenceUnitUtil.isLoaded(엔티티)를 사용하시면 됩니다.
- 초기화가 안된 프록시 인스턴스는 false가 나오고 초기화가 되거나 프록시 객체가 아닌 엔티티는 true가 나옵니다.
- 프록시를 강제로 초기화하는 방법은 하이버네이트에서 제공하는 org.hibernate.Hibernate.initialize(엔티티. getXxx)를 사용하시면 됩니다.
즉시 로딩과 지연 로딩
즉시 로딩 : 어떤 엔티티를 호출할 때 그 엔티티와 연관된 엔티티들도 함께 조회합니다.
설정 방법 : @ManyToOne(fetch = FetchType.EAGER)
@Entity
@Getter @Setter
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name ="GROUP_ID")
private Group group;
}
import javax.persistence.*;
public class Jpa {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("playbook");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
try {
transaction.begin();
Group group = new Group();
group.setName("group1");
em.persist(group);
Member member = new Member();
member.setUsername("member1");
member.setGroup(group);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
transaction.rollback();
} finally {
em.close();
}
emf.close();
}
}
즉시 로딩했을 때 쿼리문
select
member.id as id,
member.GROUP_ID as GROUP_ID,
member.username as username,
group1.GROUP_ID as GROUP_ID,
group1.name as name
from
Member member
left outer join
GROUPS group
on member.GROUP_ID = group.GROUP_ID
where
member.id=?
회원을 조회하였는데 그룹도 같이 조인되어서 나왔습니다. 그 이유는 즉시 로딩을 최적화하기 위해 조인 쿼리를 사용한 것이기 때문입니다.
조인분을 자세히 보시면 외부 조인을 사용하였습니다. 왜냐하면 Member 테이블에서 외래키인 Group_ID가 NULL 값을 허용했기 때문입니다.
그래서 이럴 경우 내부 조인을 사용할 경우 그룹에 속하지 않은 회원과 그룹 중 아무것도 조회할 수 없기 때문에 외부 조인이 사용되었습니다.
내부 조인을 사용하고 싶으면 외래키 값을 NULL값을 허용해주면 안 됩니다.
연관관계 주인에 있는 @JoinColumn 속성 중 nullable이 있습니다. nullable 속성을 false로 해주면 NULL 값을 허용을 안 해서 내부 조인을 사용할 수 있습니다. 아니면 @ManyToOne 속성 중 optional을 false로 해주면 됩니다.
지연 로딩 : 연관된 엔티티를 프록시로 조회를 하고 프록시 객체를 초기화하면서 데이터베이스를 조회합니다.
설정 방법 : @ManyToOne(fetch = FetchType.LAZY)
@Entity
@Getter @Setter
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name ="GROUP_ID")
private Group group;
}
// Group 엔티티 생략
import javax.persistence.*;
public class Jpa {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("playbook");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
try {
transaction.begin();
Group group = new Group();
group.setName("group1");
em.persist(group);
Member member = new Member();
member.setUsername("member1");
member.setGroup(group);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
Group proxyGroup = findMember.getGroup();
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
transaction.rollback();
} finally {
em.close();
}
emf.close();
}
}
지연 로딩했을 때 쿼리문
select
member.MEMBER_ID as MEMBER_ID,
member.GROUPS_ID as GROUPS_ID,
member.username as username
from
Member member
where
member.MEMBER_ID=?
즉시 로딩 쿼리 문과 비교해보면 지연 로딩 쿼리문은 조회하려는 엔티티에 관한 쿼리문만 나왔습니다.
즉시 로딩은 Member엔티티와 Group엔티티를 한 번에 조회하였는데 지연 로딩은 Group 엔티티가 프록시 객체여서 조회가 미뤄졌습니다. Group 엔티티도 조회를 하고 싶으면 프록시를 초기화를 해주면 됩니다.
Member findMember = em.find(Member.class, member.getId());
Group proxyGroup = findMember.getGroup();
// 추가
// 프록시 객체 초기화
proxyGroup.getName();
프록시 객체 초기화 후 쿼리문
select
member.MEMBER_ID as MEMBER_ID,
member.GROUPS_ID as GROUPS_ID,
member.username as username
from
Member member
where
member.MEMBER_ID=?
select
group.GROUPS_ID as GROUPS_ID,
group0_.name as name
from
GROUPS group
where
group.GROUPS_ID=?
※ 참고
@ManyToOne, @OneToOne의 fetch 기본값은 EAGER(즉시 로딩)
@OneToMany, @ManyToMany의 fetch 기본값은 LAZY(지연 로딩)
모든 연관관계를 지연 로딩으로 사용하는 것을 추천드립니다. 왜냐하면 컬렉션을 로딩할 때 비용이 많이 들고 잘못하면 너무 많은 데이터를 로딩할 수 있는 문제가 발생할 수 있기 때문입니다. 그래서 상황을 보고 즉시 로딩을 사용해야 할 경우만 즉시 로딩을 사용하시기를 바랍니다.
'JPA' 카테고리의 다른 글
[JPA] N+1 원인 및 해결방법 (0) | 2023.01.18 |
---|---|
[Querydsl] 스프링 부트에서 Querydsl 설정 (0) | 2022.06.14 |
[JPA] 단방향, 양방향, 연관관계 주인 (0) | 2022.01.26 |
[JPA] 엔티티 매핑 (0) | 2022.01.25 |
[JPA]영속성 컨텍스트란? (0) | 2022.01.19 |
댓글
이 글 공유하기
다른 글
-
[JPA] N+1 원인 및 해결방법
[JPA] N+1 원인 및 해결방법
2023.01.18 -
[Querydsl] 스프링 부트에서 Querydsl 설정
[Querydsl] 스프링 부트에서 Querydsl 설정
2022.06.14 -
[JPA] 단방향, 양방향, 연관관계 주인
[JPA] 단방향, 양방향, 연관관계 주인
2022.01.26 -
[JPA] 엔티티 매핑
[JPA] 엔티티 매핑
2022.01.25