JPA N+1 ๋ฌธ์ œ ์™„์ „ ์ •๋ณต — ์žฌํ˜„๋ถ€ํ„ฐ Fetch Join, Batch Size๊นŒ์ง€

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 ์ธ๋ฑ์Šค์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, ์ž‘์€ ๊ฐœ๋ฐœ ๋ฐ์ดํ„ฐ๋Š” ๊ฑฐ์ง“ ์•ˆ์‹ฌ์„ ์ค€๋‹ค.


์ฐธ๊ณ  ์ถœ์ฒ˜