JPA

[JPA] default_batch_fetch_size 동작

LBACK 2023. 4. 12. 21:17

OneToMany 관계에서 N+1을 해결하기 위해 default_batch_fetch_size를 처음 접하게 되었습니다. 

 

정리 글: https://lback.tistory.com/15

 

N+1 문제에 사용되는 원리는 위 글에 정리해두었지만, default_batch_fetch_size 자체의 동작에 대해 조금 더 살펴보겠습니다. 

 

기본적인 동작 역시 위 글에 정리해두었지만,

의문이 들었던 점은 2가지 부분입니다.

 

1. default_batch_fetch_size가 적용되는 경우

먼저 default_batch_fetch_size가 적용되는 경우에 대해 명확히 알고 싶었습니다.

 

이에 대해서 찾아본 결과, 다음과 같은 설명을 찾을 수 있었습니다. 

Hibernate 문서의 설명

'N+1 문제를 해결하기 위해, 지연로딩 시 미리 batch 단위로 fetch 한다'는 설명을 접할 수 있었습니다. 

 

여기서 조금 애매했던 것이, 'N+1 문제가 발생할 때만 적용되는지, 아니면 모든 지연로딩에 적용되는지' 의문이 들었습니다.

 

물론 JPA가 N+1 문제가 발생하고 있다는 것을 판단하고 처리한다는 것은 들어본 적이 없지만, 모든 지연로딩에 적용되는 것임을 확실히 하고 싶었습니다.

 

2. in 절에 들어갈 값의 조건

다음으로는 in 절에 들어갈 값 조건입니다. in에 들어갈 값이 어떻게 결정되는 것인지 명확하게 하고 싶었습니다.

이에 관해서도 다음과 같은 설명을 찾을 수 있었습니다.

The primary key values to batch fetch are chosen from among the identifiers of unfetched entity proxies or collection roles associated with the session.

Hibernate의 설명인데, 동일한 동작을 하는 @BatchSize에 대한 설명입니다. 

 

 

동작과 위 설명을 통해 in 절에 들어갈 조건은 '영속성 컨텍스트에서 관리하는 엔티티 중 아직 지연로딩이 되지 않은 연관 엔티티'라고 이해했습니다.

 

위 두가지 동작이 맞는지 확인하기 위해 간단한 테스트 코드를 작성해봤습니다. 

 

Member -> Locker는 1:1 단방향 관계입니다. 

@Test
    public void 배치_테스트(){
        // 데이터 추가
        // 회원3명, 락커 3개
        Locker locker1 = new Locker();
        locker1.setNumber(10L);
        Locker locker2 = new Locker();
        locker2.setNumber(20L);
        Locker locker3 = new Locker();
        locker3.setNumber(30L);

        Member member1 = new Member();
        member1.setName("회원1");
        member1.setLocker(locker1);
        Member member2 = new Member();
        member2.setName("회원2");
        member2.setLocker(locker2);
        Member member3 = new Member();
        member3.setName("회원3");
        member3.setLocker(locker3);

        // (Locker 는 cascade 옵션을 통해 저장)
        memberRepository.save(member1);
        memberRepository.save(member2);
        memberRepository.save(member2);

        // 플러시 및 영속성 컨텍스트 초기화
        em.flush();
        em.clear();

        Member findMember1 = memberRepository.findById(1L);
        Member findMember2 = memberRepository.findById(2L);

        System.out.println(findMember1.getLocker().getNumber());
    }

(1) 회원 3명과 락커 3개를 저장

(2) 영속성 컨텍스트 초기화

(3) 회원 1과 회원2만 조회한 후, 회원 1의 락커만 조회

 

이 코드를 실행했을 때, 이해한게 맞다면,

 

(1) batch 단위로 fetch 가 발생 (in)

(2) in 절에는 현재 영속성 컨텍스트에서 관리하는 회원 1과 회원 2의 id만 포함 (회원 3의 id 제외)

 

위와 같이 동작해야 합니다.

 

테스트를 실행하고 쿼리를 확인해보겠습니다. 

in 절이 포함된 것을 통해 batch fetch 가 이 경우에도 적용되는 것을 확인할 수 있고, 또 회원을 3명 추가했음에도 현재 2명의 회원만 조회했기 때문에 in 의 크기가 2임을 확인할 수 있습니다. 

 

결론

  • default_batch_fetch_size 는 ~ToOne을 포함한 모든 지연 로딩에 적용된다.
  • in 절에 들어갈 조건은 현재 영속성 컨텍스트에서 관리하는 엔티티 중, 아직 fetch 되지 않은 엔티티를 기준으로 한다.