Home JPA N+1 발생 원인 및 해결방법
Post
Cancel

JPA N+1 발생 원인 및 해결방법


JPA를 사용한다면 한번쯤은 마주쳤던, 또는 들어 봤던 문제입니다.
바로 N+1 문제인데요. 원인과 해결방법을 알아보겠습니다.

N + 1 문제란?


JPA에서 연관 관계가 설정된 엔티티 사이에서 한 엔티티를 조회하였을 때 조회된 엔티티의 개수(N개)만큼
연관된 엔티티를 조회하기 위해추가적인 쿼리가 발생하는 (+1) 문제를 의미합니다.



지연로딩 & 즉시로딩

JPA에서 연관 관계를 설정할 때 먼저 알아야하는 필수적인 개념이 있습니다.
그것은 바로 Fetch Type 입니다.

FetchType 이란?

  • JPA가 하나의 Entity를 조회할 때, 연관관계에 있는 객체들을 어떻게 가져올 것인지에 대한 설정값
  • default값 -> xxToOne = EAGER, xxToMany = LAZY

즉시로딩 (Eager)

  • 데이터를 조회할 때, 연관된 모든 객체의 데이터까지 한번에 불러오는 것

지연로딩 (Lazy)

  • 데이터를 조회할 때는 프록시를 통해 가짜 객체를 넣어놓고 실제 조회 시점에 데이터를 불러오는 것



발생하는 상황

N+1 문제를 재현하기 위해 아래와 같이 연관관계를 걸어주었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Entity
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "board_id")
    private Long id;

    private String title;

    private String content;

    private String writer;

    @OneToMany(mappedBy = "board", cascade = CascadeType.ALL)
    private List<Reply> replieList = new ArrayList<>();

}


@Entity
public class Reply {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "reply_id")
  private Long id;

  private String content;

  @JoinColumn(name = "board_id")
  @ManyToOne(fetch = FetchType.LAZY)
  private Board board;

}

아래는 N + 1 문제 발생을 위한 테스트 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
    @DisplayName("N+1 테스트")
    @Test
    void test() {
        saveData();
        em.flush();
        em.clear();

        System.out.println("---------- BOARD 데이터 조회 ----------");
        List<Board> boardList = boardRepository.findAll();
        System.out.println("---------- BOARD 데이터 조회 완료 (1번의 쿼리) ---------- \n");


        System.out.println("---------- BOARD 제목, 내용, 작성자 조회 ----------");
        boardList.forEach(System.out::println);
        System.out.println("---------- BOARD 제목, 내용, 작성자 조회 완료 ----------\n");

        System.out.println("---------- BOARD 내부 REPLY 내용 조회 ----------");
        boardList.forEach(board -> {
            board.getReplieList().forEach(System.out::println);
        });
        System.out.println("---------- BOARD 내부 REPLY 내용 조회 완료 (N+1 문제 발생) ----------");


    }

    private void saveData() {
        IntStream.rangeClosed(1, 10).forEach(i -> {

            Board board = Board.builder()
                    .title(i + " board-title")
                    .content(i + " board-content")
                    .writer(i + " board-writer")
                    .build();

            IntStream.rangeClosed(1, 3).forEach(j -> {
                board.writerReply(j + " reply-content");
            });

            boardRepository.save(board);
        });
    }

실행 결과는 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
---------- BOARD 데이터 조회 ----------
Hibernate: 
    select
        board0_.board_id as board_id1_0_,
        board0_.content as content2_0_,
        board0_.title as title3_0_,
        board0_.writer as writer4_0_ 
    from
        board board0_
---------- BOARD 데이터 조회 완료 (1번의 쿼리) ---------- 

---------- BOARD 제목, 내용, 작성자 조회 ----------
Board(id=172, title=1 board-title, content=1 board-content, writer=1 board-writer)
Board(id=173, title=2 board-title, content=2 board-content, writer=2 board-writer)
Board(id=174, title=3 board-title, content=3 board-content, writer=3 board-writer)
Board(id=175, title=4 board-title, content=4 board-content, writer=4 board-writer)
Board(id=176, title=5 board-title, content=5 board-content, writer=5 board-writer)
Board(id=177, title=6 board-title, content=6 board-content, writer=6 board-writer)
Board(id=178, title=7 board-title, content=7 board-content, writer=7 board-writer)
Board(id=179, title=8 board-title, content=8 board-content, writer=8 board-writer)
Board(id=180, title=9 board-title, content=9 board-content, writer=9 board-writer)
Board(id=181, title=10 board-title, content=10 board-content, writer=10 board-writer)
---------- BOARD 제목, 내용, 작성자 조회 완료 ----------

---------- BOARD 내부 REPLY 내용 조회 ----------
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Reply(id=496, content=1 reply-content, board=Board(id=172, title=1 board-title, content=1 board-content, writer=1 board-writer))
Reply(id=497, content=2 reply-content, board=Board(id=172, title=1 board-title, content=1 board-content, writer=1 board-writer))
Reply(id=498, content=3 reply-content, board=Board(id=172, title=1 board-title, content=1 board-content, writer=1 board-writer))
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Reply(id=499, content=1 reply-content, board=Board(id=173, title=2 board-title, content=2 board-content, writer=2 board-writer))
Reply(id=500, content=2 reply-content, board=Board(id=173, title=2 board-title, content=2 board-content, writer=2 board-writer))
Reply(id=501, content=3 reply-content, board=Board(id=173, title=2 board-title, content=2 board-content, writer=2 board-writer))
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Reply(id=502, content=1 reply-content, board=Board(id=174, title=3 board-title, content=3 board-content, writer=3 board-writer))
Reply(id=503, content=2 reply-content, board=Board(id=174, title=3 board-title, content=3 board-content, writer=3 board-writer))
Reply(id=504, content=3 reply-content, board=Board(id=174, title=3 board-title, content=3 board-content, writer=3 board-writer))
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Reply(id=505, content=1 reply-content, board=Board(id=175, title=4 board-title, content=4 board-content, writer=4 board-writer))
Reply(id=506, content=2 reply-content, board=Board(id=175, title=4 board-title, content=4 board-content, writer=4 board-writer))
Reply(id=507, content=3 reply-content, board=Board(id=175, title=4 board-title, content=4 board-content, writer=4 board-writer))
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Reply(id=508, content=1 reply-content, board=Board(id=176, title=5 board-title, content=5 board-content, writer=5 board-writer))
Reply(id=509, content=2 reply-content, board=Board(id=176, title=5 board-title, content=5 board-content, writer=5 board-writer))
Reply(id=510, content=3 reply-content, board=Board(id=176, title=5 board-title, content=5 board-content, writer=5 board-writer))
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Reply(id=511, content=1 reply-content, board=Board(id=177, title=6 board-title, content=6 board-content, writer=6 board-writer))
Reply(id=512, content=2 reply-content, board=Board(id=177, title=6 board-title, content=6 board-content, writer=6 board-writer))
Reply(id=513, content=3 reply-content, board=Board(id=177, title=6 board-title, content=6 board-content, writer=6 board-writer))
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Reply(id=514, content=1 reply-content, board=Board(id=178, title=7 board-title, content=7 board-content, writer=7 board-writer))
Reply(id=515, content=2 reply-content, board=Board(id=178, title=7 board-title, content=7 board-content, writer=7 board-writer))
Reply(id=516, content=3 reply-content, board=Board(id=178, title=7 board-title, content=7 board-content, writer=7 board-writer))
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Reply(id=517, content=1 reply-content, board=Board(id=179, title=8 board-title, content=8 board-content, writer=8 board-writer))
Reply(id=518, content=2 reply-content, board=Board(id=179, title=8 board-title, content=8 board-content, writer=8 board-writer))
Reply(id=519, content=3 reply-content, board=Board(id=179, title=8 board-title, content=8 board-content, writer=8 board-writer))
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Reply(id=520, content=1 reply-content, board=Board(id=180, title=9 board-title, content=9 board-content, writer=9 board-writer))
Reply(id=521, content=2 reply-content, board=Board(id=180, title=9 board-title, content=9 board-content, writer=9 board-writer))
Reply(id=522, content=3 reply-content, board=Board(id=180, title=9 board-title, content=9 board-content, writer=9 board-writer))
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Reply(id=523, content=1 reply-content, board=Board(id=181, title=10 board-title, content=10 board-content, writer=10 board-writer))
Reply(id=524, content=2 reply-content, board=Board(id=181, title=10 board-title, content=10 board-content, writer=10 board-writer))
Reply(id=525, content=3 reply-content, board=Board(id=181, title=10 board-title, content=10 board-content, writer=10 board-writer))
---------- BOARD 내부 REPLY 내용 조회 완료 (N+1 문제 발생) ----------

로그를 확인하면 Board 개수인 10개만큼의 추가적인 쿼리가 발생한걸 확인할 수 있습니다.
이 문제를 N + 1이라고 부릅니다.

그럼 이 문제는 지연로딩 에서만 발생할까요?
아닙니다. 즉시로딩 으로 변경하여 테스트 해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
---------- BOARD 데이터 조회 ----------
Hibernate: 
    select
        board0_.board_id as board_id1_0_,
        board0_.content as content2_0_,
        board0_.title as title3_0_,
        board0_.writer as writer4_0_ 
    from
        board board0_
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.content as content2_1_1_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id=?
---------- BOARD 데이터 조회 완료 (1번의 쿼리) ---------- 

---------- BOARD 제목, 내용, 작성자 조회 ----------
Board(id=162, title=1 board-title, content=1 board-content, writer=1 board-writer)
Board(id=163, title=2 board-title, content=2 board-content, writer=2 board-writer)
Board(id=164, title=3 board-title, content=3 board-content, writer=3 board-writer)
Board(id=165, title=4 board-title, content=4 board-content, writer=4 board-writer)
Board(id=166, title=5 board-title, content=5 board-content, writer=5 board-writer)
Board(id=167, title=6 board-title, content=6 board-content, writer=6 board-writer)
Board(id=168, title=7 board-title, content=7 board-content, writer=7 board-writer)
Board(id=169, title=8 board-title, content=8 board-content, writer=8 board-writer)
Board(id=170, title=9 board-title, content=9 board-content, writer=9 board-writer)
Board(id=171, title=10 board-title, content=10 board-content, writer=10 board-writer)
---------- BOARD 제목, 내용, 작성자 조회 완료 ----------

---------- BOARD 내부 REPLY 내용 조회 ----------
Reply(id=466, content=1 reply-content, board=Board(id=162, title=1 board-title, content=1 board-content, writer=1 board-writer))
Reply(id=467, content=2 reply-content, board=Board(id=162, title=1 board-title, content=1 board-content, writer=1 board-writer))
Reply(id=468, content=3 reply-content, board=Board(id=162, title=1 board-title, content=1 board-content, writer=1 board-writer))
Reply(id=469, content=1 reply-content, board=Board(id=163, title=2 board-title, content=2 board-content, writer=2 board-writer))
Reply(id=470, content=2 reply-content, board=Board(id=163, title=2 board-title, content=2 board-content, writer=2 board-writer))
Reply(id=471, content=3 reply-content, board=Board(id=163, title=2 board-title, content=2 board-content, writer=2 board-writer))
Reply(id=472, content=1 reply-content, board=Board(id=164, title=3 board-title, content=3 board-content, writer=3 board-writer))
Reply(id=473, content=2 reply-content, board=Board(id=164, title=3 board-title, content=3 board-content, writer=3 board-writer))
Reply(id=474, content=3 reply-content, board=Board(id=164, title=3 board-title, content=3 board-content, writer=3 board-writer))
Reply(id=475, content=1 reply-content, board=Board(id=165, title=4 board-title, content=4 board-content, writer=4 board-writer))
Reply(id=476, content=2 reply-content, board=Board(id=165, title=4 board-title, content=4 board-content, writer=4 board-writer))
Reply(id=477, content=3 reply-content, board=Board(id=165, title=4 board-title, content=4 board-content, writer=4 board-writer))
Reply(id=478, content=1 reply-content, board=Board(id=166, title=5 board-title, content=5 board-content, writer=5 board-writer))
Reply(id=479, content=2 reply-content, board=Board(id=166, title=5 board-title, content=5 board-content, writer=5 board-writer))
Reply(id=480, content=3 reply-content, board=Board(id=166, title=5 board-title, content=5 board-content, writer=5 board-writer))
Reply(id=481, content=1 reply-content, board=Board(id=167, title=6 board-title, content=6 board-content, writer=6 board-writer))
Reply(id=482, content=2 reply-content, board=Board(id=167, title=6 board-title, content=6 board-content, writer=6 board-writer))
Reply(id=483, content=3 reply-content, board=Board(id=167, title=6 board-title, content=6 board-content, writer=6 board-writer))
Reply(id=484, content=1 reply-content, board=Board(id=168, title=7 board-title, content=7 board-content, writer=7 board-writer))
Reply(id=485, content=2 reply-content, board=Board(id=168, title=7 board-title, content=7 board-content, writer=7 board-writer))
Reply(id=486, content=3 reply-content, board=Board(id=168, title=7 board-title, content=7 board-content, writer=7 board-writer))
Reply(id=487, content=1 reply-content, board=Board(id=169, title=8 board-title, content=8 board-content, writer=8 board-writer))
Reply(id=488, content=2 reply-content, board=Board(id=169, title=8 board-title, content=8 board-content, writer=8 board-writer))
Reply(id=489, content=3 reply-content, board=Board(id=169, title=8 board-title, content=8 board-content, writer=8 board-writer))
Reply(id=490, content=1 reply-content, board=Board(id=170, title=9 board-title, content=9 board-content, writer=9 board-writer))
Reply(id=491, content=2 reply-content, board=Board(id=170, title=9 board-title, content=9 board-content, writer=9 board-writer))
Reply(id=492, content=3 reply-content, board=Board(id=170, title=9 board-title, content=9 board-content, writer=9 board-writer))
Reply(id=493, content=1 reply-content, board=Board(id=171, title=10 board-title, content=10 board-content, writer=10 board-writer))
Reply(id=494, content=2 reply-content, board=Board(id=171, title=10 board-title, content=10 board-content, writer=10 board-writer))
Reply(id=495, content=3 reply-content, board=Board(id=171, title=10 board-title, content=10 board-content, writer=10 board-writer))
---------- BOARD 내부 REPLY 내용 조회 완료 (N+1 문제 발생) ----------

N+1 쿼리가 나가는 시점만 달라졌을뿐 N+1문제가 발생하는걸 확인할 수 있습니다.



해결 방법

fetch join 사용

  • 조회의 주체가 되는 Entity 이외에 Fetch Join이 걸린 연관 Entity도 함께 SELECT 하여 모두 영속화
  • Fetch Join이 걸린 Entity 모두 영속화하기 때문에 FetchType이 Lazy인 Entity를 참조하더라도
    이미 영속성 컨텍스트에 들어있기 때문에 따로 쿼리가 실행되지 않은 채로 N+1문제가 해결됨
1
2
3
    @Override
    @Query("SELECT distinct b FROM Board b JOIN FETCH b.replieList")
    List<Board> findAll();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
---------- BOARD 데이터 조회 ----------
Hibernate: 
    select
        distinct board0_.board_id as board_id1_0_0_,
        replielist1_.reply_id as reply_id1_1_1_,
        board0_.content as content2_0_0_,
        board0_.title as title3_0_0_,
        board0_.writer as writer4_0_0_,
        replielist1_.board_id as board_id3_1_1_,
        replielist1_.content as content2_1_1_,
        replielist1_.board_id as board_id3_1_0__,
        replielist1_.reply_id as reply_id1_1_0__ 
    from
        board board0_ 
    inner join
        reply replielist1_ 
            on board0_.board_id=replielist1_.board_id
---------- BOARD 데이터 조회 완료 (1번의 쿼리) ---------- 

---------- BOARD 제목, 내용, 작성자 조회 ----------
Board(id=212, title=1 board-title, content=1 board-content, writer=1 board-writer)
Board(id=213, title=2 board-title, content=2 board-content, writer=2 board-writer)
Board(id=214, title=3 board-title, content=3 board-content, writer=3 board-writer)
Board(id=215, title=4 board-title, content=4 board-content, writer=4 board-writer)
Board(id=216, title=5 board-title, content=5 board-content, writer=5 board-writer)
Board(id=217, title=6 board-title, content=6 board-content, writer=6 board-writer)
Board(id=218, title=7 board-title, content=7 board-content, writer=7 board-writer)
Board(id=219, title=8 board-title, content=8 board-content, writer=8 board-writer)
Board(id=220, title=9 board-title, content=9 board-content, writer=9 board-writer)
Board(id=221, title=10 board-title, content=10 board-content, writer=10 board-writer)
---------- BOARD 제목, 내용, 작성자 조회 완료 ----------

---------- BOARD 내부 REPLY 내용 조회 ----------
Reply(id=616, content=1 reply-content, board=Board(id=212, title=1 board-title, content=1 board-content, writer=1 board-writer))
Reply(id=617, content=2 reply-content, board=Board(id=212, title=1 board-title, content=1 board-content, writer=1 board-writer))
Reply(id=618, content=3 reply-content, board=Board(id=212, title=1 board-title, content=1 board-content, writer=1 board-writer))
Reply(id=619, content=1 reply-content, board=Board(id=213, title=2 board-title, content=2 board-content, writer=2 board-writer))
Reply(id=620, content=2 reply-content, board=Board(id=213, title=2 board-title, content=2 board-content, writer=2 board-writer))
Reply(id=621, content=3 reply-content, board=Board(id=213, title=2 board-title, content=2 board-content, writer=2 board-writer))
Reply(id=622, content=1 reply-content, board=Board(id=214, title=3 board-title, content=3 board-content, writer=3 board-writer))
Reply(id=623, content=2 reply-content, board=Board(id=214, title=3 board-title, content=3 board-content, writer=3 board-writer))
Reply(id=624, content=3 reply-content, board=Board(id=214, title=3 board-title, content=3 board-content, writer=3 board-writer))
Reply(id=625, content=1 reply-content, board=Board(id=215, title=4 board-title, content=4 board-content, writer=4 board-writer))
Reply(id=626, content=2 reply-content, board=Board(id=215, title=4 board-title, content=4 board-content, writer=4 board-writer))
Reply(id=627, content=3 reply-content, board=Board(id=215, title=4 board-title, content=4 board-content, writer=4 board-writer))
Reply(id=628, content=1 reply-content, board=Board(id=216, title=5 board-title, content=5 board-content, writer=5 board-writer))
Reply(id=629, content=2 reply-content, board=Board(id=216, title=5 board-title, content=5 board-content, writer=5 board-writer))
Reply(id=630, content=3 reply-content, board=Board(id=216, title=5 board-title, content=5 board-content, writer=5 board-writer))
Reply(id=631, content=1 reply-content, board=Board(id=217, title=6 board-title, content=6 board-content, writer=6 board-writer))
Reply(id=632, content=2 reply-content, board=Board(id=217, title=6 board-title, content=6 board-content, writer=6 board-writer))
Reply(id=633, content=3 reply-content, board=Board(id=217, title=6 board-title, content=6 board-content, writer=6 board-writer))
Reply(id=634, content=1 reply-content, board=Board(id=218, title=7 board-title, content=7 board-content, writer=7 board-writer))
Reply(id=635, content=2 reply-content, board=Board(id=218, title=7 board-title, content=7 board-content, writer=7 board-writer))
Reply(id=636, content=3 reply-content, board=Board(id=218, title=7 board-title, content=7 board-content, writer=7 board-writer))
Reply(id=637, content=1 reply-content, board=Board(id=219, title=8 board-title, content=8 board-content, writer=8 board-writer))
Reply(id=638, content=2 reply-content, board=Board(id=219, title=8 board-title, content=8 board-content, writer=8 board-writer))
Reply(id=639, content=3 reply-content, board=Board(id=219, title=8 board-title, content=8 board-content, writer=8 board-writer))
Reply(id=640, content=1 reply-content, board=Board(id=220, title=9 board-title, content=9 board-content, writer=9 board-writer))
Reply(id=641, content=2 reply-content, board=Board(id=220, title=9 board-title, content=9 board-content, writer=9 board-writer))
Reply(id=642, content=3 reply-content, board=Board(id=220, title=9 board-title, content=9 board-content, writer=9 board-writer))
Reply(id=643, content=1 reply-content, board=Board(id=221, title=10 board-title, content=10 board-content, writer=10 board-writer))
Reply(id=644, content=2 reply-content, board=Board(id=221, title=10 board-title, content=10 board-content, writer=10 board-writer))
Reply(id=645, content=3 reply-content, board=Board(id=221, title=10 board-title, content=10 board-content, writer=10 board-writer))
---------- BOARD 내부 REPLY 내용 조회 완료 (N+1 문제 발생) ----------

데이터를 한번에 가져와 영속화 시켜 N+1 문제가 발생되지 않습니다.

@EntityGraph 사용

  • 단순하게 fetch 조인을 어노테이션으로 사용할 수 있도록 만들어준 기능이다.
1
2
3
    @Override
    @EntityGraph(attributePaths = "replieList")
    List<Board> findAll();

결과 및 발생 쿼리는 fetch join과 동일합니다.

BatchSize

  • IN 쿼리를 통해 지정한 사이즈만큼 한번에 가져오는 것
1
2
3
@BatchSize(size = 100)
@OneToMany(mappedBy = "board", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Reply> replieList = new ArrayList<>();

위와 같이 Entity에 직접 설정 또는 yml 파일에 속성 지정 가능
spring.jpa.properties.hibernate.default_batch_fetch_size

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
---------- BOARD 데이터 조회 ----------
Hibernate: 
    select
        board0_.board_id as board_id1_0_,
        board0_.content as content2_0_,
        board0_.title as title3_0_,
        board0_.writer as writer4_0_ 
    from
        board board0_
---------- BOARD 데이터 조회 완료 (1번의 쿼리) ---------- 

---------- BOARD 제목, 내용, 작성자 조회 ----------
Board(id=252, title=1 board-title, content=1 board-content, writer=1 board-writer)
Board(id=253, title=2 board-title, content=2 board-content, writer=2 board-writer)
Board(id=254, title=3 board-title, content=3 board-content, writer=3 board-writer)
Board(id=255, title=4 board-title, content=4 board-content, writer=4 board-writer)
Board(id=256, title=5 board-title, content=5 board-content, writer=5 board-writer)
Board(id=257, title=6 board-title, content=6 board-content, writer=6 board-writer)
Board(id=258, title=7 board-title, content=7 board-content, writer=7 board-writer)
Board(id=259, title=8 board-title, content=8 board-content, writer=8 board-writer)
Board(id=260, title=9 board-title, content=9 board-content, writer=9 board-writer)
Board(id=261, title=10 board-title, content=10 board-content, writer=10 board-writer)
---------- BOARD 제목, 내용, 작성자 조회 완료 ----------

---------- BOARD 내부 REPLY 내용 조회 ----------
Hibernate: 
    select
        replielist0_.board_id as board_id3_1_1_,
        replielist0_.reply_id as reply_id1_1_1_,
        replielist0_.reply_id as reply_id1_1_0_,
        replielist0_.board_id as board_id3_1_0_,
        replielist0_.content as content2_1_0_ 
    from
        reply replielist0_ 
    where
        replielist0_.board_id in (
            ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
        )
Reply(id=736, content=1 reply-content, board=Board(id=252, title=1 board-title, content=1 board-content, writer=1 board-writer))
Reply(id=737, content=2 reply-content, board=Board(id=252, title=1 board-title, content=1 board-content, writer=1 board-writer))
Reply(id=738, content=3 reply-content, board=Board(id=252, title=1 board-title, content=1 board-content, writer=1 board-writer))
Reply(id=739, content=1 reply-content, board=Board(id=253, title=2 board-title, content=2 board-content, writer=2 board-writer))
Reply(id=740, content=2 reply-content, board=Board(id=253, title=2 board-title, content=2 board-content, writer=2 board-writer))
Reply(id=741, content=3 reply-content, board=Board(id=253, title=2 board-title, content=2 board-content, writer=2 board-writer))
Reply(id=742, content=1 reply-content, board=Board(id=254, title=3 board-title, content=3 board-content, writer=3 board-writer))
Reply(id=743, content=2 reply-content, board=Board(id=254, title=3 board-title, content=3 board-content, writer=3 board-writer))
Reply(id=744, content=3 reply-content, board=Board(id=254, title=3 board-title, content=3 board-content, writer=3 board-writer))
Reply(id=745, content=1 reply-content, board=Board(id=255, title=4 board-title, content=4 board-content, writer=4 board-writer))
Reply(id=746, content=2 reply-content, board=Board(id=255, title=4 board-title, content=4 board-content, writer=4 board-writer))
Reply(id=747, content=3 reply-content, board=Board(id=255, title=4 board-title, content=4 board-content, writer=4 board-writer))
Reply(id=748, content=1 reply-content, board=Board(id=256, title=5 board-title, content=5 board-content, writer=5 board-writer))
Reply(id=749, content=2 reply-content, board=Board(id=256, title=5 board-title, content=5 board-content, writer=5 board-writer))
Reply(id=750, content=3 reply-content, board=Board(id=256, title=5 board-title, content=5 board-content, writer=5 board-writer))
Reply(id=751, content=1 reply-content, board=Board(id=257, title=6 board-title, content=6 board-content, writer=6 board-writer))
Reply(id=752, content=2 reply-content, board=Board(id=257, title=6 board-title, content=6 board-content, writer=6 board-writer))
Reply(id=753, content=3 reply-content, board=Board(id=257, title=6 board-title, content=6 board-content, writer=6 board-writer))
Reply(id=754, content=1 reply-content, board=Board(id=258, title=7 board-title, content=7 board-content, writer=7 board-writer))
Reply(id=755, content=2 reply-content, board=Board(id=258, title=7 board-title, content=7 board-content, writer=7 board-writer))
Reply(id=756, content=3 reply-content, board=Board(id=258, title=7 board-title, content=7 board-content, writer=7 board-writer))
Reply(id=757, content=1 reply-content, board=Board(id=259, title=8 board-title, content=8 board-content, writer=8 board-writer))
Reply(id=758, content=2 reply-content, board=Board(id=259, title=8 board-title, content=8 board-content, writer=8 board-writer))
Reply(id=759, content=3 reply-content, board=Board(id=259, title=8 board-title, content=8 board-content, writer=8 board-writer))
Reply(id=760, content=1 reply-content, board=Board(id=260, title=9 board-title, content=9 board-content, writer=9 board-writer))
Reply(id=761, content=2 reply-content, board=Board(id=260, title=9 board-title, content=9 board-content, writer=9 board-writer))
Reply(id=762, content=3 reply-content, board=Board(id=260, title=9 board-title, content=9 board-content, writer=9 board-writer))
Reply(id=763, content=1 reply-content, board=Board(id=261, title=10 board-title, content=10 board-content, writer=10 board-writer))
Reply(id=764, content=2 reply-content, board=Board(id=261, title=10 board-title, content=10 board-content, writer=10 board-writer))
Reply(id=765, content=3 reply-content, board=Board(id=261, title=10 board-title, content=10 board-content, writer=10 board-writer))
---------- BOARD 내부 REPLY 내용 조회 완료 (N+1 문제 발생) ----------

위와같이 in 절을 톻애 데이터를 가져와 N +1 문제가 발생하지 않는것을 확인할 수 있습니다.



마치며

사실 위 fetch join, @EntityGraph@xxxToOne 관계에서만 적용하는게 현실적입니다.
다시 한번 fetch join 쿼리를 확인해 보시면 제가 distinct 를 해준것을 볼 수 있습니다.
@xxxToMany 관계에서 fetch joinEntityGraph를 적용하면 페이징 처리 불가능 데이터 부정합 현상 이 발생됩니다.

참고로 BatchSize는 어느 관계에서든 적용가능합니다.

  • @xxxToOne : fetch join or @EntityGraph or batch size 적용
  • @xxxToMany : batch size 적용
This post is written by PRO.