쿼리의 N+1 발생 문제
Member를 조회하면서 Team도 같이 조회를 하고 싶은 경우
select m from Member m join m.team
join 을 써서 Team을 Member와 같이 가지고 올 경우 연관된 엔티티를 함께 조회하지 않기 때문에
이를 해결하기 위한 방법 중하나가 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();
- 일반 JOIN : Fetch Join과 달리 연관 Entity에 Join을 걸어도 실제 쿼리에서 SELECT 하는 Entity는 오직 JPQL에서 조회하는 주체가 되는 Entity만 조회하여 영속화
- FETCH JOIN: 조회의 주체가 되는 Entity 이외에 Fetch Join이 걸린 연관 Entity도 함께 SELECT 하여 모두 영속화
Fetchjoin을 설명하기 전에 Reposiroty의 일반 join 과 fetchjoin의 조회 메서드를 실행해서 동작하는 쿼리를 보며 둘의 차이를 보겠다.
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대상에 대한 영속성까지는 관여하지 않는다.
// 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
- 일반 Join : join 조건을 제외하고 실제 질의하는 대상 Entity에 대한 컬럼만 SELECT
- Fetch Join : 실제 질의하는 대상 Entity와 Fetch join이 걸려있는 Entity를 포함한 컬럼 함께 SELECT