๐ก ์๋ ์ค์ต์ ์งํํ ๋ชจ๋ ์ฝ๋๋ Github์ ์์ต๋๋ค.
JPA N+1์ด๋?
์ค๋ฌด์์ JPA๋ฅผ ์ฌ์ฉํ๋ค๋ณด๋ฉด, N+1 ์ฟผ๋ฆฌ๋ฅผ ๋ง๋๊ฒ ๋ฉ๋๋ค. ์ฌ๊ธฐ์ N+1์ด๋ Team(1) ↔ Member(N) ์ฐ๊ด๊ด๊ณ๊ฐ ์๋ค๊ณ ๊ฐ์ ํ์ ๋, ํ๋์ ํ์ ์กฐํํ์ง๋ง ํ ๋ด๋ถ์ ์๋ ๋ชจ๋ ๋ฉค๋ฒ๋ค์ด ํจ๊ป ์กฐํ๋๋ฉด์ 1+N ๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ์๋ฏธํฉ๋๋ค. ์ง์ ํ ์คํธ ์ฝ๋๋ฅผ ํตํด ์ด๋ฌํ ์ํฉ๋ค์ ํด๊ฒฐํ ์ ์๋ ๋ฐฉ๋ฒ๋ค์ ์ดํด๋ณด๊ณ ๊ฐ ์ํฉ์ด ๋ ์ด๋ค ์ฌ์ด๋์ดํํธ๊ฐ ์๋์ง๋ ์์๋ณด๊ฒ ์ต๋๋ค.
Entity ๋ฐ Repository ์ฝ๋
์ค์ต์ ์ฌ์ฉ๋ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
@Builder
private Member(String name) {
this.name = name;
}
public void updateTeam(Team team) {
this.team = team;
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Member> members = new ArrayList<>();
@Builder
private Team(String name) {
this.name = name;
}
public void addMember(Member member) {
this.members.add(member);
member.updateTeam(this);
}
}
public interface TeamRepository extends JpaRepository<Team, Long> {
Optional<Team> findTeamByName(String name);
}
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class Member_Lazy_Team_Lazy_Test {
@Autowired private MemberRepository memberRepository;
@Autowired private TeamRepository teamRepository;
@PersistenceContext private EntityManager em;
@BeforeEach
void setup() {
System.out.println("----------setup start-----------");
Team team1 = Team.builder().name("team1").build();
team1.addMember(Member.builder().name("member1-1").build());
team1.addMember(Member.builder().name("member1-2").build());
Team team2 = Team.builder().name("team2").build();
team2.addMember(Member.builder().name("member2-1").build());
team2.addMember(Member.builder().name("member2-2").build());
Team team3 = Team.builder().name("team3").build();
team3.addMember(Member.builder().name("member3-1").build());
team3.addMember(Member.builder().name("member3-2").build());
teamRepository.save(team1);
teamRepository.save(team2);
teamRepository.save(team3);
clearPersistenceContext();
System.out.println("----------setup end-----------");
}
@AfterEach
void clear() {
System.out.println("----------clear start-----------");
memberRepository.deleteAll();
teamRepository.deleteAll();
clearPersistenceContext();
System.out.println("----------clear end-----------");
}
private void clearPersistenceContext() {
em.flush();
em.clear();
}
@DisplayName("๋ชจ๋ ํ์ ์กฐํํ๊ณ , ์ง์ฐ๋ก๋ฉ ๋ ๋ฉค๋ฒ๋ฅผ ์ฌ์ฉํ ๋ -> N+1์ด ๋ฐ์ํ๋ค.(team1,2,3 ์กฐํํ๋ ์ฟผ๋ฆฌ 1๊ฐ, team1,2,3์ ๋ํ ๋ฉค๋ฒ ์กฐํํ๋ ์ฟผ๋ฆฌ 3๊ฐ)")
@Test
void team_findAll_test() {
clearPersistenceContext();
System.out.println("----------team_findAll_test start-----------");
List<Team> teamList = teamRepository.findAll();
assertThat(teamList).hasSize(3);
System.out.println("----------team_findAll_test mid-----------");
teamList.stream()
.map(Team::getMembers)
.map(List::stream)
.forEach(memberStream -> memberStream
.map(Member::getName)
.forEach(System.out::println)
);
System.out.println("----------team_findAll_test end-----------");
}
@DisplayName("๋ชจ๋ ๋ฉค๋ฒ๋ฅผ ์กฐํํ๊ณ , ์ง์ฐ๋ก๋ฉ ๋ ํ์ ์ฌ์ฉํ ๋ -> N+1์ด ๋ฐ์ํ๋ค.(๋ชจ๋ ๋ฉค๋ฒ ์กฐํ 1๊ฐ, ๊ฐ ํ์ ์กฐํํ๋ ์ฟผ๋ฆฌ 3๊ฐ)")
@Test
void member_findAll_test() {
clearPersistenceContext();
System.out.println("----------member_findAll_test start-----------");
List<Member> memberList = memberRepository.findAll();
assertThat(memberList).hasSize(6);
System.out.println("----------member_findAll_test mid-----------");
memberList.stream()
.map(Member::getTeam)
.map(Team::getName)
.forEach(System.out::println);
System.out.println("----------member_findAll_test end-----------");
}
}
์ ํ ์คํธ์์ ํ์ธํ๊ณ ์ ํ๋ ๊ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
1. ๋ชจ๋ ํ์ ์กฐํํ๊ณ , ๊ฐ ํ์ ์ํ ๋ฉค๋ฒ๋ค์ ์ด๋ฆ์ ์ถ๋ ฅํ์ ๋, ์ฟผ๋ฆฌ๊ฐ ์ด๋ป๊ฒ ๋๊ฐ๋ (team_findAll_test)ํ ์กฐํ ํ, ๊ฐ ํ์ ์ํ ๋ฉค๋ฒ๋ค์ ์ฌ์ฉํ ๋ ์ถ๊ฐ ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ๋ ๊ฒ์ ์ ์ ์์ต๋๋ค. (N+1) ์ด์ ์๋ ๊ฐ ๋ฉค๋ฒ์ ID๋ก ์กฐํํ๋ ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ์ 4๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ์๋ ๊ฐ ๋ฉค๋ฒ๋ฅผ ์กฐํํ๋ 7๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ์๋๋ฐ ์ธ์ ๋ถํด๊ฐ JPA ์ชฝ์์ ์ต์ ํ๋ฅผ ํ๋์ง team_id๋ฅผ ํตํด ์กฐํํ๋ ์ฟผ๋ฆฌ ๋๋ถ์ 4๊ฐ๋ก ์ค์์ต๋๋ค. (์ธ์ ์ต์ ํ๊ฐ ์ด๋ค์ก๋์ง ์์๋๋ถ ๊ผญ ์ข ์๋ ค์ฃผ์ธ์….)
----------team_findAll_test start-----------
Hibernate:
select
t1_0.id,
t1_0.name
from
team t1_0
----------team_findAll_test mid-----------
Hibernate:
select
m1_0.team_id,
m1_0.id,
m1_0.name
from
member m1_0
where
m1_0.team_id=?
member1-1
member1-2
Hibernate:
select
m1_0.team_id,
m1_0.id,
m1_0.name
from
member m1_0
where
m1_0.team_id=?
member2-1
member2-2
Hibernate:
select
m1_0.team_id,
m1_0.id,
m1_0.name
from
member m1_0
where
m1_0.team_id=?
member3-1
member3-2
----------team_findAll_test end-----------
2. ๋ชจ๋ ๋ฉค๋ฒ๋ฅผ ์กฐํํ๊ณ , ๊ฐ ๋ฉค๋ฒ๋ค์ ํ์ ์ด๋ฆ์ ์ถ๋ ฅํ์ ๋, ์ฟผ๋ฆฌ๊ฐ ์ด๋ป๊ฒ ๋๊ฐ๋์ง (member_findAll_test)Member์ Team์ LAZY ์ ๋ต์ผ๋ก ๋์ด ์๊ธฐ ๋๋ฌธ์ member๋ฅผ ์กฐํํ๋ ์์ ์๋ member ์กฐํ ์ฟผ๋ฆฌ๋ง ๋๊ฐ๊ณ , ์ค์ Team์ ์ฌ์ฉํ ๋ Team์ ์กฐํํ๋ ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ๋๋ค.
----------member_findAll_test start-----------
Hibernate:
select
m1_0.id,
m1_0.name,
m1_0.team_id
from
member m1_0
----------member_findAll_test mid-----------
Hibernate:
select
t1_0.id,
t1_0.name
from
team t1_0
where
t1_0.id=?
team1
team1
Hibernate:
select
t1_0.id,
t1_0.name
from
team t1_0
where
t1_0.id=?
team2
team2
Hibernate:
select
t1_0.id,
t1_0.name
from
team t1_0
where
t1_0.id=?
team3
team3
----------member_findAll_test end-----------
์ด๋ ๋ฏ ์ค๋ฌด์์ N+1 ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ฉด, ํ ์คํธ ์์ค์ฒ๋ผ ๋ช๊ฐ๊ฐ ์๋ ๋ช์ฒ๊ฐ์์ ๋ช๋ง๊ฐ๊ฐ ์กฐํ๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ณ ์ฅ์ ๋ก ์ด์ด์ง ๊ฐ๋ฅ์ฑ์ด ํฝ๋๋ค. ์ด๋ป๊ฒ ํด๊ฒฐํ ์ ์๋์ง ๋ฐ๋ก ์์๋ณด๊ฒ ์ต๋๋ค.
ํด๊ฒฐ๋ฐฉ๋ฒ
1. FetchJoin ์ฌ์ฉ
public interface TeamRepository extends JpaRepository<Team, Long> {
@Query(value = "select t from Team t join fetch t.members")
List<Team> findTeamsFetchJoin();
}
@DisplayName("๋ชจ๋ ํ์ ์กฐํํ๊ณ , ์ง์ฐ๋ก๋ฉ ๋ ๋ฉค๋ฒ๋ฅผ ์ฌ์ฉํ ๋ -> ๋จ 1๊ฐ์ ์ฟผ๋ฆฌ๋ง ๋๊ฐ๋ค")
@Test
void team_findAll_test() {
clearPersistenceContext();
System.out.println("----------team_findAll_test start-----------");
List<Team> teamList = teamRepository.findTeamsFetchJoin();
assertThat(teamList).hasSize(3);
System.out.println("----------team_findAll_test mid-----------");
teamList.stream()
.map(Team::getMembers)
.map(List::stream)
.forEach(memberStream -> memberStream
.map(Member::getName)
.forEach(System.out::println)
);
System.out.println("----------team_findAll_test end-----------");
}
ํ์ ์กฐํํ ๋, ํ์ ์ํ ๋ฉค๋ฒ๋ค๊น์ง ํ๋ฐฉ์ Fetch Join ํ๋ ๋ฐฉ๋ฒ์ ๋๋ค. ์ด๋ ๊ฒ ๋๋ฉด ์ฐ๊ด๋ ์ํฐํฐ ๋๋ ์ปฌ๋ ์ ์ ํ๋ก์ ๊ฐ์ฒด๊ฐ ์๋ ์ฆ์๋ก๋ฉ์ผ๋ก ๊ฐ์ ธ์ค๊ธฐ ๋๋ฌธ์ ์ฟผ๋ฆฌ๋ ํ๋๋ง ๋ฐ์ํ๊ฒ ๋ ๊ฒ ์ ๋๋ค.
----------team_findAll_test start-----------
Hibernate:
select
t1_0.id,
m1_0.team_id,
m1_0.id,
m1_0.name,
t1_0.name
from
team t1_0
join
member m1_0
on t1_0.id=m1_0.team_id
----------team_findAll_test mid-----------
member1-1
member1-2
member2-1
member2-2
member3-1
member3-2
----------team_findAll_test end-----------
์ด๋ฐ์์ผ๋ก Inner Join์ ํตํด์ ์ฐ๊ด๋ ๋ฉค๋ฒ๋ค์ ๋ชจ๋ ์กฐํํด์ 1์ฐจ ์บ์์ ๋ฃ์ด๋๊ธฐ ๋๋ฌธ์, ๋ฉค๋ฒ์ ์ด๋ฆ์ ์ฐ๋ ์๊ฐ์๋ ์ถ๊ฐ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ์ง ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
2. EntityGraph ์ฌ์ฉ
Fetch Join์ด ์ข์ง๋ง ๋ฌธ์์ด๋ก ํ๋ ์ฝ๋ฉ์ ํด์ผํ๋ค๋ ๋จ์ ์ด ์์ต๋๋ค. ์ด๋ด ๋๋ EntityGraph๋ฅผ ์ฌ์ฉํ ์๋ ์์ต๋๋ค.
public interface TeamRepository extends JpaRepository<Team, Long> {
@EntityGraph(attributePaths = {"members"})
@Query(value = "select t from Team t")
List<Team> findTeamsEntityGraph();
}
@DisplayName("๋ชจ๋ ํ์ ์กฐํํ๊ณ , ์ง์ฐ๋ก๋ฉ ๋ ๋ฉค๋ฒ๋ฅผ ์ฌ์ฉํ ๋ -> ๋จ 1๊ฐ์ ์ฟผ๋ฆฌ๋ง ๋๊ฐ๋ค")
@Test
void team_findAll_test() {
clearPersistenceContext();
System.out.println("----------team_findAll_test start-----------");
List<Team> teamList = teamRepository.findTeamsEntityGraph();
assertThat(teamList).hasSize(3);
System.out.println("----------team_findAll_test mid-----------");
teamList.stream()
.map(Team::getMembers)
.map(List::stream)
.forEach(memberStream -> memberStream
.map(Member::getName)
.forEach(System.out::println)
);
System.out.println("----------team_findAll_test end-----------");
}
์ํฐํฐ ๊ทธ๋ํ์ ์ฅ์ ์ ๊ธฐ์กด ์ฟผ๋ฆฌ๋ฅผ ํด์น์ง ์๊ณ , ๋ฐ๋ก @EntityGraph๋ฅผ ํตํด ํน์ ํ๋๋ฅผ EAGER ๋ก๋ฉํ ์ ์๋ค๋ ๊ฒ์ ๋๋ค. ๋ํ attributePaths๋ ์ฌ๋ฌ ๊ฐ๋ฅผ ์ ์ธํด์ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
----------team_findAll_test start-----------
Hibernate:
select
t1_0.id,
m1_0.team_id,
m1_0.id,
m1_0.name,
t1_0.name
from
team t1_0
left join
member m1_0
on t1_0.id=m1_0.team_id
----------team_findAll_test mid-----------
member1-1
member1-2
member2-1
member2-2
member3-1
member3-2
----------team_findAll_test end-----------
Fetch Join๊ณผ ๋์ผํ๊ฒ EAGER๋ก ๊ฐ์ ธ์ member๋ฅผ ์ฌ์ฉํ ๋ ์ถ๊ฐ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ์ง ์์์ต๋๋ค.
ํ์ง๋ง EntityGraph๋ Fetch Join๊ณผ ๋ค๋ฅธ์ ์ด ์์ต๋๋ค.
----------team_findAll_test start-----------
Hibernate:
select
t1_0.id,
m1_0.team_id,
m1_0.id,
m1_0.name,
t1_0.name
from
team t1_0
join
member m1_0
on t1_0.id=m1_0.team_id
----------team_findAll_test mid-----------
member1-1
member1-2
member2-1
member2-2
member3-1
member3-2
----------team_findAll_test end-----------
----------team_findAll_test start-----------
Hibernate:
select
t1_0.id,
m1_0.team_id,
m1_0.id,
m1_0.name,
t1_0.name
from
team t1_0
left join
member m1_0
on t1_0.id=m1_0.team_id
----------team_findAll_test mid-----------
member1-1
member1-2
member2-1
member2-2
member3-1
member3-2
----------team_findAll_test end-----------
EntityGraph๋ Joinํ ๋ Outer Left Join์ ํ๋ค๋ ์ ์ ๋๋ค. ์ด๋ ์นดํ ์์ ๊ณฑ(**Cartesian Product)**์ด ๋ฐ์ํ๋ค๋ ๋จ์ ์ด ์๊ธฐ ๋๋ฌธ์ ๋๋๋ก์ด๋ฉด Fetch Join์ ์ฌ์ฉํ๋ ๊ฒ์ ์ถ์ฒ๋๋ฆฝ๋๋ค.
๋ง์น๋ฉฐ
N+1 ํด๊ฒฐ์ ์ํด Fetch Join๊ณผ EntityGraph๋ฅผ ์ด์ฉํด ํด๊ฒฐํ๋๋ฐ์ ๊ณผ์ฐ ๋ ์ค ๋ญ ์จ์ผ ํ ๊น์? ์ ๋ ๊ฐ์ธ์ ์ผ๋ก Fetch Join์ ์ฌ์ฉํ๋ ๊ฒ์ ์ถ์ฒ๋๋ฆฝ๋๋ค. ๊ทธ ์ด์ ๋ EntityGraph๋ Outer Join์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ์ ๋ ์์ข๊ธฐ ๋๋ฌธ์ ๋๋ค. ๊ทธ๋ ๋ค๋ฉด Fetch Join์ ๋ง๋ฅ์ผ๊น์? ๋ค์ ํฌ์คํ ์๋ Fetch Join์ด ๊ณผ์ฐ ๋ง๋ฅ์ธ๊ฐ?์ ๋ํ ์ฃผ์ ๋ก ์ฐพ์๋ต๊ฒ ์ต๋๋ค. ๋ถ์กฑํ ๊ธ ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค!
REFERENCES
JPA N+1 ๋ฐ์์์ธ๊ณผ ํด๊ฒฐ๋ฐฉ๋ฒ - Yun Blog | ๊ธฐ์ ๋ธ๋ก๊ทธ
[JPA] N+1 ๋ฌธ์ ์์ธ ๋ฐ ํด๊ฒฐ๋ฐฉ๋ฒ
JPA ๋ชจ๋ N+1 ๋ฐ์ ์ผ์ด์ค๊ณผ ํด๊ฒฐ์ฑ
JPA N+1 ๋ฌธ์ ๋ฐ ํด๊ฒฐ๋ฐฉ์
JPA | JPA N+1 ๋ฌธ์ ๋ฐ ํด๊ฒฐ๋ฐฉ์ - ๊ฐ๋ฐ ๋ธ๋ก๊ทธ
JPA ์ฑ๋ฅ N+1 ๋ฌธ์ ์ ํด๊ฒฐ ๋ฐฉ๋ฒ
fetch ์กฐ์ธ, ์ํฐํฐ ๊ทธ๋ํ ์ง๋ฌธ์ ๋๋ค. - ์ธํ๋ฐ | ์ง๋ฌธ & ๋ต๋ณ
fetch join ๊ด๋ จ ์ง๋ฌธ ๋๋ฆฝ๋๋ค!! - ์ธํ๋ฐ | ์ง๋ฌธ & ๋ต๋ณ