오봉이와 함께하는 개발 블로그
JPA - 페치 조인 기본 본문
페치 조인
실무에서 정말 정말 중요하다!
- SQL 조인의 종류가 아니다.
- JPQL에서 성능 최적화를 위해 제공하는 기능이다.
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능히다.
- join fetch 명령어 사용
- 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
엔티티 페치 조인
- N:1 관계
- 회원을 조회하며 연관돤 팀도 함께 조회하고 싶다.(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
- 즉시 로딩과 유사하지만 이건 내가 원하는 방향으로 어떤 객체 그래프를 한 번에 조회할지 동적인 타이밍에 정할 수 있다.
코드 예시
// 1 번
String query = "select m from Member m ";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
for (Member member : result) {
System.out.println("username = " + member.getUsername() + ", " + "teamName = " + member.getTeam().getName());
}
// 2 번
String query = "select m from Member m join fetch m.team";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
for (Member member : result) {
System.out.println("username = " + member.getUsername() + ", " + "teamName = " + member.getTeam().getName());
}
1번 코드는 조회 당시에는 Member만 가져오고 실제 member.getTeam().getName()을 할 때 프록시 객체를 통해 쿼리문이 날아간다.(지연로딩이기 때문)
1번 코드에서 회원1을 조회하고 팀을 조회할 때는 쿼리문을 통해 조회되지만, 회원 2를 조회할 때는 이미 1차 캐시에 팀 A가 있기 때문에 쿼리문이 날아가지 않고 조회가 된다.
또 회원 3을 조회하면 팀B가 들어있기 때문에 쿼리문이 또 날아가서 팀B가 조회된다.
이 코드로 조회하면 최악의 경우 쿼리가 4번 나가고 현재의 경우는 쿼리가 3번 나갔다.
만약 회원 100명을 조회하고, 모두 팀이 다를 경우에는 최악의 경우 101개의 쿼리가 나간다.
이 문제를 N+1 문제라고 한다.
이럴 때 fetch join을 사용해야 한다!
2번 코드를 사용하면 join을 통해 한방 쿼리가 나가서 이미 원하는 모든 값을 조회했다.
그렇기 때문에 영속성 컨텍스트에 모든 결과가 등록되어 있고 프록시 객체를 사용하지 않는다.
지연 로딩으로 설정을 해도 fetch join이 우선이 된다.
컬렉션 페치 조인
- 1:N 관계나 컬렉션을 사용할 때 사용한다.
- 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() + " ㅣ members = " + team.getMembers().size());
}
// 결과
team = team1 ㅣ members = 2
team = team1 ㅣ members = 2
team = team2 ㅣ members = 1
결과가 중복으로 출력되는데, 컬렉션일 때는 이 점을 주의해야 한다.
DB 입장에서는 1:N 조인을 하면 데이터가 뻥튀기된다. JPA도 마찬가지다.
Team 입장에서는 1개지만, Member가 2개이기 때문에 row가 2개가 된다.
객체(JPA) 입장에서는 이 상황을 알 수 없기 때문에 DB에서 조회가 되는 그대로 값을 가져올 수 밖에 없다.
값은 하나지만, 결과는 두 개기 때문에 조회한 컬렉션에는 같은 주소값을 두고 하나의 값을 공유해서 사용한다.
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);
}
}
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
팀A를 조회하면 팀A에 소속된 모든 회원들이 조회가 되고, 모든 회원의 수 만큼 팀A가 조회된다.
페치 조인과 DISTINCT
중복이 싫다면 DISTINCT 옵션을 통해서 제거할 수 있다.
SQL의 DISTINCT는 중복된 결과를 제거하는 명령이지만
JPQL의 DISTINCT는 SQL문에 DISTINCT를 추가하는 기능과 애플리케이션에서 엔티티 중복을 제거하는 기능도 해준다.
- select distinct t from Team t join fetch t.members where t.name = '팀A'
- SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과 에서 중복제거 실패
DISTINCT의 추가로 JPQL은 애플리케이션에서 중복 제거를 시도한다.
컬렉션을 퍼 올릴 때 같은 식별자를 가진 Team 엔티티를 제거한다.
[DISTINCT 추가시 결과]
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
페이 조인과 일반 조인의 차이
[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 절에 지정한 엔티티만 조회한다.
- 여기서는 팀 엔티티만 조회하고 회원 엔티티는 조회하지 않는다.(지연 로딩, 프록시 전략)
- 하지만 페치 조인을 사용할 때는 연관된 엔티티도 함께 조회한다(즉시 로딩)
- 페치 조인은 객체 그래프 SQL 한번에 조회하는 개념이다
대부분의 N+1 문제는 fetch join을 통해 해결할 수 있다!
출처 : 인프런 김영한 지식공유자님의 스프링 부트와 JPA 실무 완전 정복 로드맵 강의
'BE > JPA' 카테고리의 다른 글
JPA - 다형성 쿼리, 엔티티 직접 사용, Named 쿼리 (0) | 2022.07.06 |
---|---|
JPA - 페치 조인 한계 (0) | 2022.07.05 |
JPA - 경로 표현식 (0) | 2022.07.01 |
JPA - JPQL 함수 (0) | 2022.06.30 |
JPA - JPQL 타입 표현과 기타식, 조건식 (0) | 2022.06.30 |