01. 페치 조인(fetch join) 이란?
페치 조인은 SQL의 조인 종류와는 무관하다.
JPQL에서 성능 최적화를 위해 제공하는 기능으로, 연관된 엔티티나 컬렉션의 SQL을 한 번에 함께 조회하는 기능이다.
쿼리로 내가 원하는 대로 어떤 객체 그래프를 한 번에 조회할 것을 동적인 타이밍에 정할 수 있다.
join fetch 명령어를 사용하면 된다.
페치 조인 ::= [LEFT [OUTER] | INNER] JOIN FETCH 조인경로
02. 엔티티 페치 조인
Member를 가져올 때 Team도 가져오고 싶은 경우, 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
이다. select m 만 적었는데 SELECT M.* T.* 를 보니 M과 T의 데이터를 모두 조회하는 것을 볼 수 있다.
예시를 통해 더 자세히 살펴보자.
다음 그림을 보자. 회원1, 회원2는 팀A에, 회원3은 팀B에 소속되어 있고, 회원4는 아무런 팀에 소속되어 있지 않다. 요구사항은 회원에서 팀을 가져오고 싶은데, 한 번에 가져오고 싶은 것이다.
inner join을 하게 되면 아래와 같은 테이블이 만들어진다.
컬렉션으로 데이터를 받을 경우 아래와 같은 그림이 된다.
아래는 예시 코드이다.
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Team teamC = new Team();
teamC.setName("팀C");
em.persist(teamC);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);
Member member4 = new Member();
member4.setUsername("회원4");
em.persist(member4);
em.flush();
em.clear();
String query = "select m From Member m";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
for (Member member : result) {
System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
}
Member와 Team의 연관관계가 @ManyToOne
이고 fetchType은 LAZY 이다. team은 프록시로 들어오며 지연로딩이 일어나고, getName()
을 호출하는 시점마다 DB에 쿼리를 날린다.
첫 번째 루프를 돌면서 teamA에 대한 정보를 영속성 컨텍스트에서 찾는데, 없어서 SQL 쿼리문을 날려서 실제 데이터를 조회한다.
두 번째 루프를 돌면서 teamA에 대한 정보를 찾지만 이미 영속성 컨텍스트에 해당 정보가 존재하기 때문에 1차 캐시에서 조회된다.
세 번째 루프를 돌면서 teamB에 대한 정보를 영속성 컨텍스트에서 찾는데, 없어서 SQL 쿼리문을 날려서 실제 데이터를 조회한다.
결과는 다음과 같다.
Hibernate:
/* select
m
From
Member m */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as team_id4_0_,
member0_.username as username3_0_
from
Member member0_
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
member = 회원1, 팀A
member = 회원2, 팀A
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
member = 회원3, 팀B
member = 회원4, null
위 예제에서는 페치 조인을 사용하지 않았다. 위의 예제에서, 문제점을 발견할 수 있다. 예를 들어 회원 100명을 조회한다고 하면, 회원을 가져오기 위한 1번의 쿼리로 안해 N번 더 쿼리를 보내게 된다. 이것을 N+1
문제라고 한다. 이것을 페치 조인으로 해결하자.
쿼리문을 다음과 같이 바꿔보자.
String query = "select m From Member m join fetch m.team";
실행 쿼리를 보면, 한번에 쿼리가 나가는 것을 볼 수 있다.
Hibernate:
/* select
m
From
Member m
join
fetch m.team */ select
member0_.id as id1_0_0_,
team1_.id as id1_3_1_,
member0_.age as age2_0_0_,
member0_.TEAM_ID as team_id4_0_0_,
member0_.username as username3_0_0_,
team1_.name as name2_3_1_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
member = 회원1, 팀A
member = 회원2, 팀A
member = 회원3, 팀B
member = 회원4, null
반복문을 돌 때의 getName()
을 호출 할 때의 team은 프록시 객체가 아니라 실제 데이터로 영속성 컨텍스트에 올라간 것이다.
지연로딩 설정을 해도, 페치 조인 세팅이 우선권을 가진다.
03. 컬렉션 페치 조인
이번에는 컬렉션 페치 조인을 살펴보자. JPQL은 다음과 같다. JPQL로 보면, Team 입장에서 Member를 join하는 것이다.
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 query = "select t From Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
for (Team team : result) {
System.out.println("team = " + team.getName() + " | members = " + team.getMembers().size());
}
실행 결과를 보면, 팀에 관한 정보가 중복으로 출력된 것을 볼 수 있다.
컬렉션 페치 조인을 사용하면 이것을 주의해야 한다. DB 입장에서 일대다 조인을 하게되면 데이터가 뻥튀기 된다.
팀A 입장에서는 하나지만 Member가 2개이므로 ROW가 2개가 된다. 그러나 JPA는 ROW가 2개인지 모른다.
조회하면 같은 주소값을 가진 결과가 두 줄이 나온다. 다음은 예시 코드이다.
for (Team team : result) {
System.out.println("team = " + team.getName() + " | members = " + team.getMembers().size());
for(Member member : team.getMembers()) {
System.out.println("-> member = " + member);
}
}
이해를 돕기 위해 강의 사진을 첨부한다.
04. 페치 조인과 DISTINCT
SQL의 DISTINCT 만으로는 중복을 다 제거하지 못한다.
중복 제거 코드는 다음과 같다.
String query = "select distinct t From Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
System.out.println("result = " + result.size());
for (Team team : result) {
System.out.println("team = " + team.getName() + " | members = " + team.getMembers().size());
for(Member member : team.getMembers()) {
System.out.println("-> member = " + member);
}
}
실행 결과는 다음과 같다.
05. 페치 조인과 일반 조인의 차이
일반 조인은 다음과 같이 작성하고, team만 가져온다.
String query = "select t From Team t join t.members m";
페치 조인은 다음과 같이 작성하고, team과 member를 가져온다.
String query = "select t From Team t join fetch t.members m";
'JPA' 카테고리의 다른 글
JPQL - 경로 표현식 (0) | 2024.02.09 |
---|---|
엔티티 작성 시 주의사항 (2) | 2024.01.30 |
객체지향 쿼리 언어1 - 프로젝션 (SELECT) (0) | 2024.01.18 |
객체지향 쿼리 언어1 - 기본 문법과 쿼리 API (0) | 2024.01.18 |
값 타입 컬렉션 (0) | 2024.01.18 |