티스토리 뷰
즉시 로딩과 지연 로딩이란?
아래 예제를 통해서 즉시 로딩과 지연 로딩에 대해서 알아보았다.
엔티티, 테이블 관계도

즉시 로딩이란?
즉시 로딩은 단어 의미대로 해석할 수 있으며, JPA 상에서 데이터를 조회할 때 연관된 모든 객체의 데이터까지 한 번에 불러오는 것이다.
즉시 로딩 예제
아래 Member 클래스의 Team 참조 변수에 FetchType.EAGER를 걸었는데 이는 Team도 같이 한번에 불러오는 즉시 로딩이다.
Member.java
@Entity
public class Member {
@Id
@GeneratedValue
private Long memberId;
private String userName;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "teamId")
private Team team;
// getter, setter 생략
}
Team.java
@Entity
public class Team {
@Id
@GeneratedValue
private Long teamId;
private String teamName;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
// getter, setter 생략
}
Main.java
// 엔티티 매니저 em 생략
Team team1 = new Team();
team1.setTeamName("team1");
em.persist(team1);
Member member1 = new Member();
member1.setTeam(team1);
member1.setUserName("member1");
em.persist(member1);
Member member2 = new Member();
member2.setTeam(team1);
member2.setUserName("member2");
em.persist(member2);
Team team2 = new Team();
team2.setTeamName("team2");
em.persist(team2);
Member member3 = new Member();
member3.setTeam(team2);
member3.setUserName("member3");
em.persist(member3);
em.flush();
em.clear();
Member findMember1 = em.find(Member.class, member1.getMemberId());
System.out.println("member1.userName: " + findMember1.getUserName());
System.out.println("member1.team.memberSize: " + findMember1.getTeam().getTeamName());
실행 결과
Hibernate:
select
m1_0.memberId,
t1_0.teamId,
t1_0.teamName,
m1_0.userName
from
Member m1_0
left join
Team t1_0
on t1_0.teamId=m1_0.teamId
where
m1_0.memberId=?
member1.userName: member1
member1.team.memberSize: team1
즉시 로딩인 FetchType.EAGER를 사용했기 때문에 Member 조회시 Team도 함께 조회되었고 쿼리도 한 번만 실행된 것을 확인할 수 있다. 그런데 EAGER를 JPQL문에서 사용하면 문제점이 발생하게 된다. 아래 예제를 통해 확인해 보았다.
JPQL에서의 즉시 로딩의 문제점
Main.java
// 엔티티 매니저 em 생략
Team team1 = new Team();
team1.setTeamName("team1");
em.persist(team1);
Member member1 = new Member();
member1.setTeam(team1);
member1.setUserName("member1");
em.persist(member1);
Member member2 = new Member();
member2.setTeam(team1);
member2.setUserName("member2");
em.persist(member2);
Team team2 = new Team();
team2.setTeamName("team2");
em.persist(team2);
Member member3 = new Member();
member3.setTeam(team2);
member3.setUserName("member3");
em.persist(member3);
em.flush();
em.clear();
//Member findMember1 = em.find(Member.class, member1.getMemberId());
//System.out.println("member1.userName: " + findMember1.getUserName());
//System.out.println("member1.team.memberSize: " + findMember1.getTeam().getTeamName());
// JPQL에서의 문제점
List<Member> selectMemberList = em.createQuery("select m from Member m", Member.class).getResultList();
System.out.println("==========");
for(Member selectMember : selectMemberList) {
System.out.println("selectMember.userName: " + selectMember.getUserName());
}
실행결과
Hibernate:
/* select
m
from
Member m */ select
m1_0.memberId,
m1_0.teamId,
m1_0.userName
from
Member m1_0
Hibernate:
select
t1_0.teamId,
t1_0.teamName
from
Team t1_0
where
t1_0.teamId=?
Hibernate:
select
t1_0.teamId,
t1_0.teamName
from
Team t1_0
where
t1_0.teamId=?
==========
selectMember.userName: member1
selectMember.userName: member2
selectMember.userName: member3
실행 결과는 정상적으로 나왔으나 Team을 조회하는 쿼리가 두 번이나 나갔다. 이는 JPA에서 JPQL문인 createQuery("select m from Member m", Member.class).getResultList()의 실행 결과로 Member 객체 3개를 리스트에 저장하는데 문제가 각각의 Member 객체에 있는 Team은 EAGER(즉시로딩)가 적용되기 때문에 JPA는 Member1,2의 Team1, Member3의 Team2를 추가로 조회하게 된다. 그렇다면 만약 100개의 멤버당 100개의 팀이 따로 되어 있다면 Team은 100번 추가로 조회하게 된다. 이는 N+1 문제를 일으킨다. 결국 실무에서는 아래와 같이 지연 로딩으로 사용할 것을 권장한다.
지연 로딩이란?
지연 로딩도 즉시 로딩처럼 단어 의미대로 해석할 수 있으며, JPA 상에서 데이터를 조회할 때 연관된 모든 객체의 데이터까지 한 번에 불러오는 것이 아닌 연관된 객체를 조회할 때 그때 불러오는 것이다.

위의 캡처에서 처음 멤버를 조회할 때 Team은 실제 객체가 아닌 프록시 객체를 반환하다. 그리고 Team을 조회할 때 DB를 조회 후 실제 Team 객체를 프록시 객체(Team)에 실제 Team 객체를 참조(target)하는 변수에 초기화한다.
지연 로딩 예제
아래 Member 클래스의 Team 참조 변수에 FetchType.LAZY를 걸었는데 이는 Team을 조회하는 시점에 Team을 초기화시켜준다.
Member.java
@Entity
public class Member {
@Id
@GeneratedValue
private Long memberId;
private String userName;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "teamId")
private Team team;
// getter, setter 생략
}
Main.java
// 앞 과정은 동일하므로 생략
Member findMember1 = em.find(Member.class, member1.getMemberId());
System.out.println("member1.userName: " + findMember1.getUserName());
System.out.println("================");
System.out.println("member1.team.getClass(): " + findMember1.getTeam().getClass());
System.out.println("member1.team.memberSize: " + findMember1.getTeam().getTeamName());
실행결과
Hibernate:
select
m1_0.memberId,
m1_0.teamId,
m1_0.userName
from
Member m1_0
where
m1_0.memberId=?
member1.userName: member1
================
member1.team.getClass(): class jpabook.jpashop.domain.Team$HibernateProxy$mZQz4RUr
Hibernate:
select
t1_0.teamId,
t1_0.teamName
from
Team t1_0
where
t1_0.teamId=?
member1.team.memberSize: team1
위의 실행 결과를 보면 Member를 조회 시에 Member 테이블을 조회하는 쿼리만 한번 실행되고 그다음 Team을 조회할 때 쿼리가 실행하는 것을 볼 수 있다. 이는 Member 객체 내 Team에 LAZY인 지연 로딩을 적용했기 때문이고, Team의 클래스 정보를 보면 프록시 객체인 것을 볼 수 있다. 위에서 설명했듯이 이 Team 프록시 객체에서 name 등을 조회할 때 쿼리가 실행되며 실제 Team을 프록시 객체 내에 참조변수로 지정 후 초기화 한 것이다. (※Team을 여러 번 조회해도 이미 초기화를 하였기 때문에 쿼리는 추가로 실행되지 않는다.)
언제 지연 로딩을 사용해야 할까?
상황마다 다르겠지만 기본으로 지연 로딩을 사용하는 것이 좋을 것 같다. 위의 즉시 로딩을 사용하게 된다면 N+1 같은 문제가 발생하게 될 테고 지연 로딩을 사용하게 되면 필요에 따라 조회하기 때문에 문제점을 줄일 수 있다. 참고로 지연 로딩은 @OneToMany, @ManyToMany에 기본으로 적용되어 있으며, @ManyToOne, @OneToOne은 EAGER로 기본으로 적용 되어 있기 때문에 두 어노테이션을 주의하면 될 것 같다.
정리
즉시 로딩은 N+1 문제점을 발생시킬 수 있으며, 지연 로딩을 사용함으로써 필요에 따라 연관된 객체를 조회하면 된다. @ManyToOne, @OneToOne은 EAGER로 기본으로 적용 되어 있기 때문에 두 개의 어노테이션을 주의하자.
본 포스팅은 “자바 ORM 표준 JPA 프로그래밍 - 기본 편/인프런”를 학습한 내용을 정리한 것
'Java > JPA' 카테고리의 다른 글
| <JPA> @Enumerated 사용 시 주의할 점 (0) | 2024.10.31 |
|---|---|
| <JPA> 벌크 연산시 주의할 점 (2) | 2024.10.03 |
| <JPA> 준영속 엔티티를 수정하는 2가지 방법 (0) | 2024.09.30 |
| <JPA> 임베디드 타입이란? (0) | 2024.09.24 |
| <JPA> @MappedSuperclass 이란? (0) | 2024.09.05 |
