[Spring Boot] JPA 강의 정리-35 (JPQL 페치조인-기본)

Posted by 김성철

SpringBoot - JPA 강의 정리-35(JPQL 페치조인 - 기본)

※ 실무에서 정말정말 중요함  
※ 실무에서 제일많이 사용함  
※ 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념  
※ 대부분의 N+1 문제는 페치 조인으로 해결이 됨  

페치 조인 (fetch join)

- SQL 조인 종류가 아님  
- JPQL에서 성능 최적화를 위해 제공하는 기능  
- 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 기능  
- join fetch 명령어 사용  
- 페치 조인 ::= [LEFT [ OUTER] | INNER] JOIN FETCH 조인 경로  
- @ManyToOne(fetch = FetchType.LAZY) 으로 세팅을 해도 fetch 이 우선순위가 높아서 실행됨  

엔티티 페치 조인

회원을 조회하면서 연관된 팀도 함께 조회 (SQL 한번에)  
SQL 을 보면 회원뿐만 아니라 (T.*)도 함께 SELECT  
  
- 작성 쿼리  
=====================================================================  
select m from Member m join fetch m.team  
=====================================================================  
  
- 실행 쿼리  
=====================================================================  
select m.* , t.* from member m  
innert join team t on m.team_id = t.id  
=====================================================================  

페치 조인 사용 전 예제

아래와 같이 실행하면 N+1 문제가 발생함  
1은 처음이 실행한 쿼리이고 N은 가져온 갯수만큼 쿼리를 또 날리는 것을 의미함  
  
- 작성 코드  
아래의 코드에서는 String query = "select m From Member m"; 쿼리로 실행함  
=====================================================================  
Team tema1 = new Team();  
tema1.setName("teamA");  
em.persist(tema1);  
  
Team tema2 = new Team();  
tema2.setName("teamB");  
em.persist(tema2);  
  
Team tema3 = new Team();  
tema3.setName("teamC");  
em.persist(tema3);  
  
Member member = new Member();  
member.setUsername("회원1");  
member.setAge(8);  
member.setType(MemberType.ADMIN);  
member.setTeam(tema1);  
em.persist(member);  
  
Member member2 = new Member();  
member2.setUsername("회원2");  
member2.setAge(8);  
member2.setType(MemberType.ADMIN);  
member2.setTeam(tema1);  
em.persist(member2);  
em.flush();  
  
Member member3 = new Member();  
member3.setUsername("회원3");  
member3.setAge(8);  
member3.setType(MemberType.ADMIN);  
member3.setTeam(tema2);  
em.persist(member3);  
em.flush();  
em.clear();  
  
String query = "select m From Member m";  
//String query = "select m From Member m join fetch m.team";  
List<Member> result = em.createQuery(query , Member.class)  
		.getResultList();  
for(Member resultMember : result){  
	System.out.println("member : " +resultMember.getUsername() +" , team : " + resultMember.getTeam().getName());  
}  
=====================================================================  
  
- 실행 쿼리  
=====================================================================  
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_  
Hibernate:  
	select  
		team0_.id as id1_3_0_,  
		team0_.name as name2_3_0_  
	from  
		Team team0_  
	where  
		team0_.id=?  
member : 회원1 , team : teamA  
member : 회원2 , team : teamA  
Hibernate:  
	select  
		team0_.id as id1_3_0_,  
		team0_.name as name2_3_0_  
	from  
		Team team0_  
	where  
		team0_.id=?  
member : 회원3 , team : teamB  
  
=====================================================================  

페치 조인 사용 후 예제

- 작성코드는 위의 사용 전 예제와 똑같으나 쿼리문만 다름  
=====================================================================  
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_id5_0_0_,  
			member0_.type as type3_0_0_,  
			member0_.username as username4_0_0_,  
			team1_.name as name2_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  
=====================================================================  

컬렉션 페치 조인

- 일대다 관계 , 컬렉션 페치 조인  
- 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 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() + " | " + team.getMembers().size());  
}  
=====================================================================  
  
- 실행 쿼리  
=====================================================================  
Hibernate:  
	/* select  
		t  
	from  
		Team t  
	join  
		fetch t.members */ 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  
team = teamA | 2  
team = teamA | 2  
team = teamB | 1  
=====================================================================  
  
teamA는 하나지만 속한 Member는 두명이기때문에 teamA에 대한 데이터가 두개 생김  
  
- 작성 코드  
=====================================================================  
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() + " | " + team.getMembers().size());  
	for(Member resultMember : team.getMembers()){  
		System.out.println("## member : " + resultMember);  
	}  
}  
=====================================================================  
  
- 실행 쿼리  
=====================================================================  
Hibernate:  
	/* select  
		t  
	from  
		Team t  
	join  
		fetch t.members */ 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  
## team = teamA | 2  
## member : Member{id=4, username='회원1', age=8, team=domain.Team@534e58b6, type=ADMIN}  
## member : Member{id=5, username='회원2', age=8, team=domain.Team@534e58b6, type=ADMIN}  
## team = teamA | 2  
## member : Member{id=4, username='회원1', age=8, team=domain.Team@534e58b6, type=ADMIN}  
## member : Member{id=5, username='회원2', age=8, team=domain.Team@534e58b6, type=ADMIN}  
## team = teamB | 1  
## member : Member{id=6, username='회원3', age=8, team=domain.Team@1df1ced0, type=ADMIN}  
=====================================================================  
  
위와같이 teamA 의 데이터가 중복으로 출력됨  

페치 조인과 DISTINCT

- SQL의 DISTINCT는 중복된 결과를 제거하는 명령  
- JPQL의 DISTINCT 2가지 기능 제공  
	- 1. SQL에 DISTINCT를 추가  
	- 2. 애플리케이션에서 엔티티 중복 제거  
		같은 식별자를 가진 Team 엔티티 제거  
  
- SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과에서 중복 제거 실패  
	ex)  
		id : 1  
		name : teamA  
		memberId : 1  
		team_id : 1  
		name : 회원1  
  
		id : 1  
		name : teamA  
		memberId : 2  
		team_id : 1  
		name : 회원2  
  
- 작성 코드  
=====================================================================  
String query = "select distinct 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() + " | " + team.getMembers().size());  
	for(Member resultMember : team.getMembers()){  
		System.out.println("## member : " + resultMember);  
	}  
}  
=====================================================================  
  
- 실행 쿼리  
=====================================================================  
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_.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  
## team = teamA | 2  
## member : Member{id=4, username='회원1', age=8, team=domain.Team@1b495d4, type=ADMIN}  
## member : Member{id=5, username='회원2', age=8, team=domain.Team@1b495d4, type=ADMIN}  
## team = teamB | 1  
## member : Member{id=6, username='회원3', age=8, team=domain.Team@32b0876c, type=ADMIN}  
=====================================================================  

페치 조인과 일반 조인의 차이

- 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음  
- JPQL은 결과를 반환할 때 연관관계 고려하지 않음  
- 단지 SELECT 절에 지정한 엔티티만 조회할 뿐  
- 여기서는 팀 엔티티만 조회하고 회원 엔티티는 조회하지 않음  
- 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)  
- 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념  
  
- 일반 조인 JPQL  
=====================================================================  
select t from Team t join t.members m where t.name ='teamA'  
=====================================================================  
  
- 일반 조인 실행 쿼리  
=====================================================================  
Hibernate:  
	/* select  
		t  
	from  
		Team t  
	join  
		t.members m */ select  
			team0_.id as id1_3_,  
			team0_.name as name2_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 | 2  
## member : Member{id=4, username='회원1', age=8, team=domain.Team@4f94e148, type=ADMIN}  
## member : Member{id=5, username='회원2', age=8, team=domain.Team@4f94e148, type=ADMIN}  
## team = teamA | 2  
## member : Member{id=4, username='회원1', age=8, team=domain.Team@4f94e148, type=ADMIN}  
## member : Member{id=5, username='회원2', age=8, team=domain.Team@4f94e148, type=ADMIN}  
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 | 1  
## member : Member{id=6, username='회원3', age=8, team=domain.Team@7ff8a9dc, type=ADMIN}  
=====================================================================  
  
- 페치 조인 JPQL  
=====================================================================  
String query = "select t from Team t join fetch t.members m";  
=====================================================================  
  
- 페치 조인 실행 쿼리  
=====================================================================  
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  
## team = teamA | 2  
## member : Member{id=4, username='회원1', age=8, team=domain.Team@1b495d4, type=ADMIN}  
## member : Member{id=5, username='회원2', age=8, team=domain.Team@1b495d4, type=ADMIN}  
## team = teamA | 2  
## member : Member{id=4, username='회원1', age=8, team=domain.Team@1b495d4, type=ADMIN}  
## member : Member{id=5, username='회원2', age=8, team=domain.Team@1b495d4, type=ADMIN}  
## team = teamB | 1  
## member : Member{id=6, username='회원3', age=8, team=domain.Team@32b0876c, type=ADMIN}  
=====================================================================