[JPA] JPQL - 페치 조인(fetch join)

·2024년 4월 21일
0

JPA

목록 보기
16/17
post-thumbnail

💡페치 조인(fetch join)

실무에서 자주 사용되며 정말 중요하다

  • SQL 조인 종류는 아니고 JPA에서 제공하는 기능
  • 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;
- 팀이 없는 회원을 조회하고 싶을 때 fetch 조인을 사용하면 내부적으로 inner join을 사용한다. - 팀이 없는 회원은 누락된다.

📗페치 조인 사용 코드

  • 기존 연관관계 조회 로직의 문제점
String jpql = "select m from Member m";
List<Member> members = em.createQuery(jpql, Member.class) 
						.getResultList();
for (Member member : members) {
    System.out.println("username = " + member.getUsername() + ", " +
						"teamName = " + member.getTeam().name());
	//회원1, 팀A(SQL)
	//회원2, 팀A(1차 캐시)
	//회원3, 팀B(SQL)

	//회원 100명 -> N + 1
}

최초 jpql을 통해 Member를 조회할 때, Team의 정보를 프록시로 가져온다.
getTeam(), getName()을 통해 팀의 정보를 조회하려고 할때, 실제 객체를 SQL을 수행하여 가져온다.

한번 가져온 Team의 정보는 1차 캐시에 올라가 있으므로 다시 조회할 필요는 없지만, 회원 N명을 조회할 때 최대 N+1번의 쿼리가 수행될 수 있다.

  • 페치 조인을 통해 해결
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class) 
												 .getResultList();
for (Member member : members) {
    //페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X
    System.out.println("username = " + member.getUsername() + ", " +
					             "teamName = " + member.getTeam().name());
}

페치 조인은 조회 당시에 실제 엔티티가 담기므로 지연로딩 없이 바로 사용이 가능하다.

📗컬렉션 페치 조인

  • 일대다 관계, 컬렉션 페치 조인
//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';

➡️Team은 하나지만, Member가 1개 이상일 수도 있다.

  • 팀A는 1개이지만 그에 해당하는 멤버는 회원1과 회원2로 두명이므로 조회결과는 2개의 row가 된다.
  • 팀은 하나이기에 같은 주소값을 가진 결과가 두개 나오고, 팀 A의 입장에서는 회원1, 회원2를 총 두개를 가진다.

📗컬렉션 페치 조인 사용 코드

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);
    }
}

📗페치 조인과 DISTINCT

일대다 관계에서는 join fetch 결과가 뻥튀기 될 수 있다.

String query = "select t from Team t";
String query2 = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class).getResultList();

List<Team> result2 = em.createQuery(query2, Team .class).getResultList();

System.out.println("result size::"+ result.size()); //2
System.out.println("result2 size::"+ result2.size());//3
  • SQL의 DISTINCE는 중복된 결과를 제거하는 명령
  • JPQL의 DISTINCT는 2가지 기능을 제공
    1. SQL에 DISTINCT를 추가
    2. 애플리케이션에서 엔티티 중복 제거
select distinct t from Team t join fetch t.members where t.name = '팀A';
  • 위 코드를 실행하면 SQL에 DISTINCT를 추가하지만 ID(PK)도 다르고 NAME도 다르므로 중복제거 실패
  • 쿼리만으로는 중복제거가 안되기 때문에 JPA 추가적으로 DISTINCT가 애플리케이션에서 중복 제거를 시도
  • 같은 식별자를 가진 Team 엔티티 제거

📌반대로 다대일은 뻥튀기 되지 않는다

📌하이버네이트6 변경 사항

  • 하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용 된다.

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

  • 일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않음.
 //JPQL
select 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';
  • 해당 쿼리 수행 시 일반 조인은 연관된 엔티티를 먼저 조회하지 않기 때문엔 프록시 객체를 반환한다.
  • 실제로 해당 엔티티를 사용할 때 실제 값을 조회한다.
  • 반면, 페치 조인은 조회 시 연관관계도 함께 조회(즉시 로딩)
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념이다.

📗페치 조인의 특징과 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
    • 하이버네이트는 가능하지만, 가급적 사용하지 않는게 좋다.
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다.
	String query = "select t from Team t join fetch t.members, t.orders"
	//불가능. fetch join에서 컬렉션은 1개만 사용하자.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
    • 일대일, 다디앨 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
String query = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class)
						.setFirstResult(0)
						.setMaxResults(1)
						.getResultList();
  • 연관된 엔티티들을 SQL 한번으로 조회 - 성능 최적화
  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
    • @OneToMany(fetch = FetchType.LAZY)//글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
  • 최적화가 필요한 곳은 페치 조인 적용

📗페치 조인 - 정리

  • 모든 것을 페치 조인으로 해결 할 수는 없다.
  • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다
  • 여러 테이블을 조인해서 엔티티가 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
profile
백엔드 개발자를 꿈꿉니다 / 이전 블로그 : https://po-dadak.tistory.com/category

0개의 댓글

관련 채용 정보