데이터베이스는 쿼리의 성능을 높이기 위해 PreparedStatement를 캐싱한다. 동일한 모양의 쿼리가 여러 번 실행되면, 그 쿼리의 구조를 미리 캐싱해 두어 다시 파싱하거나 실행 계획을 새로 세우지 않도록 한다.
PreparedStatement: 미리 작성된 SQL 쿼리의 틀, 실제 실행할 때 쿼리 안의 변수를 동적으로 대입해 성능을 향상시키고, 보안적으로도 SQL 인젝션을 방지할 수 있다.
하지만 보통의 SELECT * FROM table WHERE id IN (?)라는 쿼리를 만든다고 가정할 때, 데이터가 1개일 때는 SELECT * FROM table WHERE id IN (?), 2개일 때는 SELECT * FROM table WHERE id IN (?, ?) 이런 식으로 IN 절 안에 들어가는 값들이 1개일 때와 100개일 때의 쿼리의 모양이 다르다.
위처럼 IN 연산자에 따라 쿼리의 모양이 매번 달라지면, 쿼리의 개수가 늘어나고 데이터베이스는 그만큼 더 많은 PreparedStatement를 캐싱해야 하므로 성능이 떨어지게 된다.
하이버네이트의 최적화 전략
따라서 하이버네이트는 이러한 문제를 해결하기 위해 내부적으로 IN 연산자의 조건에 들어가는 값의 개수를 효율적으로 분배하여 쿼리의 모양을 최소화하는 전략을 사용한다.
1. 쿼리 모양 최적화
예를 들어, IN 연산자에 최대 100개의 값을 넣을 수 있다고 가정했을 때 원래대로라면 100개의 값을 처리하기 위해 100개의 PreparedStatement 모양이 필요하다. 하지만 Hibernate는 이를 14개의 쿼리로 최적화한다.
- 1~10까지는 자주 사용되므로 이 부분은 그대로 둔다. (IN (?), IN (?, ?), …, IN (?, ?, … ?) 10개)
- 이후, 최대값인 100을 기준으로 절반씩 나누어 캐싱 가능한 쿼리의 모양을 제한한다.
- 100개의 값을 나누어 처리하기 위해 IN 절에서 100, 50, 25, 12 등의 단위로 나누어 처리하는 방식이다.
2. 실제 쿼리 분할 방식
만약 IN 연산자에 18개의 값을 넣는 경우, 하이버네이트는 이를 효율적으로 처리하기 위해 12와 6으로 나눈 두 개의 쿼리가 실행된다
SELECT * FROM table WHERE id IN (?, ?, ..., ?) # (12개)
SELECT * FROM table WHERE id IN (?, ?, ?, ?, ?, ?) # (6개)
이런 방식으로 필요 없는 PreparedStatement의 수를 줄이고, 데이터베이스에서 캐시할 쿼리의 개수를 최소화하게 된다.
batch fetching 옵션
Legacy 방식(기본값)
spring.jpa.properties.hibernate.batch_fetch_style: legacy
Legacy 방식은 Hibernate 5.0 이전 버전에서 사용되던 기본 배치 페칭 방식이며, 설정된 batch size만큼의 데이터를 한 번에 가져온다.
ex) 배치 사이즈가 10으로 설정되었다면, 한 번의 쿼리로 10개의 엔티티를 가져오도록 최적화된다.
최적화 없이 고정된 크기만큼 배치를 처리하므로, 가변적인 데이터 크기 상황에서는 비효율적일 수 있다. 필요한 데이터의 양이 배치 사이즈보다 적거나 많을 경우에도 모든 엔티티를 일괄적으로 처리한다.
Padded 방식
spring.jpa.properties.hibernate.batch_fetch_style: padded
Padded 방식은 Hibernate 5.0 이후에 추가된 방식으로, 배치 사이즈보다 적은 데이터를 가져와야 할 때, 배치 크기를 채우기 위해 빈 자리를 가짜 값으로 채워서 쿼리를 실행해 데이터베이스 쿼리의 캐싱 효율성을 높인다.
ex) 배치 사이즈가 10이고 7개의 엔티티만 필요하다면 나머지 3개의 자리는 가짜 값으로 패딩하여 쿼리의 모양을 일정하게 유지한다. 이를 통해 쿼리가 매번 다른 모양을 가지는 문제를 방지할 수 있어 쿼리 캐싱의 효율성을 높일 수 있다.
배치 사이즈가 고정되어 있고 가변적인 엔티티 수에 대응할 수 있어 캐싱 효율성은 높지만, 패딩된 가짜 값들이 추가되기 때문에 일부 상황에서는 성능이 저하될 수 있다.
Dynamic 방식
spring.jpa.properties.hibernate.batch_fetch_style: dynamic
Dynamic 방식은 패딩이나 고정 크기 없이, 가변적인 배치 사이즈를 사용하여 필요한 만큼만 데이터를 가져오는 방식이다.
ex) 만약 18개의 엔티티를 가져와야 한다면, 정확히 18개의 데이터를 처리할 수 있는 배치 쿼리를 생성한다.
데이터가 얼마나 필요한지에 따라 유동적으로 배치 크기가 설정되므로, 패딩이나 불필요한 자원 사용 없이 필요한 데이터만 가져올 수 있지만, 매번 쿼리의 모양이 달라지므로 PreparedStatement 캐싱의 이점을 크게 누리지 못하게 된다는 단점이 있다.
정리
데이터베이스는 자주 사용되는 쿼리의 모양을 미리 캐싱해 두고 재사용할 때 캐시된 쿼리를 불러와 성능을 높이지만, IN 절처럼 쿼리의 변수가 많고 변동성이 클 경우 쿼리 모양이 다양해져 캐싱이 어려워지게 된다. 하이버네이트는 이를 최소한의 캐시로도 성능을 최적화하기 위해 쿼리의 모양을 제한하고 큰 쿼리를 효율적으로 나누어 실행함으로써 데이터베이스에 과부하를 줄이고 성능을 높인다.
https://docs.jboss.org/hibernate/orm/4.2/manual/en-US/html/ch20.html#performance-fetching-batch
'Back-end' 카테고리의 다른 글
[Java] GC(: Garbage Collection) 로그로 G1GC 동작과정 확인하기 (1) | 2024.09.11 |
---|---|
[JPA] N+1 문제 해결 방법 (0) | 2024.08.27 |
[Spring] 정적 코드 분석을 위해 SonarCloud 사용하기 (0) | 2024.08.21 |
[Spring] SonarQube로 프로젝트 정적 코드 분석 (0) | 2024.08.19 |
[Spring Boot] 로컬 환경에서 Github Actions 테스트하기 (0) | 2024.08.16 |