[JPA] JPQL - 페치 조인1 - 기본
31 Aug 2022해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 듣고 정리한 글입니다.
페치 조인1 - 기본
페치 조인 (fetch join)
- SQL 조인 종류가 아님. (SQL 공식 문법이 아니다.)
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회라는 기능 객체 지향의 관점에서 존재하는 문법
- join fetch 명령어 사용
-
페치조인 : [ LEFT [ OUTER ] INNER ] JOIN FETCH 조인 경로
엔티티 페치 조인
- 회원을 조회하면서 연관된 팀도 함께 조회 (SQL 한 번에)
- SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT
- [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
예제
위 그림의 예제를 작성해 보도록하자.
먼저 teamA와 teamB 두개의 팀 객체를 만들고, member1, member2, member3의 3명의 회원 객체를 생성한다.
Team teamA = new Team();
teamA.setName("teamA");
entityManager.persist(teamA);
Team teamB = new Team();
teamB.setName("teamB");
entityManager.persist(teamB);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setAge(10);
member1.setType(MemberType.valueOf("USER"));
member1.setTeam(teamA);
entityManager.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setAge(10);
member2.setType(MemberType.valueOf("USER"));
member2.setTeam(teamA);
entityManager.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setAge(10);
member3.setType(MemberType.valueOf("USER"));
member3.setTeam(teamB);
entityManager.persist(member3);
만약 일반 SELECT m FROM Member m
Member Entity를 조회한뒤 객체 탐색을 이용해 Team객체를 가져온다면 어떻게 작동할까?
참고로 Team은 Member에서 fetchType.LAZY
로 선언되어있다.
String query1 = "SELECT m FROM Member m";
List<Member> members = entityManager.createQuery(query1, Member.class)
.getResultList();
for (Member member : members) {
System.out.println("member : " + member.getUsername() + ", team : " + member.getTeam().getName());
}
위의 코드를 실행하여, 나온 흐름은 아래와 같다.
/* Member 조회 */
Hibernate:
/* SELECT
m
FROM
Member m */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as team_id5_0_,
member0_.type as type3_0_,
member0_.username as username4_0_
from
Member member0_
/* TeamA 객체를 가져오기 위해 조회 */
Hibernate:
select
team0_.id as id1_3_0_,
team0_.age as age2_3_0_,
team0_.name as name3_3_0_
from
Team team0_
where
team0_.id=?
member : 회원1, team : teamA
member : 회원2, team : teamA
/* TeamB 객체를 가져오기 위해 조회 */
Hibernate:
select
team0_.id as id1_3_0_,
team0_.age as age2_3_0_,
team0_.name as name3_3_0_
from
Team team0_
where
team0_.id=?
member : 회원3, team : teamB
- 처음 Member 쿼리를 조회해 Member의 Entity들을 조회한다.
- 회원1의 TeamA가 FetchType.LAZY로 인해 프록시로 데이터를 가져와 영속성 컨텍스트에 없으므로, TeamA를 SELECT하는 쿼리를 날린다. SQL에서 조회
- 회원2의 TeamA의 데이터는 영속성 컨텍스트(1차캐시)에 존재하므로, 영속성 컨텍스트에 있는 값을 가져온다. 1차 캐시에서 조회
- 회원3의 TeamB의 데이터는 영속성 컨텍스트에 존재하지 않기 때문에, TeamB를 SELECT하는 쿼리를 새로 날려 데이터를 가져온다. SQL에서 조회
위와 같은 JPQL 쿼리의 문제점은 Team의 데이터를 가져오기 위해 위도치 않은 조회쿼리가 날라간다는 것이다. 만약 위의 데이터가 더 많은 Member 데이터를 가져온다면 수많은 Team의 조회쿼리가 날라갈 것이다.
이를 해소하기 위해 JPQL에서 지원하는 fetch join 기능을 사용해 보자.
String query1 = "SELECT m FROM Member m join fetch m.team";
List<Member> members = entityManager.createQuery(query1, Member.class)
.getResultList();
for (Member member : members) {
System.out.println("member : " + member.getUsername() + ", team : " + member.getTeam().getName());
}
/* 조인을 통해 한번에 데이터를 조회 */
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_id5_0_0_,
member0_.type as type3_0_0_,
member0_.username as username4_0_0_,
team1_.age as age2_3_1_,
team1_.name as name3_3_1_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
member : 회원1, team : teamA
member : 회원2, team : teamA
member : 회원3, team : teamB
fetch join을 사용할 경우 처음 Member 쿼리를 날릴때 Team을 조인해서 Team 프록시가 아닌 실제 객체를 가져오게 된다. 이후 조회되는 데이터는 SQL을 날리지 않고 데이터를 가져오는 것을 볼 수 있다.
컬렉션 페치 조인
- 일대다 관계, 컬렉션 페치 조인
- [JPQL] select t from Team t join fetch t.members where t.name = ‘팀A’
- [SQL] SELECT TEAM T INNER JOIN MEMBER M ON T.ID = M.TEAM_ID WHERE T.NAME = ‘팀A’
예시
이번엔 Team을 쿼리의 주체로 사용해 List
String query2 = "SELECT t FROM Team t join fetch t.members";
List<Team> teams = entityManager.createQuery(query2, Team.class)
.getResultList();
for (Team team : teams) {
System.out.println("team : " + team.getName() + ", size : " + team.getMembers().size());
for (Member member : team.getMembers()) {
System.out.println("-> member : " + member.getUsername() + ", hashcode : " + member.hashCode());
}
}
Hibernate:
/* SELECT
t
FROM
Team t
join
fetch t.members */ select
team0_.id as id1_3_0_,
members1_.id as id1_0_1_,
team0_.age as age2_3_0_,
team0_.name as name3_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
team : teamA, size : 2
-> member : 회원1, hashcode : 1628252344
-> member : 회원2, hashcode : 876487258
team : teamA, size : 2
-> member : 회원1, hashcode : 1628252344
-> member : 회원2, hashcode : 876487258
team : teamB, size : 1
-> member : 회원3, hashcode : 1276002922
컬렉션일 경우에는 주의해야 할점이 있다. 1:N의 관계를 조인해서 가져올 경우 데이터가 뻥튀기 되어서 가져온다.
출력된 결과를 확인하면 같은 hashcode를 가진 member객체와 team객체가 동일하게 들어가있는 것을 볼 수있다.
아래의 그림을 보면 컬렉션으로 가져오는것 순서를 확인해 수 있다.
그렇다면 중복을 제거하려먼 어떻게 해야 할까?
페치 조인과 DISTINCT
- SQL의 DISTINCT는 중복된 결과를 제거하는 명령
- JPQL의 DISTINCT 2가지 기능 제공
- SQL에 DISTINCT를 추가
- 애플리케이션에서 엔티티 중복 제거
- select distinct t from Team t join fetch t.members where t.name = ‘팀A’
예시
위의 컬렉션의 fetch join의 쿼리를 distinct를 통해 중복데이터를 제거해보자.
String query3 = "SELECT distinct t FROM Team t join fetch t.members";
List<Team> teams = entityManager.createQuery(query3, Team.class)
.getResultList();
for (Team team : teams) {
System.out.println("team : " + team.getName() + ", size : " + team.getMembers().size());
for (Member member : team.getMembers()) {
System.out.println("-> member : " + member.getUsername() + ", hashcode : " + member.hashCode());
}
}
Hibernate:
/* SELECT
distinct t
FROM
Team t
join
fetch t.members */ select
distinct team0_.id as id1_3_0_,
members1_.id as id1_0_1_,
team0_.age as age2_3_0_,
team0_.name as name3_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
team : teamA, size : 2
-> member : 회원1, hashcode : 1628252344
-> member : 회원2, hashcode : 876487258
team : teamB, size : 1
-> member : 회원3, hashcode : 1276002922
이렇게 중복된 데이터가 제거되고 정상적으로 조회된것을 확인 할 수 있다.
하지만 여기서 의문점이 생긴다. SQL의 DISTINCT의 경우 완벽하게 중복되는 row데이터를 없애는 명령어이다.
Team의 데이터는 아래와 같이 들어있는데 어떻게 중복을 제거할까?
JPA의 DISTINCT
- DISTINCT가 추가로 애플리케이션에서 중복 제거시도
- 같은 식별자를 가진 Team 엔티티 제거
그래서 아래와 같이 데이터가 중복이 제거되어 표기되는 것이다.
team : teamA, size : 2
-> member : 회원1, hashcode : 1628252344
-> member : 회원2, hashcode : 876487258
team : teamB, size : 1
-> member : 회원3, hashcode : 1276002922
참고
1:N의 관계는 데이터가 위와 같이 뻥튀기가 될 수있어 DISTINCT를 사용해야 한다. N:1의 관계는 크게 문제가 없으므로 편하게 사용 해도 된다.
페치 조인과 일반 조인의 차이
- 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음
- [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’
예시
String query3 = "SELECT t FROM Team t join t.members";
List<Team> teams = entityManager.createQuery(query3, Team.class)
.getResultList();
for (Team team : teams) {
System.out.println("team : " + team.getName() + ", size : " + team.getMembers().size());
for (Member member : team.getMembers()) {
System.out.println("-> member : " + member.getUsername() + ", hashcode : " + member.hashCode());
}
}
Hibernate:
/* SELECT
t
FROM
Team t
join
t.members */ select
team0_.id as id1_3_,
team0_.age as age2_3_,
team0_.name as name3_3_
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
Hibernate:
select
members0_.TEAM_ID as team_id5_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.age as age2_0_1_,
members0_.TEAM_ID as team_id5_0_1_,
members0_.type as type3_0_1_,
members0_.username as username4_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
team : teamA, size : 2
-> member : 회원1, hashcode : 378227888
-> member : 회원2, hashcode : 974631651
team : teamA, size : 2
-> member : 회원1, hashcode : 378227888
-> member : 회원2, hashcode : 974631651
Hibernate:
select
members0_.TEAM_ID as team_id5_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.age as age2_0_1_,
members0_.TEAM_ID as team_id5_0_1_,
members0_.type as type3_0_1_,
members0_.username as username4_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
team : teamB, size : 1
-> member : 회원3, hashcode : 1303122461
fetch join과는 다르게 기존의 Member 만 호출 했을때와 같이 쿼리가 쭉쭉 나가는 것을 볼 수 있다.
페치 조인과 일반 조인의 차이
- 일반 조인
- JPQL은 결과를 반환할 때 연관관계 고려X
- 단지 SELECT 절에 지정한 엔티티만 조회할 뿐
- 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회X
- 페치 조인
- 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
- 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념