๐ง N + 1 ๋ฌธ์
N + 1 ๋ฌธ์ ๋ ์ฐ๊ด๊ด๊ณ๊ฐ ์ค์ ๋ ์ํฐํฐ ์ฌ์ด์์ ํ ์ํฐํฐ๋ฅผ ์กฐํํ์์ ๋,
์กฐํ๋ ์ํฐํฐ์ ๊ฐ์(N ๊ฐ)๋งํผ ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ์กฐํํ๊ธฐ ์ํด ์ถ๊ฐ์ ์ธ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ ๋ฌธ์ ๋ฅผ ์๋ฏธํฉ๋๋ค.
์ฆ N + 1์์, 1์ ํ ์ํฐํฐ๋ฅผ ์กฐํํ๊ธฐ ์ํ ์ฟผ๋ฆฌ์ ๊ฐ์์ด๋ฉฐ, N์ ์กฐํ๋ ์ํฐํฐ์ ๊ฐ์๋งํผ ์ฐ๊ด๋ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๊ธฐ ์ํ ์ถ๊ฐ์ ์ธ ์ฟผ๋ฆฌ์ ๊ฐ์๋ฅผ ์๋ฏธํฉ๋๋ค.
N + 1๋ณด๋ค๋, 1 + N์ด๋ผ ๋ถ๋ฅด๋ ๊ฒ์ด ๋ ์ดํดํ๊ธฐ ์ฌ์ฐ๋ฉฐ, ์ ๋ฆฌํ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์ํฐํฐ ์กฐํ ์ฟผ๋ฆฌ(1 ๋ฒ) + ์กฐํ๋ ์ํฐํฐ์ ๊ฐ์(N ๊ฐ)๋งํผ ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ์กฐํํ๊ธฐ ์ํ ์ถ๊ฐ ์ฟผ๋ฆฌ (N ๋ฒ)
๐ง ๋ฐ์ํ๋ ์ํฉ
N + 1 ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ์ํฉ์ ์์๋ณด๊ธฐ ์ํด ๊ฒ์๊ธ๊ณผ ๋๊ธ์ ์์๋ก ์ฌ์ฉํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
ํ๋์ ๊ฒ์๊ธ์๋ ์ฌ๋ฌ๊ฐ์ ๋๊ธ์ด ๋ฌ๋ฆด ์ ์์ผ๋ฉฐ, ์ด๋ฅผ ์ฝ๋๋ก ๋ํ๋ด๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค. (์์ฑ์์ Getter๋ ํ์ํ์ง ์๊ฒ ์ต๋๋ค.)
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
// ์์ฑ์ & Getter ์๋ต
public Comment writeComment(final String content) {
Comment comment = new Comment(content, this);
this.comments.add(comment);
return comment;
}
}
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
// ์์ฑ์ & Getter ์๋ต
}
์ด์ N + 1 ๋ฌธ์ ๋ฅผ ๋ฐ์์ํค๊ธฐ ์ํ ํ ์คํธ์ฝ๋๋ฅผ ์์ฑํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
@SpringBootTest
@Transactional
class PostRepositoryTest {
@Autowired
private EntityManager em;
@Autowired
private PostRepository postRepository;
@Test
@DisplayName("N + 1 ๋ฐ์ ํ
์คํธ")
void test() {
saveSampleData(); // 10๊ฐ์ post์, ๊ฐ๊ฐ์ post๋ง๋ค 3๊ฐ์ฉ ๋๊ธ ์ ์ฅ
em.flush();
em.clear();
System.out.println("------------ ์์์ฑ ์ปจํ
์คํธ ๋น์ฐ๊ธฐ -----------\n\n");
System.out.println("------------ POST ์ ์ฒด ์กฐํ ์์ฒญ ------------");
List<Post> posts = postRepository.findAll();
System.out.println("------------ POST ์ ์ฒด ์กฐํ ์๋ฃ. [1๋ฒ์ ์ฟผ๋ฆฌ ๋ฐ์]------------\n\n");
System.out.println("------------ POST ์ ๋ชฉ & ๋ด์ฉ ์กฐํ ์์ฒญ ------------");
posts.forEach(it -> System.out.println("POST ์ ๋ชฉ: [%s], POST ๋ด์ฉ: [%s]".formatted(it.title(), it.content())));
System.out.println("------------ POST ์ ๋ชฉ & ๋ด์ฉ ์กฐํ ์๋ฃ. [์ถ๊ฐ์ ์ธ ์ฟผ๋ฆฌ ๋ฐ์ํ์ง ์์]------------\n\n");
System.out.println("------------ POST์ ๋ฌ๋ฆฐ comment ๋ด์ฉ ์กฐํ ์์ฒญ [์กฐํ๋ POST์ ๊ฐ์(N=10) ๋งํผ ์ถ๊ฐ์ ์ธ ์ฟผ๋ฆฌ ๋ฐ์]------------");
posts.forEach(post -> {
post.comments().forEach(comment -> {
System.out.println("POST ์ ๋ชฉ: [%s], COMMENT ๋ด์ฉ: [%s]".formatted(comment.post().title(), comment.content()));
});
});
System.out.println("------------ POST์ ๋ฌ๋ฆฐ comment ๋ด์ฉ ์กฐํ ์๋ฃ ------------\n\n");
}
private void saveSampleData() {
final String postTitleFormat = "[%d] post-title";
final String postContentFormat = "[%d] post-content";
final String commentContentFormat = "[%d] comment-content";
IntStream.rangeClosed(1, 10).forEach(i -> {
Post post = new Post(format(postTitleFormat, i), format(postContentFormat, i));
IntStream.rangeClosed(1, 3).forEach(j -> {
post.writeComment(format(commentContentFormat, j));
});
postRepository.save(post);
});
}
}
๋ก๊ทธ๋ฅผ ํ์ธํด ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๋๋จธ์ง๋ ์๋ตํ๊ฒ ์ต๋๋ค.
์์ ๊ฐ์ด ์กฐํ๋ post์ ๊ฐ์์ธ 10๊ฐ๋งํผ์ ์ถ๊ฐ์ ์ธ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ ๊ฒ์ ์ ์ ์์ต๋๋ค.
์ด๋ฅผ ๋ฐ๋ก N + 1 ๋ฌธ์ ๋ผ ๋ถ๋ฆ ๋๋ค.
๐ง ์ง์ฐ ๋ก๋ฉ๊ณผ ์ฆ์ ๋ก๋ฉ
N + 1 ๋ฌธ์ ๋ ์ง์ฐ ๋ก๋ฉ(FetchType.LAZY)๊ณผ ์ฆ์ ๋ก๋ฉ(FetchType.EAGER) ๋ชจ๋ ๊ฒฝ์ฐ์ ๋ฐ์ํ ์ ์์ต๋๋ค.
์์ ์์ ๋ ์ฆ์ ๋ก๋ฉ์ผ๋ก ์ค์ ํ๋ค๋ฉด, ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ ์ํฉ๋ง ๋ฌ๋ผ์ง ๋ฟ, N + 1๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ๋์ผํฉ๋๋ค.
์ด๋ฅผ ํ์ธํ๊ธฐ ์ํด, Post์์ Comment๋ฅผ ์ฆ์ ๋ก๋ฉ์ผ๋ก ์กฐํํ๋๋ก ๋ณ๊ฒฝํ ๋ค, ์์ ํ ์คํธ์ฝ๋๋ฅผ ๊ทธ๋๋ก ์คํ์์ผ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.(์ถ๋ ฅ๋ฌธ๋ง ์ดํด๋ฅผ ๋๊ธฐ ์ํด ์์ ํ๋๋ก ํ๊ฒ ์ต๋๋ค.)
@Entity
public class Post {
// ํ๋ ์๋ต
@OneToMany(mappedBy = "post", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
// ์์ฑ์ & Getter ์๋ต & ๋ฉ์๋ ์๋ต
}
๋๋จธ์ง ์ถ๊ฐ์ ์ธ ์ฟผ๋ฆฌ๋ ์๋ตํ ๋ค, ๋ง์ง๋ง ์ฟผ๋ฆฌ์์๋ถํฐ ๋ค์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์ฆ ๋์ผํ ์ฝ๋์์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ ์์ ๋ง ๋ฌ๋ผ์ง ๋ฟ, ์ ์ฒด์ ์ธ ์ฟผ๋ฆฌ์ ์๋ ๋์ผํ๋ค๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
์ด์ ๋ถํฐ๋ ์์ ๊ฒฝ์ฐ ๋ง๊ณ ๋, N + 1 ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ ์ฌ๋ฌ ์ํฉ๊ณผ, ์ํฉ๋ณ ํด๊ฒฐ๋ฐฉ๋ฒ์ ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์ด๋ ๋ชจ๋ ๊ฒฝ์ฐ์ ์ง์ฐ ๋ก๋ฉ์ ์ฌ์ฉํฉ๋๋ค.
์ฐธ๊ณ ๋ก ์์ฑ์์ Getter ๋ฑ์ ์๋ตํ๊ณ ์์ฑํ ๊ฒ์ ๋๋ค.
๐ง @OneToOne ๊ด๊ณ์์ ๋ฐ์ํ๋ N + 1
fetch join, @EntityGraph๋ก ํด๊ฒฐ ๊ฐ๋ฅํฉ๋๋ค.
@OneToOne ์ฐ๊ด๊ด๊ณ์์๋ ์ฃผ์ํด์ผ ํ ์ ์ด ํ๋ ์๋๋ฐ, ์ฐ๊ด๊ด๊ณ์ ์ฃผ์ธ์ด ์๋ ์ํฐํฐ๋ฅผ ์กฐํํ๋ ๊ฒฝ์ฐ, ์ง์ฐ ๋ก๋ฉ์ผ๋ก ์ค์ ํ์๋๋ผ๋ ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ์ฆ์ ๋ก๋ฉ์ผ๋ก ์กฐํํฉ๋๋ค.
๊ด๋ จํด์๋ ํด๋น ๊ธ์ ์ฐธ๊ณ ํด์ฃผ์ธ์.
OneToOne ์ฐ๊ด๊ด๊ณ ์์๋ก Member์ Locker๋ฅผ ์ค์ ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
@Entity
public class Locker {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int number;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "memeber_id")
private Member member;
public void register(Member member) {
this.member = member;
member.setLocker(this);
}
}
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne(mappedBy = "member", fetch = FetchType.LAZY)
private Locker locker;
}
๐ N + 1 ๋ฐ์ํ๋ ์ํฉ
@SpringBootTest
@Transactional
class LockerRepositoryTest {
@Autowired
private EntityManager em;
@Autowired
private MemberRepository memberRepository;
@Autowired
private LockerRepository lockerRepository;
@BeforeEach
public void init() {
IntStream.rangeClosed(1, 3).forEach(i -> {
Member member = new Member("[%d] MEMBER".formatted(i));
Locker locker = new Locker(i);
locker.register(member);
memberRepository.save(member);
lockerRepository.save(locker);
});
em.flush();
em.clear();
}
@Test
@DisplayName("OneToOne ์ฃผ์ธ์ผ๋ก ์กฐํํ๊ธฐ")
void test1() {
System.out.println("LOCKER ์กฐํ");
List<Locker> lockers = lockerRepository.findAll(); // ์ฟผ๋ฆฌ 1๋ฒ - 3๊ฐ์ Locker ์กฐํ
System.out.println("LOCKER ์กฐํ ์๋ฃ\n\n");
lockers.forEach(it -> {
// ์ฐ๊ด๊ด๊ณ์ ์ฃผ์ธ์ด ์๋ ์ํฐํฐ๋ ๋ฌด์กฐ๊ฑด ์ฆ์ ๋ก๋ฉ๋๊ธฐ ๋๋ฌธ์ ์ฟผ๋ฆฌ๊ฐ ์ถ๊ฐ๋ก ๋ฐ์ํ๋ค.
// (1[ํ์ ์กฐํ] + 1[Locker ์กฐํ]) * 3[Locker ๊ฐ์] = ์ด 6๋ฒ์ ์ฟผ๋ฆฌ
System.out.println("[%d]๋ฒ LOCKER ์ฌ์ฉํ๋ ํ์ ์ด๋ฆ: [%s]\n".formatted(it.number(), it.member().name()));
});
}
@Test
@DisplayName("OneToOne ์ฃผ์ธ ์๋ ์ํฐํฐ๋ก ์กฐํํ๊ธฐ - ์ง์ฐ ๋ก๋ฉ์ผ๋ก ์ค์ ํ๋๋ผ๋ ์ฆ์ ๋ก๋ฉ๋๋ค.")
void test2() {
System.out.println("MEMBER ์กฐํ");
// 1[ํ์ ์ ์ฒด ์กฐํ] + 3[์กฐํ๋ ํ์์ ์๋งํผ ์ฆ์ ๋ก๋ฉ์ผ๋ก ์ธํ ์ฟผ๋ฆฌ๋ฐ์] = ์ด 4๋ฒ์ ์ฟผ๋ฆฌ
List<Member> members = memberRepository.findAll();
System.out.println("MEMBER ์กฐํ ์๋ฃ\n\n");
members.forEach(it -> {
System.out.println("[%d]๋ฒ LOCKER ์ฌ์ฉํ๋ ํ์ ์ด๋ฆ: [%s]\n".formatted(it.locker().number(), it.name()));
});
}
}
๋ฐ์ํ๋ ์ฟผ๋ฆฌ๋ ์ฃผ์์ ์ฐธ๊ณ ํด์ฃผ์ธ์.
์ด์ ๋ถํฐ ์ด๋ฅผ ํด๊ฒฐํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๐ fetch join ์ฌ์ฉ
๋ค์๊ณผ ๊ฐ์ด @Query๋ฅผ ํตํด fetch join์ ์ฌ์ฉํ์ฌ ํด๊ฒฐํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Override
@Query("select m from Member m join fetch m.locker")
List<Member> findAll();
}
public interface LockerRepository extends JpaRepository<Locker, Long> {
@Override
@Query("select l from Locker l join fetch l.member")
List<Locker> findAll();
}
ํ ์คํธ์ฝ๋๋ ๋์ผํ๊ฒ ์คํํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์์ ๊ฐ์ด N + 1 ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋์ด, ํ๋ฒ์ ์ฟผ๋ฆฌ๋ก ํด๊ฒฐ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๐ @EntityGraph ์ฌ์ฉ
@EntityGraph๋ fetch join์ ํธํ๊ฒ ์ฌ์ฉํ๋๋ก ๋์์ฃผ๋ ๊ธฐ๋ฅ์ ๋๋ค.
public interface LockerRepository extends JpaRepository<Locker, Long> {
//@Query("select l from Locker l join fetch l.member")
@Override
@EntityGraph(attributePaths = {"member"})
List<Locker> findAll();
}
public interface MemberRepository extends JpaRepository<Member, Long> {
//@Query("select m from Member m join fetch m.locker")
@Override
@EntityGraph(attributePaths = {"locker"})
List<Member> findAll();
}
์ํ๋๋ ์ฟผ๋ฆฌ๋ ์์ ๋์ผํ๋ฏ๋ก ์๋ตํ๋๋ก ํ๊ฒ ์ต๋๋ค.
๐ง @ManyToOne ๊ด๊ณ๋ก ์ฐ๊ด๋ ์ํฐํฐ๊ฐ ์กฐํ๋๋ ๊ฒฝ์ฐ
fetch join, @EntityGraph๋ก ํด๊ฒฐ ๊ฐ๋ฅํฉ๋๋ค.
์ฟผ๋ฆฌ๊ฐ ํ๋ฒ ๋ ๋ฐ์ํ์ง๋ง @BatchSize๋ก๋ ํด๊ฒฐ ๊ฐ๋ฅํฉ๋๋ค.
post์ comment๋ฅผ ์์๋ก ์ฌ์ฉํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค. ๋ง์ฐฌ๊ฐ์ง๋ก ์์ฑ์์ Getter๋ฑ์ ์๋ตํ์ฌ ํ์ํ๊ฒ ์ต๋๋ค.
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
public Comment writeComment(final String content) {
Comment comment = new Comment(content, this);
this.comments.add(comment);
return comment;
}
}
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
}
๐ N + 1 ๋ฐ์ํ๋ ์ํฉ
@SpringBootTest
@Transactional
class PostRepositoryTest {
@Autowired
private EntityManager em;
@Autowired
private CommentRepository commentRepository;
@Autowired
private PostRepository postRepository;
@Test
@DisplayName("ManyToOne N + 1 ๋ฐ์ ํ
์คํธ")
void test2() {
saveSampleData(); // 3๊ฐ์ post์, ๊ฐ๊ฐ์ post๋ง๋ค 2๊ฐ์ฉ ๋๊ธ ์ ์ฅ
em.flush();
em.clear();
System.out.println("------------ ์์์ฑ ์ปจํ
์คํธ ๋น์ฐ๊ธฐ -----------\n\n");
System.out.println("------------ COMMENT ์ ์ฒด ์กฐํ ์์ฒญ [1๋ฒ]------------");
List<Comment> comments = commentRepository.findAll();
System.out.println("------------ COMMENT ์ ์ฒด ์กฐํ ์๋ฃ ------------\n\n");
System.out.println("------------ COMMENT์ ์ฐ๊ด๋ POST ์กฐํ [ N + 1 ๋ฌธ์ ๋ฐ์ ] ------------");
comments.forEach(comment -> {
System.out.println("POST ์ ๋ชฉ: [%s], COMMENT ๋ด์ฉ: [%s]".formatted(comment.post().title(), comment.content()));
});
System.out.println("------------ COMMENT์ ์ฐ๊ด๋ POST ์กฐํ ์๋ฃ ------------\n\n");
}
private void saveSampleData() {
final String postTitleFormat = "[%d] post-title";
final String postContentFormat = "[%d] post-content";
final String commentContentFormat = "[%d] comment-content";
IntStream.rangeClosed(1, 3).forEach(i -> {
Post post = new Post(format(postTitleFormat, i), format(postContentFormat, i));
IntStream.rangeClosed(1, 2).forEach(j -> {
post.writeComment(format(commentContentFormat, j));
});
postRepository.save(post);
});
}
}
์ด์ ๋ถํฐ ์ด๋ฅผ ํด๊ฒฐํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๐ fetch join ์ฌ์ฉ
public interface CommentRepository extends JpaRepository<Comment, Long> {
@Override
@Query("select c from Comment c join fetch c.post")
List<Comment> findAll();
}
๐ @EntityGraph ์ฌ์ฉ
public interface CommentRepository extends JpaRepository<Comment, Long> {
//@Query("select c from Comment c join fetch c.post")
@Override
@EntityGraph(attributePaths = {"post"})
List<Comment> findAll();
}
๋ฐ์ํ๋ ์ฟผ๋ฆฌ๋ fetch join๊ณผ ๋์ผํฉ๋๋ค.
๐ @BatchSize
์ด๋ ์ฟผ๋ฆฌ 1๋ฒ์ผ๋ก ํด๊ฒฐ๋์ง ์๊ณ , 2๋ฒ์ผ๋ก ๋๋์ด ํด๊ฒฐํฉ๋๋ค.
@Entity
@BatchSize(size = 100)
public class Post {
// ์๋ต
}
์์ ๊ฐ์ด ์ค์ ํ๊ฑฐ๋, ํน์ application.yml ํ์ผ์์ ๋ค์ ์์ฑ์ ์ค์ ํ์ฌ ์ ์ฉํ ์ ์์ต๋๋ค.
spring.jpa.properties.hibernate.default_batch_fetch_size
batch size๋ฅผ ์ฌ์ฉํ๋ฉด IN ์ฟผ๋ฆฌ๋ฅผ ํตํด ํด๊ฒฐํ๋๋ฐ, ๋ฐ์ํ๋ ์ฟผ๋ฆฌ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์์ ๊ฐ์ด comment์ id๋ฅผ IN ์ ์ ๋ฃ์ด ํ๋ฒ์ ๊ฐ์ ธ์ค๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
์ด๋ IN ์ ์ ํ๋ฒ์ ๋ค์ด๊ฐ๋ ํฌ๊ธฐ๋ฅผ BatchSize์ ์ต์ ์ผ๋ก ์ค์ ํ ์ ์์ต๋๋ค.
๐ง @OneToMany ๊ด๊ณ๋ก ์ฐ๊ด๋ ์ํฐํฐ๊ฐ ์กฐํ๋๋ ๊ฒฝ์ฐ
fetch join, @EntityGraph๋ก ํด๊ฒฐ ๊ฐ๋ฅํฉ๋๋ค.
(๊ทธ๋ฌ๋ ํ์ด์ง์ ์งํํ ์ ์์ผ๋ฉฐ, ๋ ์ด์์ ์ปฌ๋ ์ ์ ํ์น ์กฐ์ธํ๋ ๋ฐ์ดํฐ๊ฐ ๋ถ์ ํฉํ๊ฒ ์กฐํ๋๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ์ฌ์ฉํ์ง ์๋ ๊ฒ์ด ์ข์ต๋๋ค.)
OneToMany ๊ด๊ณ์์๋ @BatchSize ํน์ @Fetch(FetchMode.SUBSELECT)๋ก ํด๊ฒฐํฉ๋๋ค.
Post์ Comment์ ์์์์ Post๋ฅผ ์กฐํํ๋ฉด์ Comment๋ฅผ ํจ๊ป ์กฐํํ๋ ๊ฒฝ์ฐ์ ๋๋ค.
๐ N + 1 ๋ฐ์ํ๋ ์ํฉ
@Test
@DisplayName("OneToMany N + 1 ๋ฐ์ ํ
์คํธ")
void test3() {
saveSampleData(); // 3๊ฐ์ post์, ๊ฐ๊ฐ์ post๋ง๋ค 2๊ฐ์ฉ ๋๊ธ ์ ์ฅ
em.flush();
em.clear();
System.out.println("------------ ์์์ฑ ์ปจํ
์คํธ ๋น์ฐ๊ธฐ -----------\n\n");
System.out.println("------------ POST ์ ์ฒด ์กฐํ ์์ฒญ [1๋ฒ]------------");
List<Post> posts = postRepository.findAll();
System.out.println("------------ POST ์ ์ฒด ์กฐํ ์๋ฃ ------------\n\n");
System.out.println("------------ POST์ ์ฐ๊ด๋ COMMENT ์กฐํ [ N + 1 ๋ฌธ์ ๋ฐ์ ] ------------");
posts.forEach(post -> {
post.comments().forEach(comment -> {
System.out.println("POST ์ ๋ชฉ: [%s], COMMENT ๋ด์ฉ: [%s]".formatted(post.title(), comment.content()));
});
});
System.out.println("------------ POST์ ์ฐ๊ด๋ COMMENT ์กฐํ ์๋ฃ ------------\n\n");
}
๋ฐ์ํ๋ ์ฟผ๋ฆฌ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๐ fetch join ์ฌ์ฉ - ๊ถ์ฅํ์ง ์์
public interface PostRepository extends JpaRepository<Post, Long> {
@Override
@Query("select p from Post p join fetch p.comments")
List<Post> findAll();
}
์ฐธ๊ณ ๋ก ์ด ๊ฒฝ์ฐ ์ผ๋๋ค ์กฐ์ธ์ ํ๊ธฐ ๋๋ฌธ์ ๊ฒฐ๊ณผ๊ฐ ๋์ด๋์ ์ค๋ณต๋ ๊ฒฐ๊ณผ๊ฐ ๋ํ๋ ์ ์์์ต๋๋ค๋ง,
ํ์ด๋ฒ๋ค์ดํธ 6 ์ดํ๋ถํฐ๋ ์๋์ผ๋ก distinct๋ฅผ ํด์ฃผ๊ธฐ ๋๋ฌธ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์์ต๋๋ค.
(์ฐธ๊ณ - https://github.com/hibernate/hibernate-orm/blob/6.0/migration-guide.adoc#distinct)
๐ @EntityGraph ์ฌ์ฉ - ๊ถ์ฅํ์ง ์์
public interface PostRepository extends JpaRepository<Post, Long> {
//@Query("select p from Post p join fetch p.comments")
@Override
@EntityGraph(attributePaths = {"comments"})
List<Post> findAll();
}
๋ฐ์ํ๋ ์ฟผ๋ฆฌ๋ ์์ ๋์ผํฉ๋๋ค.
๐ @BatchSize - ๊ถ์ฅ
spring.jpa.properties.hibernate.default_batch_fetch_size = 100
๋๋
@Entity
public class Post {
@BatchSize(size = 100)
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
}
๋ฐ์ํ๋ ์ฟผ๋ฆฌ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๐ @Fetch(FetchMode.SUBSELECT)
@Entity
public class Post {
@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
}
๋ฐ์ํ๋ ์ฟผ๋ฆฌ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์๋ธ์ฟผ๋ฆฌ๋ฅผ ํตํด N + 1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๐ง N + 1 ๊ธฐ๋ณธ ํด๊ฒฐ๋ฐฉ๋ฒ ์ ๋ฆฌ
@XToOne์ ๊ฒฝ์ฐ : fetch join์ ํตํด ํด๊ฒฐ (ํน์ @EntityGraph)
@XToMany์ ๊ฒฝ์ฐ : BatchSize๋ฅผ ์ฌ์ฉํ์ฌ ํด๊ฒฐ (ํน์ @Fetch(FetchMode.SUBSELECT))
๐ง ์ค์ฒฉ๋์ด ์กฐํ๋๋ ๊ฒฝ์ฐ
์์์ ์ ๋ฆฌํ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ํตํด ๋ณต์กํ๊ฒ ์ค์ฒฉ๋์ด ์๋ ์ํฉ์ ํด๊ฒฐํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
ํ์ + ๊ทธ๋ฃน -> ๊ฒ์๊ธ -> ๋๊ธ ์์ผ๋ก ํ๋ฒ์ ์กฐํ๋๋ ์ํฉ์ ์ฌํํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
@Entity
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Writer> writers = new ArrayList<>();
}
group์ ์์ฝ์ด์ด๋ฏ๋ก ํ ์ด๋ธ ๋ช ์ groups๋ก ์ง์์ต๋๋ค.
@Entity
public class Writer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
@OneToMany(mappedBy = "writer", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Post> posts = new ArrayList<>();
}
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "writer_id")
private Writer writer;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
public Comment writeComment(final String content) {
Comment comment = new Comment(content, this);
this.comments.add(comment);
return comment;
}
}
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
protected Comment() {}
public Comment(final String content, final Post post) {
this.content = content;
this.post = post;
}
}
๐ N + 1 ๋ฐ์ํ๋ ์ํฉ
2๊ฐ์ Group์ ๋ํ์ฌ, ๊ฐ ๊ทธ๋ฃน๋น 2๋ช ์ Writer์, Writer ๊ฐ๊ฐ Post๋ฅผ 2๊ฐ์ฉ ์ผ์ผ๋ฉฐ, Post๋น Comment๊ฐ 2๊ฐ์ฉ ๋ฌ๋ ค์๋ ์ํฉ์ ์๊ฐํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
@SpringBootTest
@Transactional
class WriterRepositoryTest {
@Autowired
private TeamRepository teamRepository;
@Autowired
private WriterRepository writerRepository;
@Autowired
private PostRepository postRepository;
@Autowired
private CommentRepository commentRepository;
@Autowired
private EntityManager em;
@Test
@DisplayName("")
void test() {
// given
saveData();
// when
System.out.println("--------------WRITER ์กฐํ--------------");
List<Writer> writers = writerRepository.findAll(); // 1๋ฒ ๋ฐ์ - 4๊ฐ ์กฐํ
System.out.println("--------------WRITER ์กฐํ ๋---------------\n\n");
for (Writer writer : writers) {
for (Post post : writer.posts()) { // writer๋น 1๋ฒ์ฉ, ์ด 4๋ฒ ๋ฐ์ - ๊ฐ ์ฟผ๋ฆฌ๋ง๋ค 2๊ฐ์ฉ post ์กฐํ
for (Comment comment : post.comments()) { // post 1๊ฐ๋น 1๋ฒ์ฉ ๋ฐ์ (์ด 4(ํ์ ์) * 2(ํ์๋ณ ํฌ์คํธ ์)๋ฒ = 8๋ฒ)
// ์ถ๊ฐ๋ก Writer๋น ํ ์กฐํ -> ํ์ ์ด 2๊ฐ์ด๋ฉฐ, ํ๋ฒ ์กฐํ๋ ๊ฒ์ ์์์ฑ ์ปจํ
์คํธ์ ๋จ์ผ๋ฏ๋ก ์ถ๊ฐ์ฟผ๋ฆฌ ๋ฐ์ X -> ์ด 2๋ฒ์ ์ถ๊ฐ์ฟผ๋ฆฌ ๋ฐ์
System.out.println("GROUP[%s] - WRITER[%s] - POST[%s] - COMMENT[%s]".formatted(writer.team().name(), writer.name(), post.title(), comment.content()));
}
}
}
}
private void saveData() {
for (int j = 0; j < 2; j++) {
Team team = new Team("[%d]TEAM".formatted(j));
teamRepository.save(team);
for (int i = 0; i < 2; i++) {
Writer writer = new Writer("[%d]writer".formatted(i), 10, team);
writerRepository.save(writer);
for (int i1 = 0; i1 < 2; i1++) {
Post post = new Post("[%d]title".formatted(i1), "content", writer);
postRepository.save(post);
for (int i2 = 0; i2 < 2; i2++) {
Comment comment = post.writeComment("[%d] comment".formatted(i2));
commentRepository.save(comment);
}
}
}
}
em.flush();
em.clear();
}
}
์ด ๊ฒฝ์ฐ ์ฟผ๋ฆฌ๋ ์ด 1 + 4 + 8 + 2 = 15๋ฒ ๋ฐ์ํฉ๋๋ค.
1๋ฒ์ ์ฟผ๋ฆฌ(์ต์ด Writer ์ ์ฒด ์กฐํ ์) - ์กฐํ๋๋ Writer๋ 4๋ช
4๋ฒ์ ์ฟผ๋ฆฌ(Writer๋น Post ์กฐํ ์) - ์กฐํ๋๋ Post๋ ์ฟผ๋ฆฌ๋น 2๊ฐ, ์ด post์ ๊ฐ์๋ 8๊ฐ
8๋ฒ์ ์ฟผ๋ฆฌ(4 * 2) - Post๋น ๋ฌ๋ ค์๋ Comment๋ค์ ์กฐํํ๊ธฐ ์ํ ์ฟผ๋ฆฌ 1๋ฒ์ฉ ๋ฐ์. ๋ฐ๋ผ์ ์กฐํ๋๋ ์ด post์ ๊ฐ์์ ๋์ผ
2๋ฒ์ ์ฟผ๋ฆฌ - Writer๊ฐ ์ํ ํ์ ์กฐํํ๊ธฐ ์ํจ. ํ๋ฒ ์กฐํ๋ Team์ ์์์ฑ ์ปจํ ์คํธ์ ์ ์ฅ๋์ด ๋ค์ ์กฐํํ๋ ๊ฒฝ์ฐ ์ถ๊ฐ์ ์ธ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ์ง ์์ผ๋ฏ๋ก, ์ด 2๋ฒ์ ์ฟผ๋ฆฌ๋ง ์ถ๊ฐ์ ์ผ๋ก ๋ฐ์ํจ.
๐ง N + 1 ํด๊ฒฐ
์ฐ์ @OneToMany ๊ด๊ณ์ ์ต์ ํ๋ฅผ ์ํด BatchSize๋ฅผ ์ ์ฉํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
์์ ๊ฐ์ด ์ด 4๋ฒ(Writer 1๋ฒ, Post 1๋ฒ, Comment 1๋ฒ, Team 1๋ฒ)์ ์ฟผ๋ฆฌ๋ก ์ค์ด๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
์ด์ @ManyToOne๊ด๊ณ, ์ฆ Writer์ Team์ ๋ํ ์ฟผ๋ฆฌ๋ฅผ ์ค์ด๊ธฐ ์ํด fetch join์ ์ฌ์ฉํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
public interface WriterRepository extends JpaRepository<Writer, Long> {
@Override
@Query("select w from Writer w join fetch w.team")
List<Writer> findAll();
}
๋ค์๊ณผ ๊ฐ์ด ์ฟผ๋ฆฌ 3๋ฒ์ผ๋ก ์ค์ด๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
๐ N + 1 ๋ฐ์ํ๋ ์ํฉ - 2
์ด๋ฒ์ ๋ฐ๋๋ก Comment๋ถํฐ ์์ํด์ Team๊น์ง ์กฐํํ๋ ์ํฉ์ ํ์ธํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
@Test
@DisplayName("")
void test2() {
// given
saveData();
// when
System.out.println("-------------- COMMENT ์กฐํ--------------");
List<Comment> comments = commentRepository.findAll();
//List<Writer> writers = writerRepository.findAll(); // 1๋ฒ ๋ฐ์ - 4๊ฐ ์กฐํ
System.out.println("-------------- COMMENT ์กฐํ ๋---------------\n\n");
//1 + 4 + 8 + 2
for (Comment comment : comments) {
System.out.println("GROUP[%s] - WRITER[%s] - POST[%s] - COMMENT[%s]"
.formatted(
comment.post().writer().team().name(),
comment.post().writer().name(),
comment.post().title(),
comment.content()));
}
}
์ด ๊ฒฝ์ฐ์๋ ์๋ฌด๋ฐ ์ต์ ํ๋ฅผ ํ์ง ์์ผ๋ฉด N + 1 ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ฌ 15๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํฉ๋๋ค.
์ด์ batchsize๋ฅผ ์ ์ฉํ๋ฉด ์๋์ ๊ฐ์ด 4๋ฒ์ผ๋ก ์ฟผ๋ฆฌ๊ฐ ์ค์ด๋ญ๋๋ค.
์ด๋ ManyToOne ๊ด๊ณ์ ๋ชจ๋ fetch join์ ์ ์ฉํ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
public interface CommentRepository extends JpaRepository<Comment, Long> {
@Override
@Query("select c from Comment c join fetch c.post p join fetch p.writer w join fetch w.team")
List<Comment> findAll();
}
BatchSize์ ์๊ด์์ด ์ฟผ๋ฆฌ ํ๋ฒ์ผ๋ก ๋ชจ๋ ์กฐํ๊ฐ ๋๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
์ด๋ ๋ฏ ์ต์ด ์กฐํ๋๋ ๋์์ ๋ฐ๋ผ์๋ ์ฟผ๋ฆฌ์ ์๊ฐ ๋ฌ๋ผ์ง๊ธฐ ๋๋ฌธ์, ์ํฉ์ ๋ฐ๋ผ ์ ์ ํ ์ต์ ํ๋ฅผ ์งํํด ์ฃผ์๋ฉด ๋ ๊ฒ ๊ฐ์ต๋๋ค.