Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
Tags
- 로또 등수 코드
- 안드로이드 스튜디오
- jpa dto 매핑
- java
- sops 암호화
- spring 채팅방
- 스프링 소셜 로그인
- springboot
- sops
- 시크릿 암호화
- mysql multi-row insert
- 인증 제외
- 스프링 오어스
- oauth 로그인
- sops age
- 로또 앱 만들기
- 스프링 환경변수 설정
- 채팅방 구현
- 시크릿 깃에 올리기
- 쿠버네티스 #fabric8
- 스프링 환경변수
- 중간 테이블 엔티티 최적화
- jpa bulk insert
- 로또 등수 알고리즘
- 어노테이션 인증
- 스프링 시큐리티 없이
- jpa 최적화
- hibe
- android studio
- 어노테이션 인증 제외
Archives
- Today
- Total
야미의 개발
JPA 쿼리 성능 개선하기 - @BatchSize 어노테이션 본문
JPA에서 연관관계를 다룰 때 가장 흔히 발생하는 성능 문제 중 하나는 N+1 입니다.
한편 N+1은 lazy 로딩으로 설정했을때 Fetch join을 사용해서 거의 다 해결이 되기때문에, 여태까지 잘 사용해왔습니다.
현재 개발하고 있는 서비스를 기준으로 한번
이 어노테이션이 있을 때와 없을 때의 차이를 비교해 보겠습니다.
JOIN FETCH의 한계
1. Cartesian Product 문제
JOIN FETCH를 사용하면 일대다 관계에서 데이터 중복이 발생합니다.
@Query("""
SELECT bp FROM BeanProductEntity bp
JOIN FETCH bp.options
""")
fun findAllWithOptions(): List<BeanProductEntity>
실행 결과:
원두1 - 옵션1
원두1 - 옵션2 // 원두1이 중복
원두1 - 옵션3 // 원두1이 중복
원두2 - 옵션4
원두2 - 옵션5 // 원두2가 중복
원두가 3개의 옵션을 가지면 결과 행이 3배로 늘어납니다.
저는 그래서 이런 조회를 할때는 In 절을 사용해서 쿼리를 두 번으로 날릴 수 있도록 했는데요 @BatchSize 어노테이션이 알아서 해주는걸 이제야 발견했습니다.
2. 다중 컬렉션 JOIN FETCH 불가
// 이런 쿼리는 불가능합니다
@Query("""
SELECT bp FROM BeanProductEntity bp
JOIN FETCH bp.options
JOIN FETCH bp.reviews // MultipleBagFetchException 발생
""")
Hibernate는 두 개 이상의 컬렉션을 동시에 JOIN FETCH하는 것을 허용하지 않습니다.
3. 페이징 처리의 어려움
@Query("""
SELECT bp FROM BeanProductEntity bp
JOIN FETCH bp.options
""")
fun findAllWithOptions(pageable: Pageable): Page<BeanProductEntity>
이 경우 Hibernate가 메모리에서 페이징을 처리하게 되어 성능 문제가 발생하네요
@BatchSize의 이점
1. 카테시안 곱 해결
@Entity
class BeanProductEntity(
@OneToMany(mappedBy = "beanProduct", fetch = FetchType.LAZY)
@BatchSize(size = 100)
val options: List<BeanProductOptionEntity> = emptyList()
)
실행되는 쿼리:
-- 1. 원두만 조회 (중복 없음)
SELECT * FROM bean_product WHERE ...
-- 2. 필요할 때 옵션을 배치로 조회
SELECT * FROM bean_product_option
WHERE bean_product_id IN (1, 2, 3, 4, 5, ...)
결과 데이터에 중복이 없어 메모리 효율이 좋다고 합니다.
2. 다중 컬렉션 처리 가능
@Entity
class BeanProductEntity(
@OneToMany(mappedBy = "beanProduct", fetch = FetchType.LAZY)
@BatchSize(size = 100)
val options: List<BeanProductOptionEntity> = emptyList(),
@OneToMany(mappedBy = "beanProduct", fetch = FetchType.LAZY)
@BatchSize(size = 100)
val reviews: List<ReviewEntity> = emptyList()
)
각 컬렉션이 독립적으로 배치 로딩되므로 제약이 없습니다.
3. 페이징과의 호환성
@Query("""
SELECT bp FROM BeanProductEntity bp
ORDER BY bp.createdAt DESC
""")
fun findAllBeans(pageable: Pageable): Page<BeanProductEntity>
원두만 먼저 페이징 처리하고, 연관 데이터는 나중에 배치로 로딩하므로 페이징이 정상적으로 작동합니다.
실제 성능 비교
JOIN FETCH 사용 시
// 원두 100개, 각각 평균 3개의 옵션을 가질 때
@Query("SELECT bp FROM BeanProductEntity bp JOIN FETCH bp.options")
fun findAllWithJoinFetch(): List<BeanProductEntity>
결과:
- 쿼리 수: 1개
- 반환 행 수: 300개 (100 × 3)
- 메모리 사용량: 높음 (중복 데이터)
- 페이징: 불가능
@BatchSize 사용 시
@OneToMany(mappedBy = "beanProduct", fetch = FetchType.LAZY)
@BatchSize(size = 100)
val options: List<BeanProductOptionEntity> = emptyList()
결과:
- 쿼리 수: 2개 (원두 1개 + 옵션 1개)
- 반환 행 수: 400개 (100 + 300, 중복 없음)
- 메모리 사용량: 낮음 (중복 제거)
- 페이징: 지원됨
언제 @BatchSize를 선택해야할까?
@BatchSize가 유리한 경우
- 일대다 관계가 많은 경우: 카테시안 곱으로 인한 데이터 중복을 피할 수 있음
- 페이징이 필요한 경우: 원본 엔티티에 대한 페이징이 정확히 작동
- 다중 컬렉션을 다뤄야 하는 경우: JOIN FETCH의 제약을 우회
- 메모리 효율성이 중요한 경우: 중복 데이터 없이 필요한 만큼만 로딩
JOIN FETCH가 유리한 경우
- 일대일 또는 다대일 관계: 카테시안 곱이 발생하지 않음
- 연관 데이터 사용이 확실한 경우: 쿼리 수를 최소화할 수 있음
- 소량의 데이터: 중복이 문제가 되지 않는 수준
라고 합니다.
저는 매번 이 어노테이션을 설정하는게 번거로워서
전역 설정을 통해서 배치 설정을 하였습니다.
application.yaml
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 50
적당한 값은 직접 메모리 사용량이나 쿼리수를 확인해보면서 조정하면 될 거 같습니다.
'스프링 > 웹 개발' 카테고리의 다른 글
Comments