이번 포스팅에선 fetch = LAZY일때 N+1이 발생하는 경우를 살펴보겠습니다.
일대다 관계를 맺는 엔티티가 있고 fetch전략이 LAZY인 경우
지연로딩 덕분에 findAll()을 해도 N+1이 발생하지 않습니다.
하지만 프록시 객체로 가져온 자식의 초기화가 일어나게 되면 N+1이 발생합니다. 이 경우를 학습테스트를 통해 살펴보겠습니다.
Team
Entity
public class Team2 {
@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.LAZY, cascade = {PERSIST, REMOVE})
@JoinColumn(name = "team_id")
private List<Member2> member2s = new ArrayList<>();
protected Team2() {
}
public Team2(String name, List<Member2> member2s) {
this.name = name;
this.member2s = member2s;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<Member2> getMembers() {
return member2s;
}
}
Member
@Entity
public class Member2 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@Column(name = "member_name")
private String name;
protected Member2() {
}
public Member2(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
TeamService
@Service
public class Team2Service {
private final Team2Repository team2Repository;
public Team2Service(Team2Repository team2Repository) {
this.team2Repository = team2Repository;
}
@Transactional(readOnly = true)
public List<String> findAllMembersName() {
List<Team2> team2s = team2Repository.findAll();
return team2s.stream()
.flatMap(team2 -> team2.getMembers().stream())
.map(Member2::getName)
.collect(Collectors.toList());
}
}
학습테스트
@DisplayName("oneToMany(fetch = LAZY)에서 N+1이 발생한다.")
@Test
void oneToMany_Lazy_N_1() {
System.out.println("@@@ N+1 @@@");
List<String> allMembersName = team2Service.findAllMembersName();
System.out.println("@@@ N+1 @@@");
assertThat(allMembersName).hasSize(teamNumber * memberNumberPerTeam);
}
쿼리
@@@ N+1 @@@
Hibernate:
select
team2x0_.team_id as team_id1_7_,
team2x0_.team_name as team_nam2_7_
from
team2 team2x0_
Hibernate:
select
member2s0_.team_id as team_id3_2_0_,
member2s0_.member_id as member_i1_2_0_,
member2s0_.member_id as member_i1_2_1_,
member2s0_.member_name as member_n2_2_1_
from
member2 member2s0_
where
member2s0_.team_id=?
Hibernate:
select
member2s0_.team_id as team_id3_2_0_,
member2s0_.member_id as member_i1_2_0_,
member2s0_.member_id as member_i1_2_1_,
member2s0_.member_name as member_n2_2_1_
from
member2 member2s0_
where
member2s0_.team_id=?
Hibernate:
select
member2s0_.team_id as team_id3_2_0_,
member2s0_.member_id as member_i1_2_0_,
member2s0_.member_id as member_i1_2_1_,
member2s0_.member_name as member_n2_2_1_
from
member2 member2s0_
where
member2s0_.team_id=?
@@@ N+1 @@@
Team에 속한 모든 Member의 이름을 가져오는 로직에서 N+1이 발생함을 알 수 있습니다.
TeamService
@Transactional(readOnly = true)
public List<String> findAllMembersNameFetchJoin() {
List<Team2> team2s = team2Repository.findAllFetchJoin();
return team2s.stream()
.flatMap(team2 -> team2.getMembers().stream())
.map(Member2::getName)
.collect(Collectors.toList());
}
TeamRepository
@Query(value = "SELECT distinct t FROM Team2 t join fetch t.member2s")
List<Team2> findAllFetchJoin();
학습 테스트
@DisplayName("fetch join으로 N+1 해결")
@Test
void oneToMany_Lazy_N_1_Fetch_join() {
System.out.println("@@@ fetch join @@@");
List<String> allMembersName = team2Service.findAllMembersNameFetchJoin();
System.out.println("@@@ fetch join @@@");
assertThat(allMembersName).hasSize(teamNumber * memberNumberPerTeam);
}
쿼리
@@@ fetch join @@@
Hibernate:
select
distinct team2x0_.team_id as team_id1_7_0_,
member2s1_.member_id as member_i1_2_1_,
team2x0_.team_name as team_nam2_7_0_,
member2s1_.member_name as member_n2_2_1_,
member2s1_.team_id as team_id3_2_0__,
member2s1_.member_id as member_i1_2_0__
from
team2 team2x0_
inner join
member2 member2s1_
on team2x0_.team_id=member2s1_.team_id
@@@ fetch join @@@
fetch join을 사용하면 N+1이 해결됨을 알 수 있습니다!
클래스 레벨에 @BatchSize를 붙여도 프록시 객체이기 때문에 적용이 될 줄 알았다. 하지만 N+1문제가 해결되지 않고 여러개의 쿼리가 발생하는것을 확인할 수 있었다. 왜 그런지 찾아보았지만 아직은 이유를 찾지 못했다. 다음에 학습하고 내용을 추가해보겠다.
Team 엔티티의 List<Member> 필드
@BatchSize(size = 100)
@OneToMany(fetch = FetchType.LAZY, cascade = {PERSIST, REMOVE})
@JoinColumn(name = "team_id")
private List<Member2> member2s = new ArrayList<>();
테스트 코드
@DisplayName("클래스 레벨 @BatchSize로 N+1 해결")
@Test
void oneToMany_Lazy_N_1_BatchSize_ClassLevel() {
System.out.println("@@@ Batch Size @@@");
List<String> allMembersName = team2Service.findAllMembersName();
System.out.println("@@@ Batch Size @@@");
assertThat(allMembersName).hasSize(teamNumber * memberNumberPerTeam);
}
쿼리
@@@ Batch Size @@@
Hibernate:
select
team2x0_.team_id as team_id1_7_,
team2x0_.team_name as team_nam2_7_
from
team2 team2x0_
Hibernate:
select
member2s0_.team_id as team_id3_2_1_,
member2s0_.member_id as member_i1_2_1_,
member2s0_.member_id as member_i1_2_0_,
member2s0_.member_name as member_n2_2_0_
from
member2 member2s0_
where
member2s0_.team_id in (
?, ?, ?
)
@@@ Batch Size @@@
TeamService
public List<String> findALlMembersNameEntityGraph() {
List<Team2> team2s = team2Repository.findAllEntityGraph();
return team2s.stream()
.flatMap(team2 -> team2.getMembers().stream())
.map(Member2::getName)
.collect(Collectors.toList());
}
TeamRepository
@EntityGraph(attributePaths = "member2s")
@Query(value = "SELECT t FROM Team2 t")
List<Team2> findAllEntityGraph();
학습 테스트
@DisplayName("entityGraph로 N+1 해결")
@Test
void oneToMany_Lazy_N_1_EntityGraph() {
System.out.println("@@@ Entity Graph @@@");
List<String> allMembersName = team2Service.findALlMembersNameEntityGraph();
System.out.println("@@@ Entity Graph @@@");
assertThat(allMembersName).hasSize(teamNumber * memberNumberPerTeam);
}
쿼리
@@@ Entity Graph @@@
Hibernate:
select
team2x0_.team_id as team_id1_7_0_,
member2s1_.member_id as member_i1_2_1_,
team2x0_.team_name as team_nam2_7_0_,
member2s1_.member_name as member_n2_2_1_,
member2s1_.team_id as team_id3_2_0__,
member2s1_.member_id as member_i1_2_0__
from
team2 team2x0_
left outer join
member2 member2s1_
on team2x0_.team_id=member2s1_.team_id
@@@ Entity Graph @@@