[ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
JPQL
select m from Member m join fetch m.team
SQL
SELECT M.*, T.* FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID
회원을 조회하면서 연관된 팀도 SQL에서 한 번에 조회한다.
SQL을 보면 Member와 팀 Team을 함께 select 한다.
즉시 로딩으로 가져오는 방법과 똑같다.
단지 join fetch라고 명시적으로 선언해서 원하는 객체 그래프를 한 번에 조회하는 것
소속 팀이 없는 회원은 제외 되므로 inner join
이용
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
.getResultList();
for (Member member : members) {
//페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X
System.out.println("username = " + member.getUsername() + ", " +
"teamName = " + member.getTeam().name());
}
우리가 만약 fetch join
을 이용하지 않고 select m from Member m
쿼리를 날리면 무슨 일이 발생할까?
쿼리가 총 3번 나간다.
어떻게 쿼리가 3번이 나가는지 확인해보자.
=> N + 1 문제가 발생할 수 있다.
이럴 때 fetch join
을 이용해서 문제를 해결한다.
JPQL
select t
from Team t join fetch t.members
where t.name = ‘팀A'
SQL
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
for(Team team : teams) {
System.out.println("teamname = " + team.getName() + ", team = " + team);
for (Member member : team.getMembers()) {
//페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
System.out.println(“-> username = " + member.getUsername()+ ", member = " + member);
}
System.out.println()
}
teamname = 팀A, team = Team@0x100
username = 회원1, member = Member@0x200
username = 회원2, member = Member@0x300
teamname = 팀A, team = Team@0x100
username = 회원1, member = Member@0x200
username = 회원2, member = Member@0x300
TeamA에 Member가 2명이 있다. 그래서 TeamA 정보가 두번 출력되는 것을 확인할 수 있다.
(하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용된다.)
SQL의 DISTINCT는 중복된 결과를 제거하는 명령
JPQL의 DISTINCT는 2가지 기능을 제공한다.
select distinct t
from Team t join fetch t.members
where t.name = '팀A'
SQL에 DISTINCT를 추가하지만 ID(PK)정보가 다르므로 SQL결과에서 중복제거가 실패한다.
-> DISTINCT가 추가로 애플리케이션에서 중복 제거를 시도한다.
∴ 같은 식별자를 가진 Team 엔티티가 제거된다.
실제로 db에 distinct가 적용된 쿼리가 나간다.
하지만, db에 한 속성값이라도 다르다면 distinct가 되지 않는다.
그렇기에 실제로는 JPA에서 자체적으로 ID가 같다면 중복 제거를 해주는 것이다.
String jpql = "select distinct t from Team t join fetch t.members where t.name = '팀A'"
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
for(Team team : teams) {
System.out.println("teamname = " + team.getName() + ", team = " + team);
for (Member member : team.getMembers()) {
//페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
System.out.println(“-> username = " + member.getUsername()+ ", member = " + member);
}
System.out.println()
}
teamname = 팀A, team = Team@0x100
username = 회원1, member = Member@0x200
username = 회원2, member = Member@0x300
중복된 Team엔티티가 제거되어 출력되는 것을 확인할 수 있다.
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'
연관된 엔티티를 함께 조회하지 않는다.
JPQL은 결과를 반환할 때 연관관계를 고려하지 않는다.
-> SELECT 절에 지정한 엔티티만 조회한다.
jpql에 일반 join을 쓰면 sql에서 team 엔티티만 조회한다.
-> Member Entity는 조회하지 않는다.
Member Entity를 탐색하는 시점에 쿼리가 나간다.
JPQL
select t
from Team t join fetch t.members
where t.name = ‘팀A'
SQL
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
select t From Team t
//-- as 사용 불가
join fetch t.members as m where ...
Why?
fetch join은 연관된 모든 것을 가져오는 용도이다.
-> 즉시로딩으로 연관된 데이터를 모두 가져오는 것을 확인할 수 있다.
모든 데이터가 아닌 특정 데이터를 가져오고 싶다면 fetch join을 쓰면 안된다.
-> 원하는 회원 정보만 가져오고 싶다면 처음부터 회원에 대한 쿼리를 날리는 것이 맞다.
JPA는 객체 그래프 탐색으로 연관된 데이터를 모두 가져올 수 있는 상황을 의도한다.
-> 별칭을 이용해 특정 데이터만 가져올 수 있다면 예상치 못한 치명적 오류가 발생할 수 있다.
∴ 데이터의 정합성 & JPA의 객체 그래프 탐색 의도와 맞지 않으므로 가급적 사용하지 않는다.
일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다.
하지만 컬렉션을 페치 조인하면 하이버네이트는 경고 로그를 남기고 메모리에서 페이징한다.
(매우위험)
String query = "select t From Team t join fetch t.members m"
List<Team> result = em.createQuery(query, Team.class)
.setFirstResult(0)
.setMaxResult(1)
.getResultList();
System.out.println("result = " + result.size());
코드 실행 시 아래와 같은 경고가 발생한다.
메모리에서 페이징을 한다는 경고이다.
-> List가 100만건과 같이 엄청 큰 size인데, 페이징을 메모리에서 한다...?
장애발생이 유력하다...
WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
실행 결과
Hibernate:
/*select
t
From
Team t
join
fetch t.members m */ select
team0_.id as id1_3_0_,
members1_.id as id1_0_1_,
team0_.name as name2_3_0,
members1_.age as age2_0_1,
members1_.TEAM_ID as TEAM_ID5_0_1_,
members1_.type as type3_0_1_,
members1_.username as username4_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_0__,
members1_.id as id1_0_0__
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
result = 1
쿼리를 살펴보면 페이징 쿼리가 존재하지 않는다. -> 메모리에서 실행한다는 것을 확인할 수 있다.
page size = 1
이면 팀 A의 회원 2명이 출력되는 것이 아니라 1명만 출력된다.해결책
@BatchSize
String jpql = "select ";
List<Team> result = em.createQuery(query, Team.class)
.setFirstResult(0)
.setMaxResult(2)
.getResultList();
for (Team t : result) {
System.out.println("team = " + team.getName() + "|members=" + team.getMembers()
for (Member member : team.getMembers()) {
System.out.println("-> member = " + member);
}
아래 코드를 실행할 경우
이 경우 성능이 매우 좋지 않다. 팀이 여러개라면 그에 관련된 회원들을 불러올 때마다 새로운 SQL이 발생할 것을 예상할 수 있다.
따라서 페이징 때문에 fetch join을 사용하지 않을 때 @BatchSize
를 이용한다.
@Entity
public class Team {
...
@BatchSize(size = 100)
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
...
}
Team 엔티티에 @BatchSize
를 지정하고 똑같은 코드를 실행하면 아래와 같은 결과가 발생한다.
select
members...
from
Member members0_
where
members0_.TEAM_ID in (
?, ?
)
팀을 가져올 때 지연 로딩으로 되어있는 회원에 대해 where team_id in (A, B)로 가져온다.
각 팀마다 회원을 조회하는 쿼리가 발생했던 상황에 비해 매우 간결한 것을 확인할 수 있다.
@BatchSize
를 사용하면 된다.엔티티에 @BatchSize
어노테이션을 일일히 달아주지 않고 글로벌 옵션으로 사용할 수 있다.
resources/META-INF/persistence.xml
<property name="hibernate.default_batch_fetch_size" value="100" />
persistence.xml에 직접 해당 코드를 추가해준다.
연관된 엔티티들을 SQL 한 번으로 조회 가능하다. -> 성능 최적화
엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선시된다.
-> 글로벌 로딩 전략이
@OneToMany(fetch = FetchType.LAZY)
지연로딩이더라도 페치 조인이 우선시 된다.
실무에서 글로벌 로딩 전략은 모두 지연 로딩이다.
-> 최적화가 필요할 때 페치 조인을 적용한다.
모든 것을 fetch join으로 해결할 수 없다.
페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야하면,
페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
참고 :
김영한. 『자바 ORM 표준 JPA 프로그래밍』. 에이콘, 2015.