페치 조인(Fetch join)
특징
• SQL의 조인 종류가 아니다.
• JPQL에서 성능 최적화를 위해 제공하는 기능
=> 관련 객체 그래프를 SQL 한번에 조회하는 개념이다.
• 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
=> 페지 조인을 사용할 때 연관된 엔티티도 함께 즉시 로딩해서 조회
• join fetch 명령어 사용
=> 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
지연로딩
참고: https://ggp03016.tistory.com/68
저번 시간에 지연로딩에 대해 공부했었다. 지연로딩은 하나의 엔티티를 조회할 때 연관된 객체 그래프의 엔티티까지 즉시 로딩하지 않고, 프록시 객체로 두어 그 연관된 객체의 속성값을 호출하기 전까지 로딩시키지 않는 것을 의미한다. 원치 않는 객체까지 접근하여 일일히 조회 쿼리를 날리는 불편함을 없애기위해 가능하면 지연로딩을 설정하는 것이 좋다고 했었다.
예시로 Member에서의 Team 속성은 @ManyToOne(fetch = FetchType.LAZY) 로 설정하여 team객체의 속성값을 호출하지 않는이상 team의 데이터에 접근하지 않도록 설정했다. 이 상태에서 각 맴버의 팀 객체에 접근하도록 해보자.
예시코드
데이터 삽입
Team team1 = new Team();
team1.setName("팀1");
Team team2 = new Team();
team2.setName("팀2");
em.persist(team1);
em.persist(team2);
Member member1 = new Member();
member1.setAge(10);
member1.setUsername("회원1");
member1.setTeam(team1);
em.persist(member1);
Member member2 = new Member();
member2.setAge(10);
member2.setUsername("회원2");
member2.setTeam(team1);
em.persist(member2);
Member member3 = new Member();
member3.setAge(10);
member3.setUsername("회원3");
member3.setTeam(team2);
em.persist(member3);
em.flush();
em.clear();
팀1: 회원1, 회원2
팀2: 회원3
멤버의 팀 엔티티 조회
List<Member> resultList = em.createQuery("select m from Member m", Member.class)
.getResultList();
for (Member member : resultList) {
System.out.println("username = " + member.getUsername() + " team.name = " + member.getTeam().getName());
}
member.getTeam().getName() -> team 객체의 속성 name에 접근할 때, 팀이 2팀이므로 쿼리는 2개가 나간다. 아래 쿼리 예시가 있다.
회원들을 조회할 때 1번 쿼리가 나간다.
회원1의 team의 name을 조회할 때 쿼리가 1번 나가고, 이후 회원2의 team을 조회할 때는 이미 해당 팀이 1차 캐시에 있기 때문에 영속성 컨텍스트의 1차 캐시에서 값을 가져와서 출력한다. 회원3의 team은 또 처음 조회되기 때문에 쿼리를 1번 날려서 팀 데이터를 가져온다.
그래서 총 3번의 질의가 날라가는 것을 확인할 수 있다. 이를 통해 우리는 fetch join을 통해 한꺼번에 조회할 수 있는 편의성을 느낄 수 있다. (팀 수만큼 날아가는 질의가 더 많을 것이기 때문에)
페치 조인의 사용
엔티티 페치 조인
• 회원을 조회하면서 연관된 팀도 함께 조회(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
다대일 관계 (Member의 Team조회)
코드
List<Member> resultList = em.createQuery("select m from Member m fetch join m.team", Member.class)
.getResultList();
for (Member member : resultList) {
System.out.println("username = " + member.getUsername() + " team.name = " + member.getTeam().getName());
}
select m from Member m join fetch m.team
출력 결과
정말 신기하게도 Team 엔티티들이 모두 한쿼리로 조회가 완료된다.
다른 경우도 비교해보자 !!
1. 일반 Join의 쿼리, (fetch = FetchType.LAZY) 설정하고 처음부터 일반 join을 날린경우
코드
List<Member> resultList = em.createQuery("select m from Member m join m.team", Member.class)
.getResultList();
for (Member member : resultList) {
System.out.println("username = " + member.getUsername() + " team.name = " + member.getTeam().getName());
}
출력결과
쿼리에 join이 함께 날아간 모습이고, FetchType=Lazy이기 때문에 팀객체의 속성에 직접 접근할 때마다 쿼리를 한번씩 더날려서 로딩한 모습이다.
2. 일반 Join의 쿼리, 지연 로딩 설정 x
출력결과 즉시 로딩을 진행하여 쿼리가 모두 날라가고 팀이 모두 로딩이 된 후, 팀의 정보를 빼내올 때 1차캐시에 접근하여 이미 로딩된 팀정보를 가져오기만 하면된다. 순서가 그렇다.
하지만, 의문점인 것은 fetch join을 하게되면 쿼리 한번에 모든 팀객체 정보까지 접근할 수 있었는데, 현재 즉시로딩으로 했는데도 쿼리가 3개가 나가는가? 이다. 정확한 이유는 모르겠지만 fetch join이 관련 객체 그래프 조회에서 그만큼 효율적인 조회 방법이구나를 느낄 수 있었다.
일대다 관계 (Team의 Member조회)
이번엔 거꾸로 팀에서 맴버들의 정보에 접근해보자 !!
일대다 관계는 join으로 조회시 데이터가 뻥튀기 되는 현상을 꼭 조심해야 한다 !!
SELECT t FROM Team t JOIN FETCH t.members
코드
List<Team> resultList = em.createQuery("SELECT t FROM Team t JOIN FETCH t.members", Team.class)
.getResultList();
for (Team team : resultList) {
System.out.print("name = " + team.getName() + " ");
List<Member> memberList = team.getMemberList();
System.out.print("team.memberList: ");
for (Member member : memberList) {
System.out.print("member = " + member.getUsername() + " ");
}
System.out.println();
}
출력결과
**팀1의 데이터가 2번 출력되는 것을 확인할 수 있다. **
이는 DB에서 일대다 관계 조인 조회를 할 때 당연한 결과로, 팀 ID가 같더라도 Member ID가 다르기 때문에
조인결과는 팀의 멤버 수 만큼 더 뻥튀기되서 출력될 수 있는 것이다.
******일대다 관계 데이터 조인 조회할 때는 데이터가 뻥튀기 되는것을 꼭꼭꼭 주의하자 !! *********
Distinct
SQL은 중복된 데이터를 삭제하기 위해 DISTINCT 라는 키워드를 제공한다.
방금 조인된 결과에 DISTINCT 예약어를 사용해도 (TEAM ID, MEMBERID) 이 키 묶음이 완벽하게 일치하는 데이터가 없기 때문에 DB 상에서는 join결과가 그대로 나온다.
하지만!! JPQL의 DISTINCT는 다르다. 2가지 기능을 제공한다.
1. SQL의 DISTINCT 기능을 그대로 사용한다.
2. 추가로 에플리케이션에서 조회할 땐 엔티티 중복 제거가 가능하다.
2번을 더 파헤쳐 보자면
같은 식별자를 가진(즉 같은 TeamID를 가진) 엔티티를 제거하여 중복된 결과가 나오지 않도록 하는 것이다.
코드
em.createQuery("SELECT DISTINCT t FROM Team t JOIN FETCH t.members", Team.class)
출력결과
패치 조인의 한계
• 페치 조인 대상에는 별칭을 줄 수 없다.
ex) select t from Team t join fetch t.memberList as m;
=> m으로 뭔가를 하는건 불가능하다.
• 하이버네이트는 가능, 가급적 사용X
• 둘 이상의 컬렉션은 페치 조인 할 수 없다.
• 컬렉션을 페치 조인하면 페이징 API(setFirstResult,
setMaxResults)를 사용할 수 없다.
• 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
• 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
페치 조인 정리
• 모든 것을 페치 조인으로 해결할 수 는 없음
• 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
• 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른
결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요
한 데이터들만 조회해서 DTO로 반환하는 것이 효과적
이 게시물은 '자바 ORM 표준 JPA 프로그래밍' 강의를 수강하고 정리한 내용임을 밝힙니다.
출처: https://www.inflearn.com/course/ORM-JPA-Basic#
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 본 강의는 자바 백엔
www.inflearn.com