๐ก Spring Data JPA๋ฅผ ์ฌ์ฉํ๋ค๋ณด๋ฉด ์ฐ๊ด๊ด๊ณ๋ฅผ ๊ฐ๊ณ ์๋ ๋ ์ํฐํฐ์ ๋ํด ์กฐํ๋ฅผ ํ ๋ N+1 ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค.
์ด์ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ N+1 ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ ์ํฉ์์ ์ง์ @Query์ Join query๋ฅผ ์์ฑํ๋ฉด์ N+1 ๋ฌธ์ ๋ฅผ ์ง์ ์ ์ผ๋ก ํ์ธํ๊ณ ์ฒด๊ฐํ์ง ๋ชปํ์๋๋ฐ์,
๋์ค์ N+1 ๋ฌธ์ ์ ํด๊ฒฐ๋ฐฉ๋ฒ์ ์ฐพ์๋ณด์์ ๋, @EntityGraph ๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ๊ณผ fetch join์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ๋ฑ์ด ์์์ต๋๋ค.
๊ทธ๋์ ๋จ์ํ ์ด๊ฑฐ ๊ทธ๋ฅ Join Query ์ฌ์ฉํ๋ฉด ๋๋๊ฑฐ ์๋์ผ? ๋ผ๊ณ ์๊ฐํ๊ณ ๋์ด๊ฐ์์ต๋๋ค ๐คญย ํ์ง๋ง ์ผ๋ฐ Join๊ณผ Fetch Join ๊ฐ์ ์ฐจ์ด์ ์ ๋ชจ๋ฅด๊ณ ์ฌ์ฉํ๋ค๋๊ฒ ๊ณ์ ๋ง์์ด ๊ฑธ๋ ธ์ต๋๋ค. ๐ฅฒย
๊ทธ๋์ ํ ์คํธ๋ฅผ ํตํด ๋ ๊ฐ์ ์ฐจ์ด์ ์ ์์๋ณด๊ณ ์ ํด๋น ํฌ์คํ ์ ์ค๋นํ๊ฒ ๋์์ต๋๋ค. ๐ฅ
User entity
๐ก ์ฌ์ฉ์(User)๋ name์ ๊ฐ๋๋ค.
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name = "name")
private String name;
}
Post entity
๐ก ๊ฒ์๋ฌผ(Post)๋ title, description์ ๊ฐ๊ณ , ์ฌ์ฉ์(User) ํ๋ช ์ด ์์ฑํ ์ ์๋ค.
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id", referencedColumnName = "id")
private User user;
@Column
private String title;
@Column
private String description;
}
@BeforeEach setUp Method
๐ก ์ฌ์ฉ์(user) - ๊ฒ์๋ฌผ(post) ๊ด๊ณ๋ฅผ 5๊ฐ ์์ฑํ๋ค.
@BeforeEach
voidsetUp() {
userRepository.deleteAll();
postRepository.deleteAll();
System.out.println("==== setUp start ====");
for(int i = 0; i < 5; i++) {
User user = User.builder()
.name("user" + i)
.build();
userRepository.save(user);
Post post = Post.builder()
.title("title" + i)
.description("description" + i)
.user(user)
.build();
postRepository.save(post);
}
System.out.println("==== setUp end ====");
}
๐ก ํ ์คํธ ์๊ตฌ์ฌํญ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
"๋ชจ๋ ๊ฒ์๋ฌผ์ ์กฐํํ๊ณ , ์กฐํํ ๊ฒ์๋ฌผ์ ์์ฑ์ ์ด๋ฆ์ ์ถ๋ ฅํ๋ค."
@Test
@DisplayName("N+1๋ฌธ์ ๋ฅผ ํ์ธํ๋ค.")
void NplusOneTest() {
List<Post> posts = postRepository.findAll();
posts.stream()
.map(post -> post.getUser().getName())
.forEach(System.out::println);
}
์คํ ๊ฒฐ๊ณผ
์กฐํํ๊ณ ์ ํ๋ ์ํฐํฐ(Post)์ ๊ฐฏ์(5)๋งํผ ๋งคํ๋ ์ํฐํฐ(User)์ ๋ํด ์กฐํ query๋ฅผ ๋ ๋ฆฝ๋๋ค.
โ N(User ์กฐํ) + 1(Post ์กฐํ)
โ 1๋ฒ ์กฐํํ ๋ ค๊ณ ํ๋๋ฐ N๋ฒ ์กฐํ๋ฅผ ๋ํ๊ฒ ๋๋ค.
โ ์ด๊ฒ ๋ฐ๋ก N+1 ๋ฌธ์
์ผ๋ฐ์ ์ผ๋ก SQL์ ๊ณต๋ถํ์๋ค๋ฉด ์ฐ๊ด ๊ด๊ณ๊ฐ ์๋ ํ ์ด๋ธ์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ ๋ Join ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ํ ๋ฒ์ ์กฐํ๋ฅผ ํ๋ฉด ๋๊ฒ ๋ค ๋ผ๋ ์๊ฐ์ ํ ์ ์์ต๋๋ค.
๊ทธ๋ฌ๋ฉด B(Fetch Join), C(์ผ๋ฐ Join) ํ ์คํธ๋ฅผ ํตํด N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
Fetch ์กฐ์ธ์ผ๋ก Post ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ์ฟผ๋ฆฌ
@Query("SELECT p FROM Post p "
+ "JOIN FETCH p.user")
List<Post> findAllByFetchJoin();
ํ ์คํธ ์ฝ๋
@Test
@DisplayName("Fetch Join ์ฟผ๋ฆฌ ์ฌ์ฉํ์ฌ N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ค.")
void fetchJoinTest() {
List<Post> posts = postRepository.findAllByFetchJoin();
posts.stream()
.map(post -> post.getUser().getName())
.forEach(System.out::println);
}
์คํ ๊ฒฐ๊ณผ
Hibernate:
select post0_.id as id1_0_0_, user1_.id as id1_1_1_, post0_.description as descript2_0_0_, post0_.title as title3_0_0_, post0_.user_id as user_id4_0_0_, user1_.name as name2_1_1_
from post post0_
inner join user user1_
on post0_.user_id=user1_.id
// ์ถ๋ ฅ ๊ฒฐ๊ณผ
user0
user1
user2
user3
user4
๊ฒฐ๊ณผ ๋ถ์
fetch join์ ์ฌ์ฉํ๋ ๊ฒ์๋ฌผ์ ์กฐํํ๋ ์ฟผ๋ฆฌ์์ ๋จ์ผ ๊ฒ์๋ฌผ์ด ๊ฐ๊ณ ์๋ ์์ฑ์(User)์ ๋ชจ๋ ๋ฐ์ดํฐ๊น์ง ํ๋์ ์ฟผ๋ฆฌ๋ฌธ์ผ๋ก ์กฐํํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
์ผ๋ฐ ์กฐ์ธ์ผ๋ก Post ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ์ฟผ๋ฆฌ
@Query("SELECT p FROM Post p "
+ "LEFT JOIN User u "
+ "ON p.user.id = u.id")
List<Post> findAllByJoin();
ํ ์คํธ ์ฝ๋
@Test
@DisplayName("์ผ๋ฐ Join ์ฟผ๋ฆฌ ์ฌ์ฉํ์ฌ N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ค.")
void joinTest() {
List<Post> posts = postRepository.findAllByJoin();
posts.stream()
.map(post -> post.getUser().getName())
.forEach(System.out::println);
}
์คํ ๊ฒฐ๊ณผ
Hibernate: select post0_.id as id1_0_, post0_.description as descript2_0_, post0_.title as title3_0_, post0_.user_id as user_id4_0_ from post post0_ left outer join user user1_ on (post0_.user_id=user1_.id)
Hibernate: select user0_.id as id1_1_0_, user0_.name as name2_1_0_ from user user0_ where user0_.id=?
Hibernate: select user0_.id as id1_1_0_, user0_.name as name2_1_0_ from user user0_ where user0_.id=?
Hibernate: select user0_.id as id1_1_0_, user0_.name as name2_1_0_ from user user0_ where user0_.id=?
Hibernate: select user0_.id as id1_1_0_, user0_.name as name2_1_0_ from user user0_ where user0_.id=?
Hibernate: select user0_.id as id1_1_0_, user0_.name as name2_1_0_ from user user0_ where user0_.id=?
// ์ถ๋ ฅ ๊ฒฐ๊ณผ
user0
user1
user2
user3
user4
๊ฒฐ๊ณผ ๋ถ์
์ผ๋ฐ join์ ์ฌ์ฉํ๋ ๊ฒ์๋ฌผ์ ์กฐํํ๋ ์ฟผ๋ฆฌ์์ ๋จ์ผ ๊ฒ์๋ฌผ์ด ๊ฐ๊ณ ์๋ ์์ฑ์(User)์ id(PK)๊ฐ๋ง ์กฐํํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๊ทธ ํ ์กฐํํ user_id๋ฅผ ํตํด N๋ฒ์ User ์กฐํ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฌ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
์ด๋ผ?!
๋ถ๋ช ์ด์ ํ๋ก์ ํธ์์ ์ผ๋ฐ join์ ์ฌ์ฉํ๋๋ผ๋ N+1 ๋ฌธ์ ๋ฅผ ํ์ธํ๋๋ฐ, ํ ์คํธ๋ฅผ ์งํํด๋ณด๋ N+1 ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค. ๐คฆ๐ป
์ ์์์ ์์! (ํ๋ก์ ํธ ์ฝ๋ ํ์ธํ๋ฌ ๊ฐ๋์ค,,,) ๐จ
์ด์ ํ๋ก์ ํธ์ ์ผ๋ฐ join ์ฟผ๋ฆฌ๋ฅผ ํ์ธํด๋ณด๋ ๋ ์ํฐํฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ๋ DTO ๊ฐ์ฒด๋ฅผ ๋ฐํ๊ฐ์ผ๋ก ์ค์ ํ ๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค.
๊ทธ๋ฌ๋ฉด D(์ผ๋ฐ Join with DTO) ํ ์คํธ๋ฅผ ํตํด N+1 ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋์๋์ง ํ์ธํด๋ณด๊ฒ ์ต๋๋ค.
PostUserDTO : user ์ด๋ฆ๊ณผ post ์ ๋ชฉ์ ๊ฐ๋๋ค.
public class PostUserDTO {
private String userName;
private String postTitle;
}
์ผ๋ฐ ์กฐ์ธ์ผ๋ก DTO ๊ฐ์ฒด๋ก ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ์ฟผ๋ฆฌ
@Query("SELECT new com.example.springbootstudy.domain.PostUserDTO(u.name, p.title) FROM Post p "
+ "LEFT JOIN User u "
+ "ON p.user.id = u.id")
List<PostUserDTO> findAllPostUserByFetchJoin();
ํ ์คํธ ์ฝ๋
@Test
@DisplayName("์ผ๋ฐ Join ์ฟผ๋ฆฌ ์ DTO ๋ฅผ ์ฌ์ฉํ์ฌ N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ค.")
void joinWithDTOTest() {
List<PostUserDTO> postUserDTOS = postRepository.findAllPostUserByFetchJoin();
postUserDTOS.stream()
.map(postUser -> postUser.getUserName())
.forEach(System.out::println);
}
์คํ ๊ฒฐ๊ณผ
Hibernate: select user1_.name as col_0_0_, post0_.title as col_1_0_ from post post0_ left outer join user user1_ on (post0_.user_id=user1_.id)
// ์ถ๋ ฅ ๊ฒฐ๊ณผ
user0
user1
user2
user3
user4
๊ฒฐ๊ณผ ๋ถ์
JPQL์์ ์กฐํ ์ฟผ๋ฆฌ๋ฌธ์์ DTO๋ฅผ ๋ฐํํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
ํด๋น ๊ธฐ๋ฅ์ ํ์ฉํ์ฌ PostUserDTO ๋ฅผ ํตํด ์กฐํ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ๋ฐ์ผ๋ N+1 ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์๋ ๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค.
์ ํ ์คํธ๋ฅผ ํตํด Fetch Join๊ณผ ์ผ๋ฐ Join์ ๊ฒฐ๊ณผ๋ก์จ ์ฐจ์ด์ ์ ํ์ธํด๋ณผ ์ ์์๋๋ฐ์,
์ฐ๋ฆฌ๊ฐ ํ์ธํ๋ ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
Fetch Join
์ผ๋ฐ Join
๊ทธ๋ฌ๋ฉด Fetch Join๊ณผ ์ผ๋ฐ Join์ ์ฐจ์ด์ ์ ๋ฌด์์ผ๊น์?
Fetch Join
์ผ๋ฐ Join
Fetch Join์ ์ฌ์ฉํ์ฌ N+1๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๊ฒ์ด ๋์ด์คํ ๋ฐฉ๋ฒ์ธ ๊ฒ์ ์๊ฒ๋ ๊ฒ ๊ฐ์ต๋๋ค.
ํ์ง๋ง ์ ํ ์คํธ์์ ํ์ธํ ์ ์๋ฏ์ด ์ฃผ์ด์ง ์๊ตฌ์ฌํญ์ด ๋จ์ํ ๋ฐ์ดํฐ ์กฐํ๋ง์ ์ํํ๋ ๊ฒ์ด๋ฉด D(์ผ๋ฐ Join with DTO) ์ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํ๋๋ผ๋ N+1 ๋ฌธ์ ๊ฑฑ์ ์์ด ์๊ตฌ์ฌํญ์ ํด๊ฒฐํ ์ ์์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด Fetch Join ๊ณผ ์ผ๋ฐ Join with DTO ๋ฐฉ๋ฒ์ ๊ฐ๊ฐ ์ธ์ ์ฌ์ฉํด์ผ ์ข์ ๊ฒ์ผ๊น์?
Fetch Join
Join with DTO
์ด๋ฒ ํฌ์คํ ์์ Fetch Join, ์ผ๋ฐ Join ๊ทธ๋ฆฌ๊ณ DTO๋ฅผ ํ์ฉํ ์ผ๋ฐ Join ์ํ ๋ฐฉ๋ฒ์ ๋ํ ์ฐจ์ด์ ์ ์ง์ด๋ณด์๋๋ฐ์, ์ง์ ํ ์คํธ๋ฅผ ํ ๊ฒฐ๊ณผ๋ฅผ ํตํด ์ฐจ์ด์ ๋ฐ ์ฌ์ฉ ์ํฉ์ ๋ํ ์๊ฐ์ ํ ์ ์์ด ์ข์๋ ๊ฒ ๊ฐ์ต๋๋ค. ํ๋ฆฐ ๋ด์ฉ์ด ์๊ฑฐ๋ ๊ฐ์ด ์๋ ผํ๊ณ ์ถ์ ๋ด์ฉ์ด ์์ผ๋ฉด ํผ๋๋ฐฑ ๋ถํ๋๋ฆฌ๊ฒ ์ต๋๋ค( ์ธ์ ๋ ํ์ ) ๐