[JPA] N+1 문제 (즉시 로딩 / 지연 로딩 / 일반 Join / Fetch Join)
즉시 로딩 / 지연 로딩 / 일반 Join / Fetch Join
1. select / EAGER (즉시 로딩)
📌 Member
package prac.littleprac.domain;
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
// == 연관관계 메서드 ==//
public void changeTeam(Team team) {
team.getMembers().add(this);
this.team = team;
}
}
📌 Team
package prac.littleprac.domain;
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@Builder
private Team(String name, List<Member> members) {
this.name = name;
this.members = members;
}
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
📌 Test 코드
@Test
void 즉시로딩() {
Team teamA = Team.builder()
.name("teamA")
.build();
em.persist(teamA);
Team teamB = Team.builder()
.name("teamB")
.build();
em.persist(teamB);
for(int i=0; i<3; i++) {
Member member = Member.builder()
.team(i%2==0? teamA : teamB)
.name("member" + i)
.build();
em.persist(member);
}
em.flush();
em.clear();
String basicQuery = "select m from Member m";
List<Member> result = em.createQuery(basicQuery, Member.class)
.getResultList();
}
select
member0_.id as id1_2_,
member0_.name as name2_2_,
member0_.team_id as team_id3_2_
from
member member0_
select
team0_.id as id1_4_0_,
team0_.name as name2_4_0_
from
team team0_
where
team0_.id=?
select
team0_.id as id1_4_0_,
team0_.name as name2_4_0_
from
team team0_
where
team0_.id=?
Member를 즉시로딩 설정시 Member 조회 쿼리 1개와 연관관계인 Team 조회 쿼리 2개가 발생했다. (N+1 문제 발생)
2. select / LAZY (지연 로딩)
📌 Member
package prac.littleprac.domain;
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
// == 연관관계 메서드 ==//
public void changeTeam(Team team) {
team.getMembers().add(this);
this.team = team;
}
}
📌 Team
package prac.littleprac.domain;
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@Builder
private Team(String name, List<Member> members) {
this.name = name;
this.members = members;
}
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
📌 Test 코드
@Test
void 지연로딩() {
Team teamA = Team.builder()
.name("teamA")
.build();
em.persist(teamA);
Team teamB = Team.builder()
.name("teamB")
.build();
em.persist(teamB);
for(int i=0; i<3; i++) {
Member member = Member.builder()
.team(i%2==0? teamA : teamB)
.name("member" + i)
.build();
em.persist(member);
}
em.flush();
em.clear();
String basicQuery = "select m from Member m";
List<Member> result = em.createQuery(basicQuery, Member.class)
.getResultList();
}
select
member0_.id as id1_2_,
member0_.name as name2_2_,
member0_.team_id as team_id3_2_
from
member member0_
지연 로딩 설정시 Member 조회시 Team 프록시 객체로 조회하기 때문에 Team 조회 쿼리가 따로 생성되지 않는다.
하지만 Member엔티티에 Team 엔티티를 조회할 떄 Team 조회 쿼리가 생성된다. 이 때 N+1 문제가 발생한다.
📌 Test 코드
@Test
void 지연로딩() {
Team teamA = Team.builder()
.name("teamA")
.build();
em.persist(teamA);
Team teamB = Team.builder()
.name("teamB")
.build();
em.persist(teamB);
for(int i=0; i<3; i++) {
Member member = Member.builder()
.team(i%2==0? teamA : teamB)
.name("member" + i)
.build();
em.persist(member);
}
em.flush();
em.clear();
String basicQuery = "select m from Member m";
List<Member> result = em.createQuery(basicQuery, Member.class)
.getResultList();
for (Member member : result) {
System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
}
}
select
member0_.id as id1_2_,
member0_.name as name2_2_,
member0_.team_id as team_id3_2_
from
member member0_
select
team0_.id as id1_4_0_,
team0_.name as name2_4_0_
from
team team0_
where
team0_.id=?
select
team0_.id as id1_4_0_,
team0_.name as name2_4_0_
from
team team0_
where
team0_.id=?
Member 엔티티에서 Team 엔티티를 조회할 때 Team 조회 쿼리가 생성된다. (N+1 문제 발생)
3. 일반 Join (지연 로딩)
📌 Member
package prac.littleprac.domain;
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
// == 연관관계 메서드 ==//
public void changeTeam(Team team) {
team.getMembers().add(this);
this.team = team;
}
}
📌 Team
package prac.littleprac.domain;
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@Builder
private Team(String name, List<Member> members) {
this.name = name;
this.members = members;
}
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
📌 테스트 코드
@Test
void 일반조인() {
Team teamA = Team.builder()
.name("teamA")
.build();
em.persist(teamA);
Team teamB = Team.builder()
.name("teamB")
.build();
em.persist(teamB);
for(int i=0; i<3; i++) {
Member member = Member.builder()
.team(i%2==0? teamA : teamB)
.name("member" + i)
.build();
em.persist(member);
}
em.flush();
em.clear();
String basicQuery = "select m from Member m join m.team t";
List<Member> result = em.createQuery(basicQuery, Member.class)
.getResultList();
}
select
member0_.id as id1_2_,
member0_.name as name2_2_,
member0_.team_id as team_id3_2_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.id
Member 엔티티만 호출시점에서는 Member 조회 쿼리만 생성된다.
📌 테스트 코드
@Test
void 일반조인() {
Team teamA = Team.builder()
.name("teamA")
.build();
em.persist(teamA);
Team teamB = Team.builder()
.name("teamB")
.build();
em.persist(teamB);
for(int i=0; i<3; i++) {
Member member = Member.builder()
.team(i%2==0? teamA : teamB)
.name("member" + i)
.build();
em.persist(member);
}
em.flush();
em.clear();
String basicQuery = "select m from Member m join m.team t";
List<Member> result = em.createQuery(basicQuery, Member.class)
.getResultList();
for (Member member : result) {
System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
}
}
select
member0_.id as id1_2_,
member0_.name as name2_2_,
member0_.team_id as team_id3_2_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.id
select
team0_.id as id1_4_0_,
team0_.name as name2_4_0_
from
team team0_
where
team0_.id=?
member.getTeam().getName() = teamA
select
team0_.id as id1_4_0_,
team0_.name as name2_4_0_
from
team team0_
where
team0_.id=?
member.getTeam().getName() = teamB
member.getTeam().getName() = teamA
Member 엔티티에서 Team 엔티티를 사용 시점에서 Team 조회 쿼리가 생성되어 N+1개 문제가 발생했다.
4. Fetch Join (즉시 로딩)
📌 Member
package prac.littleprac.domain;
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
// == 연관관계 메서드 ==//
public void changeTeam(Team team) {
team.getMembers().add(this);
this.team = team;
}
}
📌 Team
package prac.littleprac.domain;
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@Builder
private Team(String name, List<Member> members) {
this.name = name;
this.members = members;
}
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
📌 테스트 코드
@Test
void 페치조인() {
Team teamA = Team.builder()
.name("teamA")
.build();
em.persist(teamA);
Team teamB = Team.builder()
.name("teamB")
.build();
em.persist(teamB);
for(int i=0; i<3; i++) {
Member member = Member.builder()
.team(i%2==0? teamA : teamB)
.name("member" + i)
.build();
em.persist(member);
}
em.flush();
em.clear();
String basicQuery = "select m from Member m join fetch m.team t";
List<Member> result = em.createQuery(basicQuery, Member.class)
.getResultList();
for (Member member : result) {
System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
}
}
select
member0_.id as id1_2_0_,
team1_.id as id1_4_1_,
member0_.name as name2_2_0_,
member0_.team_id as team_id3_2_0_,
team1_.name as name2_4_1_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.id
member.getTeam().getName() = teamA
member.getTeam().getName() = teamB
member.getTeam().getName() = teamA
페치 조인은 연관관계를 즉시 로딩처럼 쿼리 한 번에 다 끌어오기 때문에 N+1 문제를 해결할 수 있다.
✔ 정리
👀 참고 자료
https://www.inflearn.com/questions/30446
제가 이해한 것이 정확한지 궁금합니다. - 인프런 | 질문 & 답변
1. 이렇게 표로 정리된 내용이 맞나요? [사진] - 질문 & 답변 | 인프런...
www.inflearn.com
JPA 모든 N+1 발생 케이스과 해결책
N+1이 발생하는 모든 케이스 (즉시로딩, 지연로딩)에서의 해결책과 그 해결책에서의 문제를 해결하는 방법에 대해 이야기 하려합니다 😀
velog.io