Fetch Join(패치 조인)과 일반 Join의 차이점

김건우·2022년 12월 3일
1

Spring Data JPA

목록 보기
2/11
post-thumbnail

일반 join의 문제

쿼리의 N+1 발생 문제

Member를 조회하면서 Team도 같이 조회를 하고 싶은 경우

select m from Member m join m.team

join 을 써서 Team을 Member와 같이 가지고 올 경우 연관된 엔티티를 함께 조회하지 않기 때문에

  1. Member를 전체 조회하는 쿼리 한 번(이 때의 team은 프록시객체를 가져온다.)
  2. Member와 연관있는 Team을 조회하는 쿼리 한 번
    총, 두 번의 쿼리가 발생한다.
    (이 때 진짜 team객체를 가져온다. )

이를 해결하기 위한 방법 중하나가 FETCH JOIN이다.

  • SQL의 조인 종류 X
  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능

Fetch Join

Fetch join의 가장 단순한 뜻은 연관관계의 엔티티나 컬렉션을 프록시(가짜 객체)가 아닌 진짜 데이터를 한번에 같이 조회하는 기능이다!
JPQL에서 지원하는 기능으로 성능 최적화를 위해 사용한다.
연관된 엔티티나 컬렉션을 SQL로 한번에 조회하는 기능이다. JOIN FETCH 라는 명령어를 사용한다.

엔티티의 연관관계는 지연로딩으로 fetch = FetchType.LAZY 설정을 해줘야하는데 여기서 일반 JOIN을 사용하면 쿼리의 N+1문제가 발생한다.

엔티티 member는 team과 다 : 1 연관관계이고 지연로딩으로 설정되어잇다.

@Entity @Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of={"id","username","age"}) //@ToString은 가급적 내부 필드만(연관관계 없는 필드만!
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

    private Integer age;

    //member는 team 하나에만 소속될 수 있겠지? 그래서 @ManyToOne
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

}

team엔티티는 member와 1 : 다 연관관계이며 지연로딩으로 설정되어있다.

@Entity @Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(exclude = "memberes")
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "team_id")
    private Long id;

    private String name;

    //team은 여러개의 member를 가질 수 있겠지? 그래서 oneToMany
    @OneToMany(mappedBy = "team") //xxxToMany는 디폴트가 Lazy관계
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}

밑의 코드는 team을 조회하면서 fetch join으로 member엔티티까지 select해온다.

// TeamRepository.java
@Query("SELECT distinct t FROM Team t join fetch t.members")
public List<Team> findAllWithMemberUsingFetchJoin();

Fetch Join과 일반 Join의 차이점

  • 일반 JOIN : Fetch Join과 달리 연관 Entity에 Join을 걸어도 실제 쿼리에서 SELECT 하는 Entity는 오직 JPQL에서 조회하는 주체가 되는 Entity만 조회하여 영속화
  • FETCH JOIN: 조회의 주체가 되는 Entity 이외에 Fetch Join이 걸린 연관 Entity도 함께 SELECT 하여 모두 영속화

Fetchjoin을 설명하기 전에 Reposiroty의 일반 join 과 fetchjoin의 조회 메서드를 실행해서 동작하는 쿼리를 보며 둘의 차이를 보겠다.

일반Join을 사용

TeamRepository.java
@Query("SELECT distinct t FROM Team t JOIN t.members")
public List<Team> findAllWithMembersUsingJoin();

다음은 위의 일반 join사용시 동작하는 쿼리문

    select
         distinct team0_.id as id1_1_,
         team0_.name as name2_1_
    from team team0_ 
    inner join
        member members1_
            on team0_.id=members1_.team_id

보는것과 같이 team과 member가 join되어 쿼리가 실행이 되지만 가져오는 컬럼들은 Team의 컬럼만을 가져오고 있다.
즉, 일반 join에서 주의해야할 점은 실제 쿼리에는 join을 걸어주기는 하지만 join대상에 대한 영속성까지는 관여하지 않는다.

Fetch join을 사용

// TeamRepository.java
@Query("SELECT distinct t FROM Team t JOIN FETCH t.members")
public List<Team> findAllWithMembersUsingFetchJoin();

다음 아래의 쿼리믄 Fetch Join을 사용시 발생하는 쿼리문이다

Hibernate:
    select
         distinct team0_.id as id1_1_,
         members1_.id as id1_0_1_,
         team0_.name as name2_1_,
         members1_.age as age2_0_1,
         members1_.name as name3_0_1_,
         members1_.team_id as team_id4_0_1_,
         members1_.team_id as team_id4_0_1_,
         members1_.id as id1_0_1_
    from team team0_ 
    inner join
        member members1_
            on team0_.id=members1_.team_id

여기서 주의 깊게 봐야할 점은 fetch join을 하면 실제로 연관관계로 걸려있는 엔티티의 컬럼들마저 select해온다!!

  • 일반 Join : join 조건을 제외하고 실제 질의하는 대상 Entity에 대한 컬럼만 SELECT
  • Fetch Join : 실제 질의하는 대상 Entity와 Fetch join이 걸려있는 Entity를 포함한 컬럼 함께 SELECT

Fecth join의 한계

  1. 2개 이상의 컬렉션을 한 번에 페치 조인할 수 없다.
  2. 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
  3. 페치 조인 대상에는 별칭을 줄 수없다
profile
Live the moment for the moment.

0개의 댓글