즉시 로딩과 지연 로딩을 이해하기 위해 프록시 개념을 먼저 이해해야한다.
JPA 기본 | 프록시 (즉시 로딩, 지연 로딩을 이해하기 위한)
1. 프록시란?JPA에서 연관된 엔티티를 지연 로딩하기 위해 사용되는 객체다.데이터베이스 접근을 지연시켜 성능을 최적화한다.필요한 시점까지 데이터 로딩을 미뤄서 메모리 사용량을 줄이고 불
ururuwave.tistory.com
그래서 이전에 포스팅한 프록시를 첨부한다.
1. 지연 로딩, 즉시 로딩 개념
- 지연 로딩 : 연관된 엔티티를 실제로 접근할 때까지 데이터베이스 조회를 지연시키는 로딩 전략
- 즉시 로딩 : 연관된 엔티티를 함께 조회하여 즉시 데이터베이스에서 로드하는 로딩 전략
2. 지연 로딩(fetch = FetchType.LAZY)
Member 엔티티와 Team 엔티티는 양방향 연관관계를 맺고 있다.
지연 로딩 설정을 해주지 않으면, 단순히 Member 정보만 사용할 때도 Team을 조인하는 쿼리가 실행된다.
조인 없이 Member 테이블만 조회하고 싶을 때, 지연 로딩을 사용할 수 있다.
그리고 Team과 관련된 정보를 조회하면 그때 Team 프록시 객체가 초기화 된다. (Team 조회쿼리 실행 시점)
✅코드 예시
@Entity
@Getter
@Setter
public class Member extends BaseEntity{
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
@Getter
@Setter
public class Team extends BaseEntity{
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>(); //ArrayList<>()로 초기화한다. add할때 NullPointException이 뜨지 않게!
}
(fetch = FetchType.LAZY) 를 붙여준다.
이렇게 하면 Member 엔티티에 Team 필드가 지연로딩으로 설정된다.
✅main 메서드
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
// Team 엔티티 생성 및 저장
Team team = new Team();
team.setName("teamA");
em.persist(team);
// Member 엔티티 생성 및 저장
Member member = new Member();
member.setUsername("ururuwave");
member.setTeam(team); //연관관계 설정
em.persist(member);
// DB 반영 및 영속성 컨텍스트 초기화
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass()); //실제 엔티티
System.out.println("findMember = " + findMember.getTeam().getClass()); //프록시 객체
System.out.println("=====team 조회 START=====");
System.out.println("findMember = " + findMember.getTeam().getName());
System.out.println("=====team 조회 END=====");
tx.commit();
}catch (Exception e){
tx.rollback();
e.printStackTrace(); //예외 출력
}finally {
em.close();
}
}
✔️ Member 엔티티에 지연로딩 설정을 해주고나면 Team 프록시 객체를 생성할 수 있다.
- em.find()로 findMember 인스턴스를 생성하면 Member만 조회하는 쿼리가 실행된다.
findMember는 실제 Member 엔티티고 지연 로딩 설정된 findMember의 team은 Team 프록시 객체다.
✔️Team의 정보를 조회하려고 하는 시점에서 Team 프록시 객체가 초기화 된다.
✅콘솔 확인
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
m1_0.TEAM_ID,
m1_0.USERNAME
from
Member m1_0
where
m1_0.MEMBER_ID=?
findMember = class hellojpa.Member
findMember.getTeam() = class hellojpa.Team$HibernateProxy$rWCuhsMX
=====team 조회 START=====
select
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name
from
Team t1_0
where
t1_0.TEAM_ID=?
findMember = teamA
=====team 조회 END=====
Member 테이블만 조회하는 쿼리가 실행됐다.
findMember는 실제 Member 엔티티다.
findMember.getTeam()은 Team 프록시 객체다.
Team을 사용하려는 시점에서 Team 테이블을 조회한다.
3. 즉시 로딩(fetch = FetchType.EAGER)
비즈니스 로직상 Member와 Team을 함께 사용하는 빈도가 높다면 즉시 로딩을 하는 것이 효율적이다.
✅코드 예시
(fetch = FetchType.EAGER)
Member 엔티티의 (fetch = FetchType.LAZY) 부분만 EAGER로 바꿔주면 된다.
✅콘솔 확인
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name,
m1_0.USERNAME
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.MEMBER_ID=?
findMember = class hellojpa.Member
findMember.getTeam() = class hellojpa.Team
=====team 조회 START=====
teamName = teamA
=====team 조회 END=====
처음에 MEMBER, TEAM테이블을 조인하여 한꺼번에 데이터베이스를 조회한다.
지연 로딩했던 때와 달리 Team은 프록시 객체가 아닌 실제 Team 엔티티 객체다.
team 조회를 했을 때, TEAM 테이블을 조회할 필요없이 바로 teamName을 가져올 수 있다.
4. 프록시와 즉시로딩 주의
✔️실무에서는 모든 연관관계에 지연 로딩만 사용한다.
왜? 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
연관 매핑되어있는게 2개가 아닌 5개, 10개라면 무조건 조인이 발생하니 성능 저하 이슈가 생긴다.
✔️즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
N+1 문제란, 주 엔티티를 1회 조회하고 연관된 엔티티의 갯수(N)만큼 추가 쿼리가 실행되는 문제다.
✅코드 예시
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
// Team 엔티티 생성 및 저장
Team team = new Team();
team.setName("teamA");
em.persist(team);
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);
// Member 엔티티 생성 및 저장
Member member = new Member();
member.setUsername("ururuwave");
member.setTeam(team); //연관관계 설정
em.persist(member);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(teamB); //연관관계 설정
em.persist(member2);
// DB 반영 및 영속성 컨텍스트 초기화
em.flush();
em.clear();
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
//SQL : select * from member
//SQL : select * from team where TEAM_ID = ***
tx.commit();
}catch (Exception e){
tx.rollback();
e.printStackTrace(); //예외 출력
}finally {
em.close();
}
}
Member와 Team 인스턴스를 2개씩 만들었다.
JPQL을 사용하여 members 리스트를 가져왔다.
원래 의도는 select * from member 쿼리를 한번만 실행하는 것이다.
그런데 즉시 로딩으로 설정되어있다면 N개의 member 엔티티에 갯수만큼 team을 조회하는 쿼리가 N번 실행된다.
N+1문제는 데이터베이스에 너무 많은 쿼리를 발생시켜 성능을 저하 시킨다.
(*N+1 문제 해결법은 다른 포스팅에서 다룰 예정이지만, 즉시로딩을 사용하지 않고 한꺼번에 조회하는 쿼리를 실행하고 싶을 때 페치 조인을 사용하면 된다.)
✔️@ManyToOne, @OneToOne은 기본이 즉시 로딩이다. 그러니 LAZY로 설정해야한다.
✔️ @OneToMany, @ManyToMany는 기본이 지연 로딩이다.
'인프런 김영한 강의 정리 > 자바 ORM 표준 JPA 프로그래밍 기본편' 카테고리의 다른 글
JPA 기본 | 데이터 타입(1) - 기본 값과 임베디드 값 타입 (0) | 2024.07.14 |
---|---|
JPA 기본 | 영속성 전이(CASCADE)와 고아 객체 (0) | 2024.07.13 |
JPA 기본 | 프록시 (즉시 로딩, 지연 로딩을 이해하기 위한) (0) | 2024.07.13 |
JPA 기본 | @MappedSuperclass (0) | 2024.07.10 |
JPA 기본 | 상속관계 매핑 (0) | 2024.07.10 |