*목차
fetch join 특징 설명
DB SQL, JPA fetch join 쿼리 비교
연관 엔티티 LAZY 조회 확인
한계 극복
fetch join을 이용한 연관 엔티티 조회
fetch join을 이용한 연관 컬렉션 조회
한계 극복
fetch join & 일반 조인 차이
* 페치 조인 특징
1. SQL 조인 종류가 아니다.
2 JPQL에서 성능 최적화를 위해 제공한다.
3. 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회한다.
페치 조인은, 일반적으로 SQL 문법을 사용했다면 낯선 용어입니다. 왜냐하면 전통 SQL의 방식이 아니기 때문입니다. 페치 조인은 JPQL에서 제공하는 쿼리 방식으로, 연관된 엔티티나 컬렉션을 한번에 조회하고 싶을 때 사용합니다.
일반적인 DB SQL과 JPA fetch join 을 예제와 함께 비교해 보겠습니다.
* DB SQL, JPA fetch join 쿼리 비교
//JPQL
select m from Member m join fetch m.team
//SQL
select m.*, t.* from Member m inner join m.team t
on m.team_id = t.id
JPQL에서는 별칭을 써야 합니다. 또한 페치 조인은 join fetch로 사용합니다.
* 초기 Member, Team 엔티티 DB에 저장하기(팀 2개, 회원 3명)
Member : Team이 1: N의 연관관계를 가지고 있다고 가정합니다.
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Member memberA = new Member();
memberA.setName("회원1");
memberA.setTeam(teamA);
em.persist(memberA);
Member memberB = new Member();
memberB.setName("회원2");
memberB.setTeam(teamA);
em.persist(memberB);
Member memberC = new Member();
memberC.setName("회원3");
memberC.setTeam(teamB);
em.persist(memberC);
em.flush();
em.clear();
팀A에는 회원1이 속해있으며, 팀B에는 회원2, 회원3이 속해 있습니다.
* 단순 LAZY 전략을 사용한 연관 엔티티 조회
String query = "select m From Member m";
List<Member> members = em.createQuery(query, Member.class)
.getResultList();
for(Member member : members) {
System.out.println("username=" + member.getName() + ","
+ "teamName=" + member.getTeam().getName());
}
단순 LAZY 전략을 사용하여 조회하는데, 기준은 Member 입니다. Member들은 영속성 컨텍스트에, Team은 프록시로 호출됩니다.
Hibernate:
/* select
m
From
Member m */ select
member0_.id as id1_3_,
member0_.age as age2_3_,
member0_.city as city3_3_,
member0_.street as street4_3_,
member0_.zipcode as zipcode5_3_,
member0_.name as name6_3_,
member0_.TEAM_ID as TEAM_ID9_3_,
member0_.endDate as endDate7_3_,
member0_.startDate as startDat8_3_
from
Member member0_
Hibernate:
select
team0_.id as id1_5_0_,
team0_.name as name2_5_0_
from
Team team0_
where
team0_.id=?
username=회원1,teamName=팀A
username=회원2,teamName=팀A
Hibernate:
select
team0_.id as id1_5_0_,
team0_.name as name2_5_0_
from
Team team0_
where
team0_.id=?
username=회원3,teamName=팀B
N + 1 문제가 발생합니다. 왜냐하면 Team의 정보를 프록시에 보관하고 있습니다. member.getTeam().getName()은, 프록시에 있는 Team의 name을 조회합니다. 따라서, Member를 조회하는 쿼리 1개와, 그 결과로 조회된 Member 3명 중, 팀 A와 팀 B 2개의 팀이 있으므로 추가적으로 2개가 더 호출됩니다. 따라서 총 3개가 호출되었으며, 만약 모든 Member가 다른팀에 속했다면, 실제로 최대 쿼리 갯수인 4개가 발생했을 것입니다. ( 3 + 1 )
* join fetch를 사용한 연관 엔티티 조회
String query2 = "select m From Member m join fetch m.team";
List<Member> members2 = em.createQuery(query2, Member.class)
.getResultList();
for(Member member : members2) {
System.out.println("username=" + member.getName() + ","
+ "teamName=" + member.getTeam().getName());
}
join fetch를 사용해 조회합니다. LAZY와 달리, Member 뿐 아니라, Team도 바로 영속성 컨텍스트로 한번에 조회합니다.
Hibernate:
/* select
m
From
Member m
join
fetch m.team */ select
member0_.id as id1_3_0_,
team1_.id as id1_5_1_,
member0_.age as age2_3_0_,
member0_.city as city3_3_0_,
member0_.street as street4_3_0_,
member0_.zipcode as zipcode5_3_0_,
member0_.name as name6_3_0_,
member0_.TEAM_ID as TEAM_ID9_3_0_,
member0_.endDate as endDate7_3_0_,
member0_.startDate as startDat8_3_0_,
team1_.name as name2_5_1_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
username=회원1,teamName=팀A
username=회원2,teamName=팀A
username=회원3,teamName=팀B
member.getTeam().getName()이 LAZY때와는 달리, 추가적인 쿼리를 발생하지 않고도 조회됩니다. 왜냐하면, 처음 조회 시에, Member, Team의 정보를 한번에 영속성 컨텍스트로 가져왔기 때문입니다. 그래서 Team을 조회하기 위해 추가적으로 3번 발생했던 쿼리문이 필요하지 않습니다.
EAGER는 연관관계 갯수가 적으면 괜찮지만, 연관관계가 많아질수록 예상치 못한 쿼리가 엄청 추가됩니다.
LAZY를 사용했다면, Member를 먼저 조회하고, 추후에 Team에 접근할 때, 추가적으로 쿼리를 사용합니다.
FETCH JOIN을 사용했다면, Member와 연관된 Team까지 1번의 쿼리로 조회합니다.
(참고로, fetch 조인은 LAZY 로딩 전략보다 우선순위가 높습니다.)
* fetch join을 이용한 연관 컬렉션 조회
연관 엔티티를 조회할 때보다, 연관 컬렉션을 조회할 때 특히 조심해야 합니다. 왜냐하면, 결과가 뻥튀기가 될 수 있기 때문입니다.
현재, Team은 2개, Member는 3개입니다. 여기서 Team을 조회한다면 Team은 총 몇개가 나와야 할까요? 어떤 방식으로 조회하든, Team은 총 2개가 나와야 하는 것이 맞습니다. 하지만 join fetch로 조회한다면, 팀A를 기준으로 회원1, 회원2 라는 컬렉션 때문에 실제보다 더 많은 수를 반환합니다.
String query3 = "select t From Team t join fetch t.members";
List<Team> teams = em.createQuery(query3, Team.class)
.getResultList();
for(Team team : teams) {
System.out.println("username=" + team.getName() + ","
+ "members=" + team.getMembers().size());
}
Hibernate:
/* select
t
From
Team t
join
fetch t.members */ select
team0_.id as id1_5_0_,
members1_.id as id1_3_1_,
team0_.name as name2_5_0_,
members1_.age as age2_3_1_,
members1_.city as city3_3_1_,
members1_.street as street4_3_1_,
members1_.zipcode as zipcode5_3_1_,
members1_.name as name6_3_1_,
members1_.TEAM_ID as TEAM_ID9_3_1_,
members1_.endDate as endDate7_3_1_,
members1_.startDate as startDat8_3_1_,
members1_.TEAM_ID as TEAM_ID9_3_0__,
members1_.id as id1_3_0__
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
username=팀A,members=2
username=팀A,members=2
username=팀B,members=1
Team은 분명히 2개인데 조회결과 3개가 출력되었습니다. 왜냐하면, 엔티티 컬렉션을 fetch join으로 조회하면 inner join하는 과정에서 데이터가 뻥튀기가 되기 때문입니다. 팀A와 연관관계 있는 Member는 2개이므로 2개 모두 출력됩니다.
같은 주소값을 가진 결과가 2개 나옵니다. 1개만 나와도 되는데, 주소값까지 같은 엔티티가 2개가 반환되었습니다.
* 한계 극복
distinct를 사용해서 한계를 극복해봅시다.
* JPQL의 DISTINCT 2가지 기능
1. SQL에 DISTINCT를 추가
2. 애플리케이션의 중복 엔티티 제거
주소값이 같고 중복되는 엔티티를 distinct를 사용해서 제거합니다.
String query4 = "select distinct t From Team t join fetch t.members";
List<Team> teams2 = em.createQuery(query4, Team.class)
.getResultList();
for(Team team : teams2) {
System.out.println("username=" + team.getName() + ","
+ "members=" + team.getMembers().size());
}
Hibernate:
/* select
distinct t
From
Team t
join
fetch t.members */ select
distinct team0_.id as id1_5_0_,
members1_.id as id1_3_1_,
team0_.name as name2_5_0_,
members1_.age as age2_3_1_,
members1_.city as city3_3_1_,
members1_.street as street4_3_1_,
members1_.zipcode as zipcode5_3_1_,
members1_.name as name6_3_1_,
members1_.TEAM_ID as TEAM_ID9_3_1_,
members1_.endDate as endDate7_3_1_,
members1_.startDate as startDat8_3_1_,
members1_.TEAM_ID as TEAM_ID9_3_0__,
members1_.id as id1_3_0__
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
username=팀A,members=2
username=팀B,members=1
그 결과 3개의 출력문을 냈던 이전 실행과 달리 중복을 제거하고 2개만 출력합니다
* fetch join & 일반 조인 차이
- 일반 조인은 실행 시, 연관된 엔티티를 함께 조회하지 않습니다.
JPQL은 결과를 반환할 때, 연관관계를 고려하지 않습니다. 단지 SELECT 절에 지정한 엔티티만 조회합니다.
- fetch join을 사용할 때만 연관된 엔티티도 함께 조회됩니다.
페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념입니다.
//JPQL
select t from Team t join t.members m where t.name = '팀A'
//SQL
select t.* from Team t inner join Member m on t.id = m.team_id
where t.name ='팀A'
일반 조인을 사용하면, Team 엔티티만 조회하고, Member 엔티티를 조회하지 않습니다.
//JPQL
select t from Team t join fetch t.members where t.name='팀A'
//SQL
select t.*, m.* from Team t inner join t.members m
on t.id = m.team_id where t.name='팀A'
페치 조인을 사용하면, Team 엔티티뿐만 아니라, Member 엔티티까지 조회합니다.
일반 조인 | 페치 조인 | |
로딩방식 | 즉시로딩 OR 지연로딩 | 즉시로딩 |
연관 엔티티 조회 여부 | 연관 엔티티 조회 X | 연관 엔티티 조회 O |
최대쿼리 횟수 | N + 1 | 1 |
* 참고
'Spring > Spring JPA' 카테고리의 다른 글
JPA를 이용해 History 테이블 자동 생성하기 (1) | 2021.12.14 |
---|---|
페치 조인 ( fetch join ) - 2 (0) | 2021.10.06 |
N + 1 문제 (0) | 2021.09.30 |
영속성 전이(CASCADE)와 고아객체 (0) | 2021.09.14 |
JPA 프록시(+즉시로딩, 지연로딩 비교) (0) | 2021.09.14 |