JPA N+1현상 삽질
JPA N+1현상 querydsl fetchJoin 삽질한 경험 공유
- Kotlin
- SpringBoot(2.2.4)
- querydsl(4.2.1)
문제
JPA로 쿼리를 하다보면, N + 1 문제를 마주하게 될 수도 있는데요.
사실 저의 문제는 정확하게 말하면 N+1 문제는 아닙니다. N+1이란 @OneToMany의 관계에서 One에 해당하는 엔티티를 조회하면 Many의 해당하는 엔티티가 지연로딩 되어 그 갯수만큼 더 조회한다고 하여 One+N(Many) N+1현상이라고 하는데 저의 경우엔 @ManyToOne의 조회인데도 One에 해당하는 엔티티가 영속성 컨텍스트에 존재하지 않으면 쿼리하게 되는 현상입니다.
저는 보통 @Query 어노테이션에 JPQL 쿼리를 작성해서 해결하곤 했었습니다.
근데 querydsl에도 fetchJoin이 있다는걸 최근(?)에서야 알고 사용해보려고 하는데 문제가 해결이 되지 않았습니다. 그 이유는.. 참 어처구니가 없었는데요 혹시나 이런 경우를 겪고 있는 분이 계실까 해서 공유차 포스팅합니다.
이유는 저처럼 join 함수를 잘못 이해하고 사용하면 매개변수 타입에 따라 fetchJoin 함수를 호출하여도 N + 1 문제가 해결이 안될수도 있다는 것인데요 ㅋㅋ
예제 구성
먼저 상황을 설명하기 위해
제가 구성한 예제 구성은 다음과 같습니다.
Restaurant(식당)과 Menu(식당의 메뉴)를 표현하는 다음과 같은 ERD와 엔티티가 있습니다.
데이터베이스 ERD
엔티티 ERD
Restaurant 엔티티 코드
@Entity
data class Restaurant(
var name: String = ""
): MutableEntity()
Menu 엔티티 코드
@Entity
data class Menu(
@ManyToOne
var restaurant: Restaurant,
var price: BigDecimal = BigDecimal.ZERO,
var name: String = ""
): MutableEntity()
데이터
문제의 코드
문제의 코드 입니다. (어떤게 문제인지 바로 아시겠나요?)
class MenuRepositoryCustomImpl: MenuRepositoryCustom, QuerydslRepositorySupport(Menu::class.java) {
override fun findAllJoinRestaurant(): MutableList<Menu> {
val m = QMenu.menu
val r = QRestaurant.restaurant
return from(m)
.leftJoin(r)
.on(r.id.eq(m.restaurant.id))
.fetchJoin()
.fetch()
}
}
함수 호출을 위해 테스트코드를 작성하고 실행했습니다. ( 테스트의 결과값 보다는 N+1 쿼리가 수행되는 것을 보기 위함이므로 일단 테스트는 통과하게끔 했습니다. )
@Test
fun `Menu 만 조회했는데, Restaurant 가 조회되는 N+1 현상 테스트` () {
val menus = menuRepository.findAllJoinRestaurant()
assertTrue(true)
}
해당 함수를 호출해서 query하면!? N + 1현상이 발생합니다.
첫번째 쿼리인 menu left outer join restaurant 를 했는데도 불구하고, 밑에 세개의 추가 쿼리가 실행되었습니다.
왜 이럴까? 하고 querydsl의 leftJoin 함수가 정의된 JPQLQuery 인터페이스를 추적 해보았습니다.
언뜻 보기에 함수의 주석 설명에는
- 주어진 target 매개변수를 기반으로 left join이 생성되어 질 것이고,
- fetchJoin을 사용하기 위해서는 fetchJoin 파라미터를 해당 join 에 추가 (정확하게는 파라미터로 이해하기보다는, fetch join 할 join 함수 뒤에 fetchJoin 함수를 추가해주면 됩니다.)
하라고 되어있습니다. 이렇게만 보면 제 코드에서 leftJoin 함수에 매개변수로 넘겨준 target이 restaurant이니 맞는 것 같기도 합니다만 현재 문제에서 안되는 이유를 찾기 위해 코드를 추적하다 querydsl fetchJoin은 다음과 같이 동작하여 JPQLQuery를 생성한다는 것을 알게되었습니다.
디버그 된 변수를 살펴보시면 self에 JPQLQuery가 텍스트 로 표현되고 있습니다.
가만살펴보니 뭔가 이상합니다. 흠 네.. 지금껏 멍청하게 target을 잘못 전달하고 있었던 것이었습니다.
fetch Join이 되려면
타겟이 restaurant가 아닌 menu의 프로퍼티로 존재하는 menu.restaurant가 되어야 하는것이었습니다.
그래서 코드를 다음과 같이 수정하고 해결하였습니다!
수정된 코드
class MenuRepositoryCustomImpl: MenuRepositoryCustom, QuerydslRepositorySupport(Menu::class.java) {
override fun findAllJoinRestaurant(): MutableList<Menu> {
val m = QMenu.menu
val r = QRestaurant.restaurant
return from(m)
//.leftJoin(r)
.leftJoin(m.restaurant)
//.on(r.id.eq(m.restaurant.id)) entity 연관관계 기반으로 어차피 맵핑되서 join함 on 조건을 주면 fetch 할 때 with 에 property를 에
.fetchJoin()
.fetch()
}
}
수정 후 쿼리
N + 1이 해결 되고 join 쿼리 한번만 수행 되었습니다.