JPA N+1 ๋ฌธ์ . ์ ๋์๊ฐ๋ API๊ฐ ๋ฐ์ดํฐ๊ฐ ์์ด์ ๊ฐ์๊ธฐ ๋๋ ค์ก๋ค๋ฉด, ์ญ์คํ๊ตฌ N+1์ด๋ค. ์ ์๊ธฐ๋์ง, ์ด๋ป๊ฒ ์ฐพ๋์ง, ๊ทธ๋ฆฌ๊ณ Fetch Join·EntityGraph·Batch Size๋ก ์ด๋ป๊ฒ ์ก๋์ง๋ฅผ ์ฟผ๋ฆฌ ๋ก๊ทธ์ ํจ๊ป ์ ๋ฆฌํ๋ค.
๋ค์ด๊ฐ๋ฉฐ
JPA๋ฅผ ์ฐ๋ค ๋ณด๋ฉด ์ฝ๋๋ ๊น๋ํ๋ฐ ์ฑ๋ฅ์ด ์ด์ํ๊ฒ ์ ๋์ค๋ ์๊ฐ์ด ์จ๋ค.
๋ถ๋ช
ํ์ ๋ชฉ๋ก์ ํ ๋ฒ ์กฐํํ์ ๋ฟ์ธ๋ฐ, ๋ก๊ทธ๋ฅผ ๋ณด๋ฉด ์ฟผ๋ฆฌ๊ฐ ์์ญ, ์๋ฐฑ ๊ฐ๊ฐ ๋๊ฐ๋ค. ์ด๊ฒ ๋ฐ๋ก N+1 ๋ฌธ์ ๋ค.
N+1์ ์กฐ์ฉํ๋ค. ๊ฐ๋ฐ DB์ ๋ฐ์ดํฐ๊ฐ ๋ช ๊ฑด ์์ ๋ ๋ฉ์ฉกํ๋ค๊ฐ, ์ด์์์ ๋ฐ์ดํฐ๊ฐ ์์ด๋ฉด ๊ทธ์ ์ผ ํฐ์ง๋ค.
DB ์ธ๋ฑ์ค ๊ธ์์ "๊ฐ๋ฐ DB์ ์์ ๋ฐ์ดํฐ๋ก ํ๋จํ์ง ๋ง๋ผ"๊ณ ํ๋๋ฐ, N+1์ด ๋ฑ ๊ทธ๋ฐ ๊ฒฝ์ฐ๋ค.
N+1์ ๋ฒ๊ทธ๊ฐ ์๋๋ผ "์๋๋๋ก ๋์ํ๋๋ฐ ๋๋ฆฐ" ๋ฌธ์ ๋ค. ๊ทธ๋์ ๋ ๋์น๊ธฐ ์ฝ๋ค.
์ด ๊ธ์์๋ N+1์ ์ง์ ์ฌํํ๊ณ , ํ์งํ๊ณ , ์ธ ๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก ํด๊ฒฐํ๋ค. ์์๋ Spring Data JPA / Hibernate ๊ธฐ์ค์ด๋ค.
N+1 ๋ฌธ์ ๋
์ฐ๊ด๊ด๊ณ๊ฐ ์๋ ์ํฐํฐ๋ฅผ ์กฐํํ ๋, ์ฐ๊ด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ค๊ณ ์ถ๊ฐ ์ฟผ๋ฆฌ๊ฐ N๋ฒ ๋ ๋๊ฐ๋ ํ์์ด๋ค.
์ฒ์ 1๋ฒ(๋ชฉ๋ก ์กฐํ) + ์ฐ๊ด ๋ฐ์ดํฐ N๋ฒ = "N+1".
ํ์(Member)๊ณผ ํ(Team)์ด ์๋ค๊ณ ํ์. ํ์์ ํ๋์ ํ์ ์ํ๋ค.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
@ManyToOne(fetch = FetchType.LAZY) // ์ง์ฐ ๋ก๋ฉ
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
1. N+1 ์ฌํํ๊ธฐ
ํ์ ์ ์ฒด๋ฅผ ์กฐํํ๊ณ , ๊ฐ ํ์์ ํ ์ด๋ฆ์ ์ถ๋ ฅํ๋ ํ๋ฒํ ์ฝ๋๋ค.
List<Member> members = memberRepository.findAll(); // ์ฟผ๋ฆฌ 1๋ฒ
for (Member member : members) {
// member.team์ LAZY → ์ค์ ์ ๊ทผํ๋ ์๊ฐ ์ฟผ๋ฆฌ ๋ฐ์
System.out.println(member.getTeam().getName()); // ํ์ ์๋งํผ ์ฟผ๋ฆฌ
}
์คํ๋๋ SQL์ ๋ณด๋ฉด ๋ฌธ์ ๊ฐ ๋๋ฌ๋๋ค.
-- 1. ํ์ ์ ์ฒด ์กฐํ (1๋ฒ)
SELECT * FROM member;
-- 2. ๊ฐ ํ์์ ํ์ ์กฐํ (ํ์์ด 100๋ช
์ด๋ฉด 100๋ฒ!)
SELECT * FROM team WHERE team_id = 1;
SELECT * FROM team WHERE team_id = 2;
SELECT * FROM team WHERE team_id = 3;
-- ... 100๋ฒ ๋ฐ๋ณต
ํ์์ด 100๋ช
์ด๋ฉด ์ฟผ๋ฆฌ๊ฐ 1 + 100 = 101๋ฒ ๋๊ฐ๋ค. ๋ฐ์ดํฐ๊ฐ ๋์๋ก ์ฟผ๋ฆฌ๋ ์ ํ์ผ๋ก ๋์ด ์ฑ๋ฅ์ด ๋ฌด๋์ง๋ค.
์ง์ฐ ๋ก๋ฉ(LAZY)๋ ์ฐ๊ด ๊ฐ์ฒด์ ๋ฃจํ ์์์ ์ ๊ทผํ๋ ๊ฒ ์ ํ์ ์ธ ํจํด์ด๋ค.
2. N+1 ํ์งํ๊ธฐ
ํด๊ฒฐ๋ณด๋ค ๋จผ์ , ์ง๊ธ ์ฟผ๋ฆฌ๊ฐ ๋ช ๋ฒ ๋๊ฐ๋์ง ๋์ผ๋ก ๋ด์ผ ํ๋ค. ๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ SQL ๋ก๊ทธ๋ฅผ ์ผ๋ ๊ฒ์ด๋ค.
# application.yml
spring:
jpa:
show-sql: true # ์คํ SQL ์ถ๋ ฅ
properties:
hibernate:
format_sql: true # ๋ณด๊ธฐ ์ข๊ฒ ์ ๋ ฌ
logging:
level:
org.hibernate.SQL: debug
๋ ์ ํํ๊ฒ ๋ณด๋ ค๋ฉด p6spy ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฐ๋ฉด ์ค์ ๋ฐ์ธ๋ฉ๋ ๊ฐ๊น์ง, ์ฟผ๋ฆฌ ์คํ ํ์๊น์ง ํ๋์ ๋ณด์ธ๋ค.
// build.gradle.kts
implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.1")
ํ ์คํธ ์ฝ๋๋ก ์ฟผ๋ฆฌ ์๋ฅผ ๊ฒ์ฆํด๋๋ฉด, ๋์ค์ ๋๊ฐ N+1์ ๋ค์ ๋ง๋ค์์ ๋ ๋ฐ๋ก ์กํ๋ค.
@Test
void Nํ๋ฌ์ค1_๊ฒ์ฆ() {
List<Member> members = memberRepository.findAll();
members.forEach(m -> m.getTeam().getName());
// ์ฟผ๋ฆฌ ๋ก๊ทธ๋ฅผ ๋ณด๊ณ 1๋ฒ๋ง ๋๊ฐ๋์ง ํ์ธ
}
3. ํด๊ฒฐ๋ฒ 1: Fetch Join
๊ฐ์ฅ ์ง์ ์ ์ธ ํด๋ฒ. JPQL์์ JOIN FETCH๋ฅผ ์ฐ๋ฉด ์ฐ๊ด ์ํฐํฐ๋ฅผ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ํจ๊ป ๊ฐ์ ธ์จ๋ค.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("SELECT m FROM Member m JOIN FETCH m.team")
List<Member> findAllWithTeam();
}
์คํ๋๋ SQL์ ๋จ ํ ๋ฒ์ด๋ค.
-- JOIN์ผ๋ก ํ์๊ณผ ํ์ ํ ๋ฒ์ ์กฐํ
SELECT m.*, t.*
FROM member m
JOIN team t ON m.team_id = t.id;
101๋ฒ์ด 1๋ฒ์ผ๋ก ์ค์๋ค. ToOne ๊ด๊ณ(@ManyToOne, @OneToOne)์์๋ fetch join์ด ๊ฐ์ฅ ๊น๋ํ ํด๋ฒ์ด๋ค.
4. ํด๊ฒฐ๋ฒ 2: @EntityGraph
Fetch Join์ JPQL ์์ด ์ด๋ ธํ ์ด์ ์ผ๋ก ์ ์ธํ๋ ๋ฐฉ๋ฒ์ด๋ค. ๋ฉ์๋ ์ด๋ฆ ๊ธฐ๋ฐ ์ฟผ๋ฆฌ์๋ ๋ถ์ผ ์ ์์ด ํธํ๋ค.
public interface MemberRepository extends JpaRepository<Member, Long> {
@EntityGraph(attributePaths = {"team"}) // team์ ํจ๊ป ๋ก๋ฉ
List<Member> findAll();
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username);
}
๋์์ fetch join๊ณผ ๊ฐ๋ค(๋ด๋ถ์ ์ผ๋ก left outer join). JPQL์ ๋ฐ๋ก ์ฐ๊ธฐ ์ซ๊ณ ์ฐ๊ด๊ด๊ณ๋ง ์ฆ์ ๋ก๋ฉํ๊ณ ์ถ์ ๋ ๊ฐํธํ๋ค.
๋ณต์กํ ์กฐ๊ฑด์ด ๋ถ์ผ๋ฉด fetch join์ด ๋ ์ ์ฐํ๋ค.
5. ํด๊ฒฐ๋ฒ 3: Batch Size
์ปฌ๋ ์
(@OneToMany)์ ๋ค๋ฃฐ ๋๋, fetch join์ ์ฐ๊ธฐ ์ด๋ ค์ด ์ํฉ์์ ๊ฐ๋ ฅํ ํด๋ฒ์ด๋ค.
IN ์ ๋ก ๋ฌถ์ด์ N๋ฒ ์ฟผ๋ฆฌ๋ฅผ ๋ช ๋ฒ์ผ๋ก ์ค์ธ๋ค.
# application.yml — ์ ์ญ ์ค์ (๊ถ์ฅ)
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
์ด ์ค์ ํ๋๋ก, ์ฐ๊ด ๋ฐ์ดํฐ๋ฅผ ๊ฐ๋ณ ์กฐํํ๋ ๋์ IN ์ ๋ก ๋ฌถ๋๋ค.
-- Batch Size ์ ์ฉ ์ : team_id๋ง๋ค ๋ฐ๋ก (N๋ฒ)
SELECT * FROM member WHERE team_id = 1;
SELECT * FROM member WHERE team_id = 2;
-- ...
-- Batch Size ์ ์ฉ ํ: IN ์ ๋ก ํ ๋ฒ์ (N/100๋ฒ)
SELECT * FROM member WHERE team_id IN (1, 2, 3, ..., 100);
ํ 100๊ฐ์ ํ์์ ๊ฐ์ ธ์ฌ ๋, batch size๊ฐ 100์ด๋ฉด ์ฟผ๋ฆฌ๊ฐ 100๋ฒ → 1๋ฒ์ผ๋ก ์ค์ด๋ ๋ค.
ํน์ ์ํฐํฐ์๋ง ์ ์ฉํ๋ ค๋ฉด @BatchSize(size = 100)๋ฅผ ํ๋๋ ํด๋์ค์ ๋ถ์ด๋ฉด ๋๋ค.
6. ํจ์ : ์ปฌ๋ ์ Fetch Join + ํ์ด์ง
์ฌ๊ธฐ์ ๋ง์ด ๋นํ๋ค. ์ปฌ๋ ์ ์ fetch joinํ๋ฉด์ ํ์ด์งํ๋ฉด ์ฌ๊ฐํ ๋ฌธ์ ๊ฐ ์๊ธด๋ค.
// ์ํ: ์ปฌ๋ ์
fetch join + ํ์ด์ง
@Query("SELECT t FROM Team t JOIN FETCH t.members")
Page<Team> findAllWithMembers(Pageable pageable);
์ด๋ ๊ฒ ํ๋ฉด Hibernate๊ฐ ๊ฒฝ๊ณ ๋ฅผ ๋์ด๋ค.
HHH000104: firstResult/maxResults specified with collection fetch;
applying in memory!
DB์์ ํ์ด์งํ์ง ๋ชปํ๊ณ ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ๋ค ์ฌ๋ฆฐ ๋ค ์๋ผ๋ธ๋ค.
๋ฐ์ดํฐ๊ฐ ๋ง์ผ๋ฉด ๊ทธ๋๋ก OOM(๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ)์ผ๋ก ์ฃฝ๋๋ค.
์ปฌ๋ ์
์ fetch joinํ๋ฉด ๊ฒฐ๊ณผ ํ์ด ๋ปฅํ๊ธฐ(์นดํ
์์ ๊ณฑ)๋๊ธฐ ๋๋ฌธ์ ํ์ด์ง ์์ฒด๊ฐ ๋ถ๊ฐ๋ฅํ ๊ฒ์ด๋ค.
ํด๊ฒฐ: ํ์ด์ง์ด ํ์ํ ์ปฌ๋ ์ ์ fetch join ๋์ Batch Size๋ก ํผ๋ค.
// ToOne์ fetch join, ์ปฌ๋ ์
์ batch size์ ๋งก๊ธด๋ค
@Query("SELECT t FROM Team t") // ์ปฌ๋ ์
์ fetch join ์ ํจ
Page<Team> findAllTeams(Pageable pageable);
// default_batch_fetch_size ์ค์ ์ผ๋ก members๋ IN ์ ๋ก ๋ก๋ฉ๋จ
๋ ํ๋, ์ปฌ๋ ์
์ ๋ ๊ฐ ์ด์ fetch joinํ๋ฉด MultipleBagFetchException์ด ํฐ์ง๋ค.
์ด๊ฒ๋ batch size๋ก ํ์ด์ผ ํ๋ค.
7. ์ธ์ ๋ฌด์์ ์ธ๊น
| ์ํฉ | ๊ถ์ฅ ํด๋ฒ | ์ด์ |
ToOne ๊ด๊ณ (@ManyToOne) |
Fetch Join ๋๋ @EntityGraph | ํ ๋ฐฉ ์ฟผ๋ฆฌ๋ก ๊น๋ |
| ์ปฌ๋ ์ + ํ์ด์ง ์์ | Fetch Join (๋จ, 1๊ฐ๋ง) | ์นดํ ์์ ๊ณฑ ์ฃผ์ |
| ์ปฌ๋ ์ + ํ์ด์ง ์์ | Batch Size | ๋ฉ๋ชจ๋ฆฌ ํ์ด์ง ํํผ |
| ์ปฌ๋ ์ 2๊ฐ ์ด์ | Batch Size | MultipleBagFetchException ํํผ |
| ์ ์ญ ๊ธฐ๋ณธ ๋ฐฉ์ด | default_batch_fetch_size |
๊น์๋๋ฉด ์์ ๋ง ์ญํ |
์ค๋ฌด์์ ๊ฐ์ฅ ๋ฌด๋ํ ์กฐํฉ์ "ToOne์ fetch join, ์ปฌ๋ ์
์ default_batch_fetch_size" ๋ค.
์ด ๋๋ง ์ฑ๊ฒจ๋ ๋๋ถ๋ถ์ N+1์ด ์ฌ๋ผ์ง๋ค.
๋ง์น๋ฉฐ
N+1์ JPA์ ๋ฒ๊ทธ๊ฐ ์๋๋ผ, ์ง์ฐ ๋ก๋ฉ์ด๋ผ๋ ํธ๋ฆฌํ ๊ธฐ๋ฅ์ ๊ทธ๋ฆผ์๋ค.
์ฝ๋๋ ์์ฐ์ค๋ฌ์ด๋ฐ ๋ค์์ ์ฟผ๋ฆฌ๊ฐ ํญ๋ฐํ๋ค.
๊ทธ๋์ ํต์ฌ์ "๋ด๊ฐ ์ง ์ฝ๋๊ฐ ์ค์ ๋ก ์ฟผ๋ฆฌ๋ฅผ ๋ช ๋ฒ ๋ ๋ฆฌ๋์ง ๋ณด๋ ์ต๊ด" ์ด๋ค.
์ถ์ฒํ๋ ์์๋ ์ด๋ ๋ค.
๋จผ์ show-sql์ด๋ p6spy๋ก ์ฟผ๋ฆฌ ๋ก๊ทธ๋ฅผ ์ผ์ N+1์ด ์๋์ง ํ์ธํ๊ณ , ToOne์ fetch join, ์ปฌ๋ ์
์ batch size๋ก ํด๊ฒฐํ๋ค.
๊ทธ๋ฆฌ๊ณ default_batch_fetch_size๋ฅผ ์ ์ญ์ ๊น์ ์์ ๋ง์ ๋๋ค.
๊ทธ๋ฆฌ๊ณ ๊ฐ์ฅ ์ค์ํ ๊ฑด, ์ด์๊ณผ ๋น์ทํ ์์ ๋ฐ์ดํฐ๋ก ํ
์คํธํ๋ ๊ฒ์ด๋ค.
N+1์ ๋ฐ์ดํฐ๊ฐ ์ ์ผ๋ฉด ์ ๋ ์ ๋ณด์ธ๋ค.
DB ์ธ๋ฑ์ค์ ๋ง์ฐฌ๊ฐ์ง๋ก, ์์ ๊ฐ๋ฐ ๋ฐ์ดํฐ๋ ๊ฑฐ์ง ์์ฌ์ ์ค๋ค.
์ฐธ๊ณ ์ถ์ฒ
- Hibernate ORM Documentation: Fetching — https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#fetching
- Spring Data JPA: Entity Graphs — https://docs.spring.io/spring-data/jpa/reference/jpa/entity-graph.html
- Vlad Mihalcea: N+1 query problem — https://vladmihalcea.com/n-plus-1-query-problem/
'๐ Tech Stack > Backend' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| ์ฟ ํค vs ์ธ์ vs ํ ํฐ โ ์น ์ธ์ฆ ๋ฐฉ์ ๋น๊ต (0) | 2026.06.09 |
|---|---|
| RestClient๋ก ์ธ๋ถ API ํธ์ถํ๊ธฐ (0) | 2026.05.27 |
| Spring Security 7.0 + jjwt 0.12.6๋ก JWT ์ธ์ฆ ๊ตฌํํ๊ธฐ (0) | 2026.03.04 |