Member를 조회할 때 꼭 Team도 함께 조회해야 할까?
단순히 Member 정보만 사용하는 비즈니스 로직에서 Team을 조회할 필요는 없지 않나?
이런 질문에서 시작되는 개념이 즉시 로딩과 지연로딩이다.
@Entity
public class Member {
@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;
}
em.find()
를 실행하면 member
객체가 반환되며,
member.getTeam()
객체는 Team객체가 아닌 프록시 객체가 들어가게 된다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId()); // (1)
Team findTeam = findMember.getTeam();
System.out.println(findTeam.getClass()); // class hellojpa.Team$HibernateProxy$SfG9Y9qu
System.out.println(emf.getPersistenceUnitUtil().isLoaded(findTeam)); // false
findTeam.getName(); // (2) 실제 team을 사용하는 시점에서 초기화
System.out.println(emf.getPersistenceUnitUtil().isLoaded(findTeam)); // true
위의 (1)
코드가 실행될 때 나오는 쿼리는 다음과 같다.
select
member0_.MEMBER_ID as member_i1_2_0_,
member0_.TEAM_ID as team_id3_2_0_,
member0_.USERNAME as username2_2_0_
from
Member member0_
where
member0_.MEMBER_ID=?
보면 알 수 있듯이 Team
을 같이 조회하지 않음을 알 수 있다.
위의 (2)
코드가 실행될 때 나오는 쿼리는 다음과 같다.
select
team0_.TEAM_ID as team_id1_4_0_,
team0_.NAME as name2_4_0_
from
Team team0_
where
team0_.TEAM_ID=?
즉 findTeam.getName()
을 하는 시점 team
의 정보를 필요로 하는 시점에서야 조회를 한다.
이 때 프록시 객체가 초기화되는데 이것을 LAZY 지연로딩 전략이라 한다.
Member와 Team을 함께 자주 사용한다면 즉시 로딩을 고려할 수 있다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name="USERNAME")
private String username;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
즉시 로딩(EAGER)은 Member 조회시 항상 Team도 조회한다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId()); // (1)
Team findTeam = findMember.getTeam();
System.out.println(findTeam.getClass()); // class hellojpa.Team
위의 (1)
코드가 실행될 때 나오는 쿼리는 다음과 같다.
select
member0_.member_id as member_i1_7_0_,
member0_.age as age2_7_0_,
member0_.team_id as team_id4_7_0_,
member0_.username as username3_7_0_,
team1_.team_id as team_id1_14_1_,
team1_.name as name2_14_1_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.member_id=?
지연로딩과 달리 Team
의 정보까지 같이 가져옴을 알 수 있다.
예를 들어 Member
가 10개정도의 객체와 연관관계가 있다면 em.find()
를 할 때 join 쿼리가 10개나 나오게 된다. 이런 경우 쿼리가 엄청 길어지고 성능도 낮아진다.
Team team = new Team();
team.setName("TeamA");
Team team2 = new Team();
team2.setName("TeamB");
em.persist(team);
em.persist(team2);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(team2);
em.persist(member);
em.persist(member2);
em.flush();
em.clear();
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
select
member0_.MEMBER_ID as member_i1_2_,
member0_.TEAM_ID as team_id3_2_,
member0_.USERNAME as username2_2_
from
Member member0_
select
team0_.TEAM_ID as team_id1_4_0_,
team0_.NAME as name2_4_0_
from
Team team0_
where
team0_.TEAM_ID=?
select
team0_.TEAM_ID as team_id1_4_0_,
team0_.NAME as name2_4_0_
from
Team team0_
where
team0_.TEAM_ID=?
select m from Member m
이라는 쿼리로 2개의 member
가 리스트에 들어온다.
이 2개의 member
는 각각 team
객체가 필요하므로 2개의 select 쿼리가 나간다.
N+1문제란 처음의 select m from Member m
쿼리 1개에 이 쿼리의 결과로 나온 N개의 객체에 대한 쿼리 N개가 더 나간다고 해서 N+1문제이다.
지연 로딩을 사용한다면 Member의 정보만 필요할 때는 Member의 정보만 가져오고,
Team의 정보도 같이 필요하면 fetch 조인을 사용하여 N+1 문제를 해결할 수 있다.
그러나 즉시 로딩을 사용한다면 Member와 Team의 정보를 같이 필요로 할 때
fetch 조인을 사용하여 N+1 문제를 해결할 수 있지만,
Member의 정보만 필요로 할 때 Member의 정보만 가져올 수 없다.
위의 즉시로딩 주의점에 대해 설명한 것을 근거로 사용하지 않을 것을 권장한다.