[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들어가기 전 이번 글에서는 N+1이 무엇이고 발생 원인과 N+1을 방지하기 위한 임시방편, 해결방법에 대해 알아보겠습니다. 그리고 예시로 Person, House 엔티티가 있습니다. Person(N) : House(1) 관계로 이루어져 있습니다. 즉 Person이 해당 예시에서 연관관계 주인입니다. @Entity @Getter @Setter public class House { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String address; } @Entity @Getter @Setter public class Person { @Id @GeneratedValue… -
[Querydsl] 스프링 부트에서 Querydsl 설정
[Querydsl] 스프링 부트에서 Querydsl 설정
2022.06.14들어가기 전 스프링 부트 2.6 이상 버전에서는 2.6 이하 버전에서 설정한 부분에 조금 추가를 하셔야 정상적으로 동작합니다. 필자는 스프링 부트 버전 2.7 Gradle 버전 7.4.1을 기준으로 설정을 하였습니다. 스프링 부트 2.6 이하 버전 먼저 스프링 부트 2.6이하 버전에서 querydsl 설정을 알아보겠습니다. build.gradle plugins { // querydsl 추가 id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" } dependencies { implementation 'com.querydsl:querydsl-jpa' } //querydsl 추가 def querydslDir = "$buildDir/generated/queryds… -
[JPA] 단방향, 양방향, 연관관계 주인
[JPA] 단방향, 양방향, 연관관계 주인
2022.01.26단방향, 양방향 테이블은 외래키 하나로 양방향 쿼리가 가능해서 방향이라는 개념이 없습니다. 객체는 참조용 필드를 가지고 있는 객체만 연관된 객체를 조회할 수 있습니다. 단방향 : 객체 관계에서 한쪽만 참조하는 것을 말합니다. 양방향 : 객체 관계에서 양쪽이 서로 참조하는 것을 말합니다. 연관관계 주인 데이터베이스는 외래키 하나로 두 개의 테이블을 연관관계를 맺습니다. 데이터베이스는 연관관계를 관리하는 지점은 외래키 하나인 반면에 엔티티를 양방향으로 매핑하면 두 개의 객체는 서로 참조를 해서 객체의 연관관계를 관리하는 지점은 2개가 됩니다. 연관관계 주인은 두 객체의 연관관계 중 하나를 정해서 데이터베이스의 외래키를 관리하는 것입니다. 연관관계 주인은 @JoinColumn(name = "참조하는 테이블 기… -
[JPA] 엔티티 매핑
[JPA] 엔티티 매핑
2022.01.25JPA에서 사용하는 엔티티 매핑에 대해 알아보겠습니다. 객체와 테이블 매핑 기본키 매핑 필드와 컬럼 매핑 객체와 테이블 매핑 @Entity @Table @Entity 테이블과 매핑할 클래스에 @Entity를 붙입니다. @Entity 속성 속성 기능 기본값 name JPA에서 사용할 엔티티 이름을 지정하고 같은 엔티티 클래스가 있다면 이름을 지정해서 충돌하지 않도록 해야합니다. 설정하지 않으면 클래스 이름 그대로 사용합니다. @Entity public class Jpa { } @Entity(name = "Member") public class Member{ } @Entity 적용 시 주의 사항 접근 지정자가 public 또는 protected인 기본 생성자가 필수입니다. final 클래스, enum, in…
댓글을 사용할 수 없습니다.