프레임워크(Framework)/JPA

[JPA] JPQL 페치 조인(Fetch join) - 객체지향 쿼리 언어 JPQL (8)

잇트루 2022. 12. 20. 02:00
반응형
본 내용은 온라인 강의 사이트 인프런의 김영한 님의 강의 내용이 포함되어 있습니다.
'자바 ORM 표준 JPA 프로그래밍 - 기본편'
 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

페치 조인(Fetch Join)

페치 조인은 JPQL에서 성능 최적화를 위해 제공하는 기능이다.

  • SQL 조인의 종류가 아니다.

연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능으로 join fetch 명령어로 사용할 수 있다.

  • 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인 경로

 

 

엔티티 페치 조인

페치 조인을 사용하여 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회하는 경우

// 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
  • join fetch 사용
    • 회원(m)과 팀(m.team)을 함께 조회
  • 페치 조인의 경우 별칭을 사용할 수 없다.

 

 

엔티티 페치 조인 예시

  • 엔티티 페치 조인 JPQL 코드에서 select m으로 회원 엔티티만 선택하여 실행
  • 실행된 SQL에서는 SELECT M.*, T.*로 Member와 연관된 Team도 함께 조회
  • Member와 Team 객체가 객체 그래프를 유지하면서 조회된 것

 

 

페치 조인 사용 코드

String jpql = "select m from Member m join fetch m.team";

List<Member> members = em.createQuery(jpql, Member.class).getResultList();

for (Member member : members) {
		System.out.println("username = " + member.getUsername() + ", " +
				"teamname = " + member.getTeam().name());
}
// 출력
username = 회원1, teamname = 팀A
username = 회원2, teamname = 팀A
username = 회원3, teamname = 팀B
  • 페치 조인을 사용하면 회원과 팀을 지연 로딩(LAZY)으로 설정해도 지연 로딩이 일어나지 않는다.
  • 실제 엔티티를 조회하므로 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.

 

 

컬렉션 페치 조인

일대다 관계인 컬렉션을 페치 조인할 경우

// JPQL
select t from Team t fetch t.members where t.name = 'teamA'

// 실행된 SQL
SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
  • 팀(t)을 조회하면서 페치 조인을 사용하여 연관된 회원 컬렉션(t.members)도 함께 조회한다.

 

 

컬렉션 페치 조인 예시

  • 컬렉션을 페치 조인한 JPQL에서 select t로 팀만 선택하여 실행
  • 실행된 SQL에서는 SELECT T.*, M.*로 팀과 연관된 회원도 함께 조회
  • TEAM 테이블에서 팀 A는 하나지만 MEMBER 테이블과 조인을 하면서 결과가 증가
    • 팀 A가 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);
    }
}
// 출력
teamname = 팀A, team = hellojpa.jpql.Team@1e60b459
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
teamname = 팀A, team = hellojpa.jpql.Team@1e60b459
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
  • 컬렉션을 페치 조인하면 팀 A는 하나임에도 불구하고, MEMBER 테이블과 조인하면서 결과가 증가한다.
    • 팀 A의 결과가 2건 조회된다.
  • 팀과 회원을 지연 로딩(LAZY)으로 설정해도 지연 로딩이 일어나지 않는다.
  • 이러한 문제를 해결하기 위해 DISTINCT로 중복된 결과를 제거할 수 있다.

 

 

페치 조인과 DISTINCT

SQL에서 DISTINCT는 중복된 결과를 제거하는 명령어이다.

JPQL의 DISTINCT 명령어는 2가지 기능을 제공한다.

  • SQL에 DISTINCT를 추가
  • 애플리케이션에서 엔티티 중복 제거
select distinct t from Team t fetch t.members where t.name = 'teamA'
teamname = 팀A, team = hellojpa.jpql.Team@4fe533ff
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
  • DISTINCT가 추가로 애플리케이션에서 중복 제거를 시도한다.
  • 같은 식별자를 가진 Team 엔티티를 제거(팀 A가 2번 조회되는 것을 방지)

 

 

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

페치 조인을 사용하지 않는 경우

// JPQL
select t from Team t join t.members m where t.name = 'teamA'

// 실행된 SQL
SELECT T.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
  • JPQL에서 팀과 회원 컬렉션을 함께 조회한다고 해도 연관된 회원 컬렉션도 함께 조회하지 않는다.
  • JPQL은 결과를 반환할 때 연관관계까지 고려하지 않으며, SELECT 절에 지정한 엔티티만 조회한다.
  • 지연 로딩(LAZY) 설정 시 프록시나 초기화하지 않은 컬렉션 래퍼를 반환한다.
  • 즉시 로딩(EAGER)으로 설정 시 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한 번 더 실행한다.

 

 

페치 조인을 사용하는 경우

// JPQL
select t from Team t join fetch t.members where t.name = 'teamA'

// 실행된 SQL
SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
teamname = 팀A, team = hellojpa.jpql.Team@4fe533ff
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
teamname = 팀A, team = hellojpa.jpql.Team@4fe533ff
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
  • 팀과 회원 컬렉션을 함께 조회할 시 연관된 회원 컬렉션도 함께 조회한다.

 

 

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

일반 조인

  • JPQL은 결과를 반환할 때 연관관계를 고려하지 않는다.
  • SELECT 절에 지정한 엔티티만 조회할 뿐이다.
  • 일반 조인의 경우 팀 엔티티만 조회하고, 회원 엔티티는 조회하지 않는다.

 

페치 조인

  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회한다. (즉시 로딩)
  • 페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념이다.

 

 

페치 조인의 특징과 한계

페치 조인의 특징

  • 페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티를 함께 조회한다. - 성능 최적화
  • 페치 조인은 글로벌 로딩 전략보다 우선적으로 실행된다.
    • 글로벌 로딩 전략 : @OneToMany(fetch = FetchType.XXX)
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩이다.
  • 최적화가 필요한 곳은 페치 조인을 적용한다.(N + 1 문제 해결 등)

 

페치 조인의 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
    • 하이버네이트는 가능하지만 가급적 사용하지 않는 것이 좋다.
  • 둘 이상의 컬렉션을 페치 할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
    • 페이징 API : setFirstResult, setMaxResults
    • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다.
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징 하므로 매우 위험하다.

 

페치 조인 정리

  • 모든 것을 페치 조인으로 해결할 수는 없다.
  • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
  • 여러 테이블을 조인하여 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 일반 조인을 사용한다.
    • 필요한 데이터들만 조회하여 DTO로 반환하는 것이 효과적
반응형