프로젝트를 진행하면서 쿼리 최적화한 과정을 기록하려고 한다. SpringBoot3.x , JPA를 사용한 프로젝트이다.
🔍 연관관계
기본적으로 JPA를 사용하여 프로젝트를 진행하면 Delete, Update, 맵핑관계가 설정되어 있는 조회 로직에서 쿼리를 확인하고 최적화를 진행하고 있다. 이번에는 일대다 관계를 가진 엔티티를 조회할 때의 쿼리 최적화를 진행하였다.
@AllOpen
@Entity
class Product(
@ManyToOne
@JoinColumn
var category: ProductCategory,
) {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
}
@Entity
class ProductCategory(
@OneToMany(mappedBy = "category")
val products: MutableList<Product>
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
}
🔍 문제의 쿼리
검색어를 기준으로 Product를 조회하는 쿼리를 확인해보자
fun findBySearchWord(searchWord: String): List<Product> {
return queryFactory
.selectFrom(product)
.where(product.productName.contains(searchWord))
.limit(10)
.fetch()
}
쿼리가 여러개 나갔음을 확인할 수 있다. 기본적으로 @ToOne관계에서는 fetch 속성의 기본값이 Eager이기 때문에 Product
를 조회하면 ProductCategory
를 조회하는 쿼리가 한 번 더 나가게 된다. 기본적으로 Product
를 사용하는 곳에서 ProductCategory
가 항상 같이 사용된다면 fetchJoin()을 이용해서 쿼리를 최적화하는 것이 좋다.
🔍 Fetch-join을 활용한 최적화
fun findBySearchWord(searchWord: String): List<Product> {
return queryFactory
.selectFrom(product)
.join(product.category, productCategory).fetchJoin() // 이 부분 추가됨
.where(product.productName.contains(searchWord))
.limit(2000)
.fetch()
}
Fetch-Join을 사용하면 쿼리가 하나만 나가기 때문에 최적화가 된다고 하는데 프로젝트가 간단해서 그런지 이를 느끼기는 쉽지 않은 것 같다.
개인적으로 Fetch-Join을 쓰는 경우가 한 가지 더 있는데 트랜잭션 밖에서 관련 엔티티를 지연로딩 할 때이다.
https://brightmango.tistory.com/entry/OSIV-필터와-지연로딩-에러
fetch-join의 단점
모든 기술에는 장,단점이 있듯이 fetch-join에도 단점과 한계가 존재한다.
- 둘 이상의 컬렉션에는 적용할 수 없다.
- B와 C 모두에 대해 가져오기 조인을 적용하면 B와 C의 각 조합이 중복되는 결과 집합이 생성된다.
- 페이징을 적용할 수 없다.
- 데이터 수를 예측할 수 없다.
이런 한계를 극복하기 위해 batch-fetch-size를 지정해줄 수 있다.