본문 바로가기
[개발] 프레임워크/JPA

[JPA] @BatchSize와 쿼리 캐시에 관해서 알아보기

by Devsong26 2024. 11. 28.

Spring Boot 1.4.6.RELEASE 버전에서 개발을 진행하고 있었습니다.

album과 song이라는 테이블이 있고 song은 외래키를 가지고 있는 주 테이블이며 album은 대상 테이블입니다.

 

아래는 클래스 코드입니다.

@Data
@Entity
public class Album {

    ...

    @OneToMany(mappedBy = "album")
    private List<Song> songList = new ArrayList<>();
	
    ...
    
}

@Data
@Entity
public class Song {

	...
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "album_id", referencedColumnName = "id")
    private Album album;
	
    ...
    
}

 

이 때 Album을 조회하게 되면 Album의 전체 수 N 만큼 song을 조회하는 N개의 쿼리가 수행이 됩니다. 

 

그래서 수정하여 N + 1 이슈를 개선하기 위해 @BatchSize를 입력하여 수행되는 쿼리의 수를 줄이려고 했습니다.

@BatchSize의 size 옵션은 최대 1000을 안 넘는 것을 권장하고 있습니다.

 

아래는 수정된 Album 클래스입니다.

 

@Data
@Entity
public class Album {

    ...
	
    @BatchSize(size = "1_000")
    @OneToMany(mappedBy = "album")
    private List<Song> songList = new ArrayList<>();
	
    ...
    
}

 

이와 같이 사이즈를 1000을 하게 됐을 때 IN 절로 1000개의 album_id가 입력되어 1000개 보다 적은 수의 조회 결과가 발생했을 때 한 번의 쿼리가 수행될 걸 기대했습니다. 하지만 총 개수 66개에서 쿼리는 62개, 4개로 두 번 수행이 됐습니다. 저는 쿼리를 최대한 적게 수행하는 것이 부하를 줄이는 것이라 생각했지만 hibernate의 내부 동작은 이와 다른 것을 확인하여 레퍼런스를 찾아봤습니다.

 

다음은 레퍼런스의 내용 중 일부를 발췌했습니다.

 

하이버네이트 4.2 버전 문서이며 LEGACY에 보면 pre-built batch size로 1 - 10, 12, 25 를 말하고 있습니다.

조회하려는 수의 2를 나눈 몫을 배치 사이즈로 가져가는 것을 볼 수 있습니다(12). 

 

그래서 1000을 사이즈로 정하면 다음과 같이 배치 사이즈를 가져갑니다.

1000, 500, 250, 125, 62, 31, 15, 7, 3, 1

 

이렇게 쿼리 배치 사이즈를 나누는 이유는 쿼리 캐시를 통해 성능 개선을 하기 위함이라고 합니다.

다음은 성능에 관련된 김영한님의 답변글 내용입니다.

보통 RDB들은 select * from x where in (?) 와 같은 preparedstatement는 미리 문법을 파싱해서 최대한 캐싱을 해둡니다.

데이터가 1개, 2개, 3개, 100개가 있으면 모두 각각 다음 처럼 최대 100개의 preparedstatement 쿼리를 만들어야 합니다.

select * from x where in (?)

select * from x where in (?, ?)

select * from x where in (?, ?, ?)

select * from x where in (?, ?, ? ...)

이렇게 되면 DB 입장에서 너무 많은 preparedstatement 쿼리를 캐싱해야 하고, 성능도 떨어지게 됩니다.

그래서 하이버네이트는 이 문제를 해결하기 위해 내부에서 나름 최적화를 합니다.

100 = 설정값

50 = 100/2

25 = 50/2

12 = 25/2

그리고 1~10까지는 자주 사용하니 모두 설정

이런식으로 잡아둡니다.

 

batch_fetch_style을 DYNAMIC으로 한다면 제가 원하는 대로 한 번의 쿼리 수행으로 데이터를 조회하겠지만 쿼리 캐싱과 관련된 성능을 떨어뜨리게 된다고하여 LEGACY를 이용하여 사용하기로 했습니다.

 

hibernate 6.x (Spring boot 3.x) 부터는 배치 페치 스타일이 DYNAMIC처럼 사이즈를 기반으로 해서 IN 절에 데이터를 추가합니다.

Performs a separate SQL select to load a number of related data items using an IN-restriction as part of the SQL WHERE-clause based on a batch size

 

 

 


 

REFERENCE URIs

https://docs.jboss.org/hibernate/orm/4.2/manual/en-US/html/ch20.html#performance-fetching-batch

 

Chapter 20. Improving performance

Sometimes, you probably don't want to implement an intrusive interface, maybe due to portable concern, which is fine and Hibernate will take care of this internally with a wrapper class which implements that interface, and also an internal cache that maps

docs.jboss.org

https://www.inflearn.com/community/questions/34469/default-batch-fetch-size-%EA%B4%80%EB%A0%A8%EC%A7%88%EB%AC%B8

 

default_batch_fetch_size 관련질문 - 인프런 | 커뮤니티 질문&답변

누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.

www.inflearn.com

https://docs.jboss.org/hibernate/orm/6.0/userguide/html_single/Hibernate_User_Guide.html#fetching-basics

 

Hibernate ORM 6.0.0.CR1 User Guide

Fetching, essentially, is the process of grabbing data from the database and making it available to the application. Tuning how an application does fetching is one of the biggest factors in determining how an application will perform. Fetching too much dat

docs.jboss.org