QueryDSL에서 JPA N+1 문제 해결하기
JPA를 배우며 N+1 문제에 대해 인지를 했지만, QueryDSL을 사용했을 때 어색하고, 잘 활용하지 못했던 경험이 있었습니다.
그래서 자주 사용하는 QueryDsl 기준으로 N+1문제를 해결하는 방법에 대해 알아보기 위해 글을 작성했습니다.
JPA N+1
한 Entity를 조회했을 때 한 개의 쿼리가 생성되는 기댓값을 가지고 조회를 합니다. 하지만 어떤 상황의 경우에 한 개의 쿼리가 아닌 여러 개의 쿼리가 N개만큼 추가로 발생하는데 이것을 JPA의 N+1 문제라고 합니다. N+1문제가 발생하는 상황은 다음과 같습니다.
- 즉시 로딩 엔티티를 조회하는 경우
- 즉시 로딩의 경우 엔티티를 조회 할때마다 쿼리를 바로 날리기 때문에 N+1문제가 발생합니다.
- 지연 로딩으로 가져온 엔티티의 하위 엔티티도 함께 조회하는 경우
- 지연 로딩의 경우 참조된 하위 엔티티도 함께 조회할 때 하위 엔티티는 영속성 컨텍스트에는 프록시 상태로 존재합니다. 따라서 쿼리를 실행하여 데이터베이스에서 하위 엔티티를 가져오게 되어 N+1문제가 발생합니다.
명시적 조인과 묵시적 조인
먼저 N+1문제를 확인하기 전에 하나의 쿼리가 성립될 수 있도록 조인 방법에 대해 알아보겠습니다. QueryDSL에서 조인할 때 2가지 방법이 존재합니다. 아래 두 가지 방법을 통해 조인 할 수 있습니다.
//명시적 조인
List<Post> posts = jpaQueryFactory.selectFrom(post)
.leftJoin(postImage).on(postImage.post.id.eq(post.id))
.fetch();
//묵시적 조인
List<Post> posts = jpaQueryFactory.selectFrom(post)
.leftJoin(post.postImages)
.fetch();
- 명시적 조인
- 명시적 조인이란 사용자가 Join 키워드를 사용하여 연관된 엔티티 간의 관계를 지정하는 것을 말합니다.
- 명시적 조인은 관계를 명확하게 드러내기 때문에 코드의 가독성을 높일 수 있습니다.
- 묵시적 조인
- 묵시적 조인은 JPA가 엔티티 간의 관계를 자동으로 인식하여 필요한 Join을 수행하는 것을 의미합니다.
- 엔티티 간의 관계가 매핑되어 있을 때 활용할 수 있습니다.
- 묵시적 조인은 코드를 간결하게 작성할 수 있지만, 가독성이 낮아질 수 있고 성능 문제를 야기할 수 있습니다.
컬렉션 조회에서의 N+1
QueryDSL을 활용하여 한 개의 쿼리가 생성되고 나서, 다시 응답 값을 조회할때 N+1문제가 발생하게 됩니다. 이 경우는 어떻게 처리해야 할까요? fetchJoin을 활용하거나, batchSize를 설정하여 해결할 수 있습니다.
fetchJoin 활용
- 다음과 같이 묵시적 조인과 fetchJoin을 사용함으로써 QueryDSL를 사용하여 List를 불러올 때 N+1문제를 발생하지 않도록 할 수 있습니다.
- fetchJoin을 활용하지 않았을 때는 사진 2번에서 각각의 프록시 상태에 값들을 N+1만큼 조회하는 것을 볼 수 있습니다.
- 한 대상에 여러 fetchJoin을 사용되게 되면 MultipleBagFetchException이 발생합니다.
@Test
void whenUsedFetchJoin() {
List<Post> posts = jpaQueryFactory.selectFrom(post)
.leftJoin(post.postImages)
.fetchJoin()
.fetch();
for (Post post : posts) {
post.getPostImages().stream()
.map(PostImage::getImageUrl)
.forEach(System.out::println);
}
BatchSize 활용
- yml에 batch_size를 사용하여 처리할 수 있습니다. 데이터베이스로부터 데이터를 가져올 때 한 번에 가져오는 최대 행 수를 나타내며 SQL의 IN 절로 처리가 됩니다. 여기에서 배치는 데이터베이스에서 한 번에 가져오는 데이터의 묶음을 의미합니다.
- fetchJoin을 사용하지 않고 조회를 해도 기준이 되는 엔티티인 Post를 조회하고 1:N 관계인 Image를 IN 절을 통해 가져오는걸 확인 할 수 있습니다.
//application.yml
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
//batch
@Test
@Transactional
void whenUsedBatchSize() {
List<Post> posts = jpaQueryFactory.selectFrom(post)
.leftJoin(post.postImages)
.fetch();
for (Post post : posts) {
post.getPostImages().stream()
.map(PostImage::getImageUrl)
.forEach(System.out::println);
}
}
이렇게 QueryDSL에서 N+1문제를 해결하는 방법을 살펴보았습니다. 다음 글에서는 QueryDSL을 사용하여 한 대상의 여러 1:N 관계를 효과적으로 조회하는 방법에 대해 알아보고, 1:N 관계에서 페이징할 때 발생할 수 있는 카테시안 곱 문제에 대한 해결 방법도 함께 다뤄보겠습니다.
'Spring' 카테고리의 다른 글
[JPA] JPQL개념과 사용법 패턴 정리 (0) | 2024.02.22 |
---|---|
[JPA] QueryDSL의 페이징 카테시안 곱 문제 해결 방법 (1:N Pagination) (0) | 2024.02.16 |
[JPA] JPA의 연관관계 예제 정리 (1) | 2024.02.05 |
[JPA] 영속성 컨텍스트의 특징과 프록시 이해하기 (1) | 2024.02.01 |
[JPA] JPA 개념과 기본 설정 가이드 (0) | 2024.01.29 |