๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐ŸŒพBackEnd/๐ŸŒฑ Spring

N+1 ๋ฌธ์ œ ๋‹ค์–‘ํ•œ ํ•ด๊ฒฐ๋ฒ•

by MuGeon Kim 2023. 12. 15.
๋ฐ˜์‘ํ˜•

์„œ๋ก 


  • JPA๋ฅผ ํ•™์Šตํ•˜๋ฉด ๋ฌด์กฐ๊ฑด ๋“ฃ๋Š” ํ‚ค์›Œ๋“œ๋Š” N+1 ์ด๋‹ค. ๋ณดํ†ต ๋ธ”๋กœ๊ทธ์—์„œ ์†Œ๊ฐœํ•˜๋Š” ๋ฐฉ์‹์€ fetch join์„ ํ†ตํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•œ๋‹ค๊ณ  ์ด์•ผ๊ธฐํ•œ๋‹ค.
  • ๋ฌผ๋ก  ํ‹€๋ฆฐ ๋ฐฉ์‹์€ ์•„๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์‹ค์ œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค๋ฉด์„œ N+1 ๋ฌธ์ œ๋ฅผ ๋งŽ์ด ๋งŒ๋‚˜๋ณด๋ฉด์„œ N+1์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์€ ์—ฌ๋Ÿฌ๊ฐ€์ง€๊ฐ€ ์žˆ๋‹ค.
  • ์ƒํ™ฉ์— ๋”ฐ๋ผ์„œ N+1 ๋ฌธ์ œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์„ ์ ์ ˆํ•˜๊ฒŒ ํ•ด๊ฒฐํ•˜๋Š”๊ฒŒ ์„ฑ๋Šฅ ์ €ํ•˜์˜ ๋ฌธ์ œ์— ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.

๊ธ€์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ด์ „์— ๊ฐ„๋‹จํ•˜๊ฒŒ ์ •๋ฆฌํ•˜๋ฉด
1:1 ์—ฐ๊ด€๊ด€๊ณ„ : Fetch join
Collection ์—ฐ๊ด€๊ด€๊ณ„ : default_batch_fetch_size
N๊ฐœ์˜ ์ปฌ๋ ‰์…˜์„ fetch join์„ ํ•˜๋ฉด MultipleBagFetchException์ด ๋ฐœ์ƒํ•œ๋‹ค.
ํŠน์ • ์ปฌ๋Ÿผ์„ ์กฐํšŒํ•  ๊ฒฝ์šฐ์— join์„ ํ•˜๊ณ  Projection์„ Dto๋กœ ๋งคํ•‘์„ ํ•œ๋‹ค.

๋ณธ๋ก 


1. ์™œ N+1 ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ์„ ํ•˜๋‚˜์š”?

  • JPA๋ฅผ ์ฒ˜์Œ ํ•™์Šตํ•˜๋ฉด ๊น€์˜ํ•œ๋‹˜ ๊ฐ•์˜๋ฅผ ๋ณด๋ฉด ์ฒ˜์Œ์— ๋‚˜์˜ค๋Š”๊ฑด ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ๊ฐ์ฒด์ง€ํ–ฅ ์–ธ์–ด๊ฐ„์˜ ํŒจ๋Ÿฌ๋‹ค์ž„ ์ฐจ์ด๋ฅผ ์ด์•ผ๊ธฐํ•œ๋‹ค. JPA์—์„œ ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ๋งบ์œผ๋ฉด ๋ ˆํผ๋Ÿฐ์Šค๋ฅผ ํ†ตํ•˜์—ฌ ๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” ๊ฐ์ฒด์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” Select๋ฅผ ํ†ตํ•ด์•ผ์ง€๋งŒ ์ ‘๊ทผ์„ ํ•œ๋‹ค.

1-1.๊ฐ„๋‹จํ•œ ์—”ํ‹ฐํ‹ฐ ์ฝ”๋“œ

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "MEMBER", uniqueConstraints = {
        @UniqueConstraint(name = "MEMBER_EMAIL", columnNames = {"email"}),
})
public class Member extends BaseEntity{
    @OneToMany(mappedBy = "member",fetch = FetchType.EAGER)
    private List<Request> requests = new ArrayList<>();

}
------------------------------------------------------------------------------------
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class Request {
    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;
}
  • Fetch Type์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ToMany์—์„œ๋Š” Lazy, ToOne์€ Eager๋กœ ์„ค์ •์ด ๋˜์–ด์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ผ๋‹จ ํ˜„์žฌ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์‚ดํŽด๋ณด๋ฉด ํšŒ์›(1) : ๊ฒŒ์‹œํŒ(N)์œผ๋กœ ๋˜์–ด์ ธ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ด์ œ N+1์„ ๋ฐœ์ƒ์„ ์‹œ์ผœ์„œ ๋ฌธ์ œ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

2. N+1 ๋ฌธ์ œ

2-1. Eager์—์„œ N+1 ๋ฌธ์ œ

    @BeforeEach
    void setUp(){
        List<Request> requestArrayList = new ArrayList<>();
        IntStream.range(0, 10)
                .mapToObj(i -> new Request("title" + i))
                .forEach(requestArrayList::add);

        requestRepository.saveAll(requestArrayList);

        List<Member> memberArrayList = new ArrayList<>();

        IntStream.range(0, 10)
                .mapToObj(i -> Member.builder()
                        .name("member" + i)
                        .requests(requestArrayList)
                        .build())
                .forEach(memberArrayList::add);
        memberRepository.saveAll(memberArrayList);

        entityManager.clear();
    }

    @Test
    @Transactional
    public void fix() throws Exception {
        System.out.println("===============================================================");
        List<Member> memberList = memberRepository.findAll();
        System.out.println("===============================================================");

        assertThat(memberList.size()).isNull();
    }
  • ๊ฐ„๋‹จํ•˜๊ฒŒ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ์„ ํ–ˆ๋‹ค. ์œ„์— @BeforeEach๋ฅผ ํ†ตํ•˜์—ฌ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์„ธํŒ…ํ•˜๊ณ  10๊ฐœ์˜ ํšŒ์›์„ ์ „์ฒด๋ฅผ ์กฐํšŒํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. ์ด๊ฒƒ์„ ์‹คํ–‰ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฐœ์ƒํ•œ๋‹ค.
    ```

  • ํšŒ์› ์กฐํšŒ
    Hibernate: select m1_0.id,m1_0.name from member_table m1_0

  • Request ์กฐํšŒ
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?

  • Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    ```

  • ๋ฐ”๋กœ N+1 ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค. ์ฒ˜์Œ ํšŒ์›์„ ์กฐํšŒํ•˜๊ณ  ๊ด€๋ จ Request๋ฅผ 10๋ฒˆ ์กฐํšŒํ•˜๋Š” ์ฟผ๋ฆฌ๊ฐ€ ๋‚˜๊ฐ„๋‹ค.

  • 2-2. Lazy์—์„œ N+1๋ฌธ์ œ

  • ํšŒ์›์˜ Fetch Type์„ Lazy๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ๋˜‘๊ฐ™์ด ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด ํ•œ๋ฒˆ๋งŒ ๋ฐœ์ƒํ•œ๋‹ค.

  • =============================================================== Hibernate: select m1_0.id,m1_0.name from member_table m1_0 ===============================================================

  • ๊ทธ๋Ÿฌ๋ฉด N+1 ๋ฌธ์ œ๋Š” ํ•ด๊ฒฐ์ด ๋˜์—ˆ๋Š”๊ฐ€? ์•„์ง์€ ์•„๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰์„ ํ•ด๋ณด๊ฒ ๋‹ค.

    ===============================================================
    Hibernate: select m1_0.id,m1_0.name from member_table m1_0
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id=?
    ===============================================================
  • @Test @Transactional public void fix() throws Exception { System.out.println("==============================================================="); memberRepository.findAll().stream().flatMap(member -> member.getRequests().stream() .map(Request::getTitle)) .collect(Collectors.toList()); System.out.println("==============================================================="); assertThat(memberRepository.findAll().size()).isNull(); }


> ์ •๋ฆฌ 
Eager๋ฅผ ์‚ฌ์šฉํ•˜๋“  Lazy๋ฅผ ์‚ฌ์šฉํ•˜๋“  ๊ฒฐ๊ตญ ๋™์ผํ•˜๊ฒŒ ๋ฐœ์ƒํ•œ๋‹ค. Lazy๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹จ์ง€ ํ”„๋ก์‹œ ๊ฐ์ฒด๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ ๋•Œ๋ฌธ์— N+1์ด ๋ฐ์ดํ„ฐ ์‚ฌ์šฉํ•˜๋Š” ์‹œ์ ์œผ๋กœ ๋ฏธ๋ฃจ๋Š” ๊ฒƒ์ด์ง€ ํ•ด๊ฒฐํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.

> N+1 ๋ฐœ์ƒํ•˜๋Š” ์ด์œ 
jpaRepository์— ์ •์˜ํ•œ ์ธํ„ฐํŽ˜์ด์Šค ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด JPA๋Š” ๋ฉ”์„œ๋“œ ์ด๋ฆ„์„ ๋ถ„์„ํ•ด์„œ JPQL์„ ์ƒ์„ฑํ•˜์—ฌ ์‹คํ–‰ํ•˜๊ฒŒ ๋œ๋‹ค. JPQL์€ SQL์„ ์ถ”์ƒํ™”ํ•œ ๊ฐ์ฒด์ง€ํ–ฅ ์ฟผ๋ฆฌ ์–ธ์–ด๋กœ์„œ ํŠน์ • SQL์— ์ข…์†๋˜์ง€ ์•Š๊ณ  ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด์™€ ํ•„๋“œ ์ด๋ฆ„์„ ๊ฐ€์ง€๊ณ  ์ฟผ๋ฆฌ๋ฅผ ํ•œ๋‹ค. 


<br/>

## 3. N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ

### 3-1. Fetch Join + Lazy Loading
- ๋ณดํ†ต JPA๋ฅผ ์ฒ˜์Œ ์‚ฌ์šฉํ•˜๋ฉด N+1 ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ์‹์„ Fetch Join์„ ์‚ฌ์šฉํ•ด์„œ ํ•ด๊ฒฐํ•œ๋‹ค. ํ•˜์ง€๋งŒ ๋งŒ๋Šฅ์€ ์•„๋‹ˆ๋‹ค. ์ผ๋‹จ ๊ฐ„๋‹จํ•œ ์ฝ”๋“œ๋ฅผ ๋ณด๊ณ  ์žฅ๋‹จ์ ์— ๋Œ€ํ•ด์„œ ์„ค๋ช…์„ ํ•˜๊ฒ ๋‹ค.

- ์ผ๋‹จ ๊ธฐ์กด์˜ ์ฝ”๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ฒ ๋‹ค. ์œ„์— Lazy๋ฅผ ์ฒ˜๋ฆฌํ•˜์—ฌ๋„ N+1 ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ์„ ํ•˜์˜€๋‹ค. ์ด๊ฑธ Lazy Loading, Fetch Join์„ ํ†ตํ•˜์—ฌ ํ•ด๊ฒฐํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

<br/>

- Repository
```java
    @Query("select distinct m from Member m join fetch m.requests")
    List<Member>findAllRelatedRequest();
  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
  • @Test @Transactional public void fix() throws Exception { System.out.println("==============================================================="); memberRepository.findAllRelatedRequest().stream().flatMap(member -> member.getRequests().stream() .map(Request::getTitle)) .collect(Collectors.toList()); System.out.println("==============================================================="); assertThat(memberRepository.findAll().size()).isNull(); }
  • ๊ธฐ์กด์— findAll์—์„œ ์ƒˆ๋กญ๊ฒŒ ๋งŒ๋“  findAllRelatedRequest๋กœ ๋ณ€๊ฒฝ์„ ํ•˜์˜€๋‹ค. ์ด๋ ‡๊ฒŒ ๋ณ€๊ฒฝ์„ ํ•˜๋‹ˆ ๊ธฐ์กด์— N+1 ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ ์ฟผ๋ฆฌ์—์„œ ํ•œ๋ฐฉ ์ฟผ๋ฆฌ๋กœ ๋ณ€๊ฒฝ์ด ๋˜์—ˆ๋‹ค.
===============================================================
Hibernate: select distinct m1_0.id,m1_0.name,r1_0.member_id,r1_0.id,r1_0.title 
from member_table m1_0 
join request r1_0 on m1_0.id=r1_0.member_id
===============================================================

Fetch Join์— ๋Œ€ํ•œ ํ•œ๊ณ„

1. Fetch join๊ณผ ์ผ๋ฐ˜ Join์˜ ์ฐจ์ด ( ํŒจ๋Ÿฌ๋‹ค์ž„ ๋ถˆ์ผ์น˜ ์ค„์—ฌ์คŒ )

N+1 ๋ฌธ์ œ๋ฅผ fetch join์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†๋‹ค. ์ผ๋‹จ fetch join์— ๋Œ€ํ•ด์„œ ์„ค๋ช…ํ•˜์ž๋ฉด ์šฐ๋ฆฌ๊ฐ€ ์•Œ๊ณ  ์žˆ๋Š” join๊ณผ ์ฐจ์ด๊ฐ€ ์žˆ๋‹ค.

  • fetch join์€ orm์—์„œ ์‚ฌ์šฉํ•˜๋ฉฐ ๋””๋น„ ์Šคํ‚ค๋งˆ๋ฅผ ์—”ํ‹ฐํ‹ฐ๋กœ ์ž๋™ ๋ณ€ํ™˜ > ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ์˜์†ํ™”๋ฅผ ํ•ด์ค€๋‹ค.
  • ์ด๋Ÿฌํ•œ ํŠน์ง• ๋•๋ถ„์— fetch join์„ ํ•ด์„œ ๊ฐ€์ ธ์˜จ ์—ฐ๊ด€ ๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” 1์ฐจ ์บ์‹œ์— ์ €์žฅ์ด ๋˜๊ณ  ๋‹ค์‹œ ์กฐํšŒ๋ฅผ ํ•˜์—ฌ๋„ ์ฟผ๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค.
  • ํ•˜์ง€๋งŒ ์ผ๋ฐ˜ join์ฟผ๋ฆฌ๋Š” ๋‹จ์ˆœํžˆ sql์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ฐœ๋…์ด๊ธฐ ๋•Œ๋ฌธ์— ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์™€ ๊ด€๋ จ์ด ์—†๋‹ค. ์ด๊ฒƒ์ด ํŒจ๋Ÿฌ๋‹ค์ž„์˜ ์ฐจ์ด์ด๋ฉฐ fetch join์€ ์ด๋ฅผ ์ค„์—ฌ์ฃผ๋Š” ์—ญํ™œ์„ ํ•œ๋‹ค.

2. Collection ์—ฐ๊ด€๊ด€๊ณ„ Fetch Join์‹œ ๋ฐ์ดํ„ฐ ๋ปฅํŠ€๊ธฐ (Distinct ์ถ”๊ฐ€)

  • ์œ„์— ๊ทธ๋ฆผ์„ ๋ณด๋ฉด 1:n ๊ด€๊ณ„๊ฐ€ ๋˜์–ด์ ธ ์žˆ๋‹ค. ์œ„์— ์‚ฌ์ง„์ฒ˜๋Ÿผ ๋ฐ์ดํ„ฐ๋ฅผ ์ค‘๋ณต ๋˜์–ด ์กด์žฌํ•จ์ž…๋‹ˆ๋‹ค. ์ด ๋•Œ๋ฌธ์— fetch join์„ ํ•˜๋ฉด n๊ฐœ ๋งŒํผ ๊ด€๊ณ„๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.
  • ์ด ๋•Œ๋ฌธ์— distinct์ ˆ์„ ํ™œ์šฉ์„ ํ•ด์•ผ๋ฉ๋‹ˆ๋‹ค.
  • ๊ทธ๋Ÿฌ๋ฉด ์—ฌ๊ฑฐ์„œ ๊ณ ๋ฏผํ•  ๋ถ€๋ถ„์ด ์žˆ๋‹ค.

SQL Distinct / JPQL Distinct ์ฐจ์ด

  • SQL์—์„œ Distinct์ ˆ์€ DB์—์„œ ์ˆ˜ํ–‰๋˜๋ฉฐ JOIN ๋ฐœ์ƒํ•œ ๋ฐ์ดํ„ฐ ํ˜•ํƒœ์—์„œ ๊ฐ ROW๋ฅผ ๋น„๊ตํ•˜์—ฌ ๋‹ค๋ฅธ ๊ฒฝ์šฐ๋งŒ ๋‚จ๊ธด๋‹ค. ํ•˜์ง€๋งŒ JPA์˜ Distinct๋Š” ์—”ํ‹ฐํ‹ฐ๊ฐ์ฒด์— ๋Œ€ํ•ด์„œ Distinct๋ฅผ ์ˆ˜ํ–‰์„ ํ•ฉ๋‹ˆ๋‹ค.

3. N๊ฐœ ์ปฌ๋ ‰์…˜ Fetch Join์‹œ MultipleBagFetchException

  • ์ฒ˜์Œ์— ์ปฌ๋ ‰์…˜์„ 2๊ฐœ๋ฅผ FETCH JOIN์„ ํ•˜๋ฉด ์ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
  • ์ฆ‰ ํ•˜๋‚˜๋งŒ FETCH JOIN์„ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด์„œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ํ•˜๋‚˜ ์ถ”๊ฐ€๋ฅผ ์‹œํ‚ค๊ณ  ์˜ค๋ฅ˜๋ฅผ ์‚ดํŽด๋ณด๊ฒ ๋‹ค.
    @OneToMany(mappedBy = "member",fetch = FetchType.LAZY)
    private List<Request> requests = new ArrayList<>();

    @OneToMany(mappedBy = "member",fetch = FetchType.LAZY)
    private List<Notice> notices = new ArrayList<>();

    @Query("select distinct m from Member m join fetch m.requests join fetch m.notices")
    List<Member>findAllRelatedRequest();
  • ์ด๋ ‡๊ฒŒ ๊ด€๊ณ„๋ฅผ ๋งบ๊ณ  ์ปฌ๋ ‰์…˜ 2๊ฐœ๋ฅผ fetch join์„ ํ•˜๊ฒŒ๋˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
  • org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.example.localcache.domain.Member.notices, com.example.localcache.domain.Member.requests] at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:371) at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246)
  • ๊ทธ๋Ÿฌ๋ฉด ์ด๋ ‡๊ฒŒ ์ปฌ๋ ‰์…˜์„ ์–ด๋–ป๊ฒŒ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐ์„ ํ•ด์•ผ๋˜๋Š”๊ฐ€?? ์ด๊ฒƒ์€ batch size๋กœ ํ•ด๊ฒฐํ•˜๋ฉด ๋œ๋‹ค. ๋ฐ‘์—์„œ ์‚ดํŽด๋ณด๋ฉด ๋œ๋‹ค.

4. ํŽ˜์ด์ง• ์ œํ•œ(Out Of Memory)

  • fetch join์„ ํ•˜์—ฌ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์ด์ง•์„ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

  • ์™œ๋ƒํ•˜๋ฉด ์ฟผ๋ฆฌ ์ˆ˜ํ–‰ํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ชจ๋‘ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฉ”๋ชจ๋ฆฌ์— ์˜ฌ๋ ค์„œ ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜ํ–‰์„ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋งŒ์•ฝ์— ๋งŒ๊ฑด์„ ๊ฐ€์ ธ์˜ค๋ฉด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์˜ฌ๋ฆฌ๊ฒŒ ๋˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ์„ ํ•ฉ๋‹ˆ๋‹ค.

    2022-01-16 12:37:18.309  WARN 39536 --- [           main] o.h.h.internal.ast.QueryTranslatorImpl   : HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!

3-2. default_batch_fetch_size, @BatchSize

  • Lazy Loading์‹œ ํ”„๋ก์‹œ ๊ฐ์ฒด๋ฅผ ์กฐํšŒํ•  ๋•Œ where in์ ˆ๋กœ ๋ฌถ์–ด์„œ ํ•œ๋ฒˆ์— ์กฐํšŒ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ์˜ต์…˜์ž…๋‹ˆ๋‹ค. yml์— ์ „์—ญ ์˜ต์…˜์œผ๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ๊ณ  @BatchSize๋ฅผ ํ†ตํ•ด ์—ฐ๊ด€๊ด€๊ณ„ BatchSize๋ฅผ ๋‹ค๋ฅด๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

batchsize๋Š” ๋ช‡์œผ๋กœ ์ ์šฉ์„ ํ•ด์•ผ๋˜๋‚˜์š”??
์ผ๋ฐ˜์ ์œผ๋กœ 100~1000์œผ๋กœ ์„ค์ •์„ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ dbms์— ๋”ฐ๋ผ์„œ where in์ ˆ์€ 1000๊นŒ์ง€ ์ œํ•œํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— 1000์ด์ƒ์€ ์„ค์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ทธ๋ ‡๋‹ค๊ณ  ๋„ˆ๋ฌด ํฌ๊ฒŒํ•˜๋ฉด was์—์„œ ๋ฉ”๋ชจ๋ฆฌ์— ๋กœ๋”ฉํ•˜๋””์— ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋น„์Šค์— ๋งž๊ฒŒ ์ ์ ˆํ•˜๊ฒŒ ์„ค์ •์„ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        show_sql: true
        default_batch_fetch_size: 100

    @BatchSize(size = 10)
    @OneToMany(mappedBy = "member",fetch = FetchType.LAZY)
    private List<Request> requests = new ArrayList<>();
===============================================================
Hibernate: select m1_0.id,m1_0.name from member_table m1_0
Hibernate: select r1_0.member_id,r1_0.id,r1_0.title from request r1_0 where r1_0.member_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
===============================================================
Hibernate: select m1_0.id,m1_0.name from member_table m1_0

Fetch join์˜ ํ•œ๊ณ„๋ฅผ Batch Size๋กœ ํ•ด๊ฒฐ

  • ์ปฌ๋ ‰์…˜ fetch join์‹œ paging ๋ฌธ์ œ๋‚˜ ์—ฌ๋Ÿฌ๊ฐœ ์ปฌ๋ ‰์…˜์„ fetch join์„ ํ•  ์ˆ˜ ์—†๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
  • ์ฟผ๋ฆฌ ์ˆ˜๋กœ๋Š” fetch join์ด ํ•œ๋ฐฉ์œผ๋กœ ๋‚˜๊ฐ€๊ธฐ ๋•Œ๋ฌธ์— ์œ ๋ฆฌํ•˜๋‹ค. ํ•˜์ง€๋งŒ batch size๋Š” in์ ˆ์„ ํ•˜๊ณ  ์‚ฌ์ด์ฆˆ์— ๋”ฐ๋ผ์„œ ๋ช‡๋ฒˆ์˜ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋ฐ์ดํ„ฐ ์ „์†ก๋Ÿ‰ ๊ด€์ ์—์„œ๋Š” Batch Size๊ฐ€ ์œ ๋ฆฌํ•ฉ๋‹ˆ๋‹ค. Fetch Join์€ Join์„ ํ•˜๊ณ  ๋‚˜์„œ ๊ฐ€์ ธ์˜ค๊ธฐ ๋•Œ๋ฌธ์— ์ค‘๋ณต ๋ฐ์ดํ„ฐ๋ฅผ ๋งŽ์ด ๊ฐ€์ ธ์™€์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

  1. ์ปฌ๋ ‰์…˜ n๊ฐœ ์กฐํšŒ : batch size ํ•ด๊ฒฐ
  2. ์ฟผ๋ฆฌ ๊ฐœ์ˆ˜ : fetch join์ด 1๊ฐœ์˜ ์ฟผ๋ฆฌ๋กœ ํ•ด๊ฒฐ > batch size๋Š” in์ ˆ + ์ถ”๊ฐ€์ ์ธ ์ฟผ๋ฆฌ ( ์ตœ์ ํ™”)
  3. ๋ฐ์ดํ„ฐ ์ „์†ก : fetch join์€ join์„ ํ•˜๊ณ  ์ค‘๋ณต ๋ฐ์ดํ„ฐ๋ฅผ ๋งŽ์ด ๊ฐ€์ ธ์˜ด batch size๊ฐ€ ์œ ๋ฆฌ

3-3. @EntityGraph

  • EntityGraph๋Š” ์–ด๋…ธํ…Œ์ด์…˜ ๋ฐฉ์‹์œผ๋กœ ํŽธํ•˜๊ฒŒ N+1 ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์„œ trade off๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ์‚ฌ์‹ค์€ Lazy Loading์„ Eager Loading์œผ๋กœ ๋ถ€๋ถ„์ ์œผ๋กœ ์ „ํ™˜ํ•˜๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.
  • ์—ฌ๋Ÿฌ 1:N ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ํ•œ๋ฒˆ์— Joinํ•ด ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. FetchJoin์˜ ๊ฒฝ์šฐ 1๊ฐœ์˜ Collection๊นŒ์ง€๋งŒ ๊ฐ™์ด Joinํ•˜์—ฌ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    @EntityGraph(attributePaths = {"requests"})
    @Query("select o from Member o")
    List<Member>findAllEntityGraph();
  • ์—ฌ๊ธฐ์„œ fetch join๊ณผ ์ฐจ์ด์ ์€ EntityGraph๋Š” left outer join์œผ๋กœ ๊ฐ€์ ธ์˜ค๊ณ  ์ปฌ๋ ‰์…˜ fetch join์„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์ค‘๋ณต์ ์ธ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ์ฃผ์˜๋ฅผ ํ•ด์•ผ๋œ๋‹ค. ์ด๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์ปฌ๋ ‰์…˜์˜ ์ž๋ฃŒ๊ตฌ์กฐ๋ฅผ set ๋˜๋Š” jpql์—์„œ distinct ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

3-4. join์—ฐ์‚ฐ > Projectionํ•˜์—ฌ ํŠน์ • ์ปฌ๋Ÿผ๋งŒ Dto๋กœ ์กฐํšŒ

select new ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ.Dto(์›ํ•˜๋Š” ํ•„๋“œ) 
from Member m
join m.request r
where m.id=r.id
  • ์žฅ์ ์œผ๋กœ๋Š” ๋งŽ์€ ์ปฌ๋Ÿผ์—์„œ projectionํ•˜์—ฌ ํŠน์ • ์ปฌ๋Ÿผ๋งŒ ์กฐํšŒ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ปค๋ฒ„๋ง ์ธ๋ฑ์Šค๋กœ ์ฒ˜๋ฆฌ๋  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ์ ์ธ ์ด์ ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.
  • ํ•˜์ง€๋งŒ ๋‹จ์ ์œผ๋กœ๋Š” ์˜์†์„ฑ ์ปจํ…Œ์ŠคํŠธ์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋™์ž‘ํ•˜๊ณ  repository๊ฐ€ dto์— ์˜์กดํ•˜๊ธฐ ๋•Œ๋ฌธ์— dao ๋ณ€๊ฒฝ์ด ํ•„์š”ํ•˜๋‹ค.

๊ฒฐ๋ก 


๊ธ€์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ด์ „์— ๊ฐ„๋‹จํ•˜๊ฒŒ ์ •๋ฆฌํ•˜๋ฉด
1:1 ์—ฐ๊ด€๊ด€๊ณ„ : Fetch join
Collection ์—ฐ๊ด€๊ด€๊ณ„ : default_batch_fetch_size
N๊ฐœ์˜ ์ปฌ๋ ‰์…˜์„ fetch join์„ ํ•˜๋ฉด MultipleBagFetchException์ด ๋ฐœ์ƒํ•œ๋‹ค.
ํŠน์ • ์ปฌ๋Ÿผ์„ ์กฐํšŒํ•  ๊ฒฝ์šฐ์— join์„ ํ•˜๊ณ  Projection์„ Dto๋กœ ๋งคํ•‘์„ ํ•œ๋‹ค.

์ฐธ๊ณ 


https://junhyunny.github.io/spring-boot/jpa/jpa-fetch-join-paging-problem/

https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1

๋ฐ˜์‘ํ˜•