JPA 게시글은 대부분 인프런의 김영한님의 강의인 '자바 ORM 표준 JPA 프로그래밍' 기반으로 내용을 정리했습니다.
즉시 로딩과 지연 로딩
나는 여전히 Member만을 조회하고 싶어.
단순히 Member 정보만 사용하는 비즈니스 로직 같은 경우에 어떻게 Member만 조회해올 수 있을까? 이전 글에서는 프록시에 대해 배웠지만 사실 그건 잘 사용하지 않고, 지연 로딩을 사용해서 프록시로 조회하는 방법을 많이 사용한다.
지연 로딩 LAZY
// Member.class 중 일부
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
Member 클래스에서 Team과 연관관계 매핑 애노테이션에 fetch = FetchType.LAZY 속성을 넣어주면 된다. LAZY 속성을 설정하면 해당 Team 객체를 프록시 객체로 조회하고 Member 클래스만 DB에서 조회하게 된다.
// 실행 클래스에서 Member 조회
Member m = em.find(Member.class, member1.getId());
// 출력 로그
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.createdBy as createdB2_3_0_,
member0_.createdDate as createdD3_3_0_,
member0_.lastModifiedBy as lastModi4_3_0_,
member0_.lastModifiedDate as lastModi5_3_0_,
member0_.TEAM_ID as TEAM_ID7_3_0_,
member0_.USERNAME as USERNAME6_3_0_
from
Member member0_
where
member0_.MEMBER_ID=?
실행 클래스에서 Member를 조회하게 되면 SELECT 쿼리에 member만 조회된 걸 확인할 수 있다.
//...생략
Member m = em.find(Member.class, member1.getId());
System.out.println("m = " + m.getTeam().getClass());
System.out.println("==========");
m.getTeam().getName();
System.out.println("==========");
//...생략
자 이제 추가로 member 객체에 team 데이터를 넣고 위의 코드를 실행하면 Team을 getClass()하는 경우 프록시 객체 정보가 반환된다. 그리고 Team과 관련된 데이터를 호출할 때는 그제서야 Team에 프록시 초기화가 되면서 DB에 SELECT 쿼리가 전송된다.
// 출력 로그
m = class hellojpa.Team$HibernateProxy$6DzDe8wW
==========
Hibernate:
select
team0_.TEAM_ID as TEAM_ID1_5_0_,
team0_.createdBy as createdB2_5_0_,
team0_.createdDate as createdD3_5_0_,
team0_.lastModifiedBy as lastModi4_5_0_,
team0_.lastModifiedDate as lastModi5_5_0_,
team0_.name as name6_5_0_
from
Team team0_
where
team0_.TEAM_ID=?
==========
Member와 Team을 자주 함께 사용한다면?
LAZY말고 EAGER를 사용하면 된다. Member 클래스에서 연관관계 매핑 fetch 속성을 수정한다.
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
그러면 처음 Member를 SELECT 하는 쿼리가 전송될 때의 SELECT에서 Member와 Team을 JOIN해서 한 번에 가져온다.
// 출력 로그
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.createdBy as createdB2_3_0_,
member0_.createdDate as createdD3_3_0_,
member0_.lastModifiedBy as lastModi4_3_0_,
member0_.lastModifiedDate as lastModi5_3_0_,
member0_.TEAM_ID as TEAM_ID7_3_0_,
member0_.USERNAME as USERNAME6_3_0_,
team1_.TEAM_ID as TEAM_ID1_5_1_,
team1_.createdBy as createdB2_5_1_,
team1_.createdDate as createdD3_5_1_,
team1_.lastModifiedBy as lastModi4_5_1_,
team1_.lastModifiedDate as lastModi5_5_1_,
team1_.name as name6_5_1_
from
Member member0_
left outer join
Team team1_
on member0_.TEAM_ID=team1_.TEAM_ID
where
member0_.MEMBER_ID=?
이렇게 em.find()로 가져올 때부터 연관관계에 있는 객체에 대한 테이블을 JOIN 해오는게 즉시 로딩이다.
프록시와 즉시로딩 주의
- 가급적 지연 로딩만 사용한다.(특히 실무에서)
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
- 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.
// ... 생략
Team team = new Team();
team.setName("teamA");
em.persist(team);
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(teamB);
em.persist(member2);
em.flush();
em.clear();
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
// ... 생략
위의 코드에서는 member1과, member2가 존재하며 각각 다른 팀을 가지고있다. 이 둘을 JPQL SELECT문으로 member를 조회해오면 member1의 팀과 member2의 팀에 대한 데이터를 가져오기 위해 SELECT가 두 번 나가게 된다.
// 출력 로그
Hibernate:
/* select
m
from
Member m */ select
member0_.MEMBER_ID as MEMBER_I1_3_,
member0_.createdBy as createdB2_3_,
member0_.createdDate as createdD3_3_,
member0_.lastModifiedBy as lastModi4_3_,
member0_.lastModifiedDate as lastModi5_3_,
member0_.TEAM_ID as TEAM_ID7_3_,
member0_.USERNAME as USERNAME6_3_
from
Member member0_
Hibernate:
select
team0_.TEAM_ID as TEAM_ID1_5_0_,
team0_.createdBy as createdB2_5_0_,
team0_.createdDate as createdD3_5_0_,
team0_.lastModifiedBy as lastModi4_5_0_,
team0_.lastModifiedDate as lastModi5_5_0_,
team0_.name as name6_5_0_
from
Team team0_
where
team0_.TEAM_ID=?
Hibernate:
select
team0_.TEAM_ID as TEAM_ID1_5_0_,
team0_.createdBy as createdB2_5_0_,
team0_.createdDate as createdD3_5_0_,
team0_.lastModifiedBy as lastModi4_5_0_,
team0_.lastModifiedDate as lastModi5_5_0_,
team0_.name as name6_5_0_
from
Team team0_
where
team0_.TEAM_ID=?
N + 1은 결국에 1 = JPQL로 제일 처음 전달된 쿼리, N = 처음 쿼리로 인해 추가 쿼리가 N개가 나간다는 의미이다. 이 문제는 LAZY로 설정하면 해결되는 문제이다.
- @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 LAZY로 설정해주어야 한다.
- @OneToMany, @ManyToMany는 기본이 지연 로딩이다.
- 한 번에 조회해야 할 때는 fetch join을 사용한다. (후술)
지연 로딩 활용 - 이론
- Member와 Team은 자주 함께 사용 -> 즉시 로딩
- Member와 Order는 가끔 사용 -> 지연 로딩
- Order와 Product는 자주 함께 사용 -> 즉시로딩
지연 로딩 활용 - 실무
- 모든 연관관계에 지연 로딩을 사용해라!
- 실무에서 즉시 로딩을 사용하지 마라!
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라! (후술)
- 즉시 로딩은 상상하지 못한 쿼리가 나가 성능 이슈를 불러올 수 있다.