N+1발생하는 경우를 살펴보겠습니다. 이번 포스팅에선 fetch = EAGER인 경우만 살펴보겠습니다!
일대다 관계를 맺는 엔티티가 있고 fetch전략이 EAGER인 경우
부모 객체를 findAll()을 통해 조회하면 N+1이 발생합니다.
학습테스트를 통해 확인해보겠습니다.
Team 엔티티
@Entity
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
@Column(name = "team_name")
private String name;
@OneToMany(fetch = FetchType.EAGER, cascade = {PERSIST, REMOVE})
@JoinColumn(name = "team_id")
private List<Member> members = new ArrayList<>();
protected Team() {
}
public Team(String name, List<Member> members) {
this.name = name;
this.members = members;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<Member> getMembers() {
return members;
}
}
Member 엔티티
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@Column(name = "member_name")
private String name;
protected Member() {
}
public Member(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
학습테스트 코드
@BeforeEach
void setUp() {
for (int i = 0; i<3; ++i) {
Member member = new Member("member" + i);
Team team = new Team("team1", List.of(member));
teamRepository.save(team);
}
em.flush();
em.clear();
}
@DisplayName("oneToMany(fetch = EAGER)일때 N+1 발생 확인")
@Test
void oneToManyEager_N_1() {
System.out.println("@@@ N+1 @@@");
List<Team> teams = teamRepository.findAll();
System.out.println("@@@ N+1 @@@");
assertThat(teams).hasSize(3);
assertThat(teams.get(0).getMembers()).hasSize(1);
}
쿼리
@@@ N+1 @@@
Hibernate:
select
team0_.team_id as team_id1_5_,
team0_.team_name as team_nam2_5_
from
team team0_
Hibernate:
select
members0_.team_id as team_id3_1_0_,
members0_.member_id as member_i1_1_0_,
members0_.member_id as member_i1_1_1_,
members0_.member_name as member_n2_1_1_
from
member members0_
where
members0_.team_id=?
Hibernate:
select
members0_.team_id as team_id3_1_0_,
members0_.member_id as member_i1_1_0_,
members0_.member_id as member_i1_1_1_,
members0_.member_name as member_n2_1_1_
from
member members0_
where
members0_.team_id=?
Hibernate:
select
members0_.team_id as team_id3_1_0_,
members0_.member_id as member_i1_1_0_,
members0_.member_id as member_i1_1_1_,
members0_.member_name as member_n2_1_1_
from
member members0_
where
members0_.team_id=?
@@@ N+1 @@@
3개의 팀에 대한 member를 찾기위해 3번의 쿼리가 더 나간것을 알 수 있다.
만약 팀이 1000개, 10000개 혹은 10만개였으면 성능에 큰 문제가 발생했을 것이다.
fetch join은 SQL에서 이야기하는 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다. ( LEFT | INNER ) JOIN FETCH
문법을 통해 사용할 수 있다. fetch join을 사용하는 repository 메서드를 만들고 이를 사용해 해결하자.
Repository
public interface TeamRepository extends JpaRepository<Team, Long> {
@Query(value = "SELECT distinct t FROM Team t join fetch t.members")
List<Team> findAllWithMember();
}
학습테스트 코드
@DisplayName("oneToMany(fetch = EAGER) fetch join으로 N+1 해결")
@Test
void oneToManyEager_N_1_Fetch_Join() {
System.out.println("@@@ fetch join @@@");
List<Team> teams = teamRepository.findAllWithMember();
System.out.println("@@@ fetch join @@@");
assertThat(teams).hasSize(3);
assertThat(teams.get(0).getMembers()).hasSize(1);
}
쿼리
@@@ fetch join @@@
Hibernate:
select
distinct team0_.team_id as team_id1_5_0_,
members1_.member_id as member_i1_1_1_,
team0_.team_name as team_nam2_5_0_,
members1_.member_name as member_n2_1_1_,
members1_.team_id as team_id3_1_0__,
members1_.member_id as member_i1_1_0__
from
team team0_
inner join
member members1_
on team0_.team_id=members1_.team_id
@@@ fetch join @@@
1+3만큼 나가던 쿼리가 단 한권으로 해결되는 것을 확인할 수 있다!!
BatchSize는 collection이나 lazy entity를 batch로 가져오게 해준다.
WHERE절이 같은 여러 개의 SELECT 쿼리들을 하나의 IN 쿼리로 만들어준다.
1. Entity의 클레스 레벨에 붙이는 경우엔 프록시로 호출하는 경우만 동작한다.
Member엔티티 클래스 레벨에 @BatchSize를 붙여보았습니다.
@BatchSize 적용한 Member엔티티
@BatchSize(size = 100)
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@Column(name = "member_name")
private String name;
protected Member() {
}
public Member(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
쿼리
@@@ N+1 @@@
Hibernate:
select
team0_.team_id as team_id1_5_,
team0_.team_name as team_nam2_5_
from
team team0_
Hibernate:
select
members0_.team_id as team_id3_1_0_,
members0_.member_id as member_i1_1_0_,
members0_.member_id as member_i1_1_1_,
members0_.member_name as member_n2_1_1_
from
member members0_
where
members0_.team_id=?
Hibernate:
select
members0_.team_id as team_id3_1_0_,
members0_.member_id as member_i1_1_0_,
members0_.member_id as member_i1_1_1_,
members0_.member_name as member_n2_1_1_
from
member members0_
where
members0_.team_id=?
Hibernate:
select
members0_.team_id as team_id3_1_0_,
members0_.member_id as member_i1_1_0_,
members0_.member_id as member_i1_1_1_,
members0_.member_name as member_n2_1_1_
from
member members0_
where
members0_.team_id=?
@@@ N+1 @@@
N+1이 해결되지 않았습니다. 클래스 레벨에서 @BatchSize는 프록시로 호출될때 적용되는데, fetch = EAGER인 경우 프록시 객체가 아니기 때문에 적용되지 않음을 알 수 있습니다.
2. 필드에 붙이는 경우엔 N+1 해결
Team 엔티티에서 컬렉션으로 가지고 있는 Member 엔티티에 필드레벨에서 @BatchSize를 붙이면 N+1이 해결되는 것을 알 수 있습니다.
@BatchSize 적용한 Team엔티티
@Entity
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
@Column(name = "team_name")
private String name;
@BatchSize(size = 100)
@OneToMany(fetch = FetchType.EAGER, cascade = {PERSIST, REMOVE})
@JoinColumn(name = "team_id")
private List<Member> members = new ArrayList<>();
protected Team() {
}
public Team(String name, List<Member> members) {
this.name = name;
this.members = members;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<Member> getMembers() {
return members;
}
}
쿼리
@@@ N+1 @@@
Hibernate:
select
team0_.team_id as team_id1_5_,
team0_.team_name as team_nam2_5_
from
team team0_
Hibernate:
select
members0_.team_id as team_id3_1_1_,
members0_.member_id as member_i1_1_1_,
members0_.member_id as member_i1_1_0_,
members0_.member_name as member_n2_1_0_
from
member members0_
where
members0_.team_id in (
?, ?, ?
)
@@@ N+1 @@@
@EntityGraph라는 것을 사용해 해결할 수도 있다. repository에 @EntityGraph를 붙여주고 사용하면 된다. 코드로 확인해보자
Repository
@EntityGraph(attributePaths = "members")
@Query(value = "SELECT t FROM Team t")
List<Team> findAllEntityGraph();
쿼리
@@@ N+1 @@@
Hibernate:
select
distinct team0_.team_id as team_id1_5_0_,
members1_.member_id as member_i1_1_1_,
team0_.team_name as team_nam2_5_0_,
members1_.member_name as member_n2_1_1_,
members1_.team_id as team_id3_1_0__,
members1_.member_id as member_i1_1_0__
from
team team0_
inner join
member members1_
on team0_.team_id=members1_.team_id
@@@ N+1 @@@
@OneToMany(fetch = EAGER)에서 N+1이 발생하는 경우와 이를 해결할 수 있는 3가지 방법(fetch join, @BatchSize, @EntityGraph)에 대해 간략히 살펴보았다. 다음 정리에선 fetch = LAZY에 대해서, 그리고 카테시안 곱을 방지하는 방법에 대해 알아보겠다.
아래 블로그를 한 번 읽어보고 포스팅할 예정이다!!
https://jojoldu.tistory.com/165
https://jojoldu.tistory.com/457
참고
자바 ORM 표준 JPA 프로그래밍
https://velog.io/@mohai2618/JPA-N1-%EB%AC%B8%EC%A0%9C-%EC%B5%9C%EC%A0%81%ED%99%94