-
[JPA] Fetch Join 할 때 MultipleBagFetchException 해결하는 법Server/JPA 2021. 11. 24. 14:53728x90반응형
Spring Data JPA에서 MultiBagFetchException 해결하기이번 글에서는
JPA를 사용한지 얼마 안된 초보자 입장에서 겪은 어려움을 공유하고 어떻게 해결해나갔는지 공유하며 생각을 정리해보려 합니다.
위의 화면에 대한
API를 만들어야 하는 상황입니다. 위의 뷰에서는내가 팔로우 하고 있는 사람이 작성한 게시글이 나타나야 합니다.
View를 보면서DB Table을 간단하게 같이 보겠습니다.하나의 게시글 : 여러 장의 사진 (1 : N)하나의 게시글 : 여러 개의 가게 평가 해시 태그 (1 : N)하나의 게시글 : 같이 가게를 간 사람들의 닉네임 (1 : N)
위의 조건에 해당하는 데이터를 다 가져와야 하나의
Post(게시글)을 보여줄 수 있습니다. 즉,JOIN이 꽤 많이 들어가는 작업이 필요한데요. 그래서 일단Post Entity를 먼저 살펴보겠습니다.
Post Entity를 보면 위에서 말한 것처럼Post와@OneToMany관계인Entity가 3개 존재하는 것을 볼 수 있습니다. (참고로 모든 참조 관계는LAZY Loading입니다.)
원래는
내가 팔로우하고 있는 유저들이 작성한 게시글을 들고와야 하지만, 지금은 이게 중요한 건 아니기에findAll()로 전체 게시글을 다 가져오는 것으로 예제 코드를 작성하였습니다.
그리고
DTO내부에서from메소드의 역할을Entity -> DTO로 변환하는 작업을 진행합니다. 즉,현재 모든 FetchType은 Lazy Loading이고,DTO from메소드를 통해서Entity -> DTO로 변환하고 있는 상황입니다. 이런 상태에서 실행했을 때 쿼리가 몇 번 실행되는지 알아보겠습니다.

쿼리가 나가는 상황을 요약하면 아래와 같습니다.
post.findAll() 쿼리 실행 => (id 1, 2, 3) 반환 => 쿼리 1번 수행post에 해당하는 with_user 가져오기=>쿼리 3번 실행post에 해당하는 post_image 가져오기=>쿼리 3번 실행post에 해당하는 post_evaluate 가져오기=>쿼리 3번 실행
위와 같이 쿼리가 실행되고 있습니다. 즉, 게시글이 현재는 3개여서 3번씩 반복이 되었지만, 게시글이 훨씬 더 많다면 훨씬 더 많은 쿼리가 실행되었을 것입니다.

그래서
fetch join을 사용하여 위에서 발생하는N + 1문제를 해결하기 위해 사용하였습니다. 그런데 위와 쿼리를 보면 빨간 네모의fetch join이@OneToMany관계를 가지고 있고@OneToMany관계 2번을fetch join으로 한번에 사용하고 있는 것을 볼 수 있습니다.
이 상태로 실행하면
MultipleBagFetchException이 발생하는 것을 볼 수 있습니다. 이렇게N + 1문제를 해결하기 위해fetch join을 사용하면MultipleBagFetchException을 만나게 됩니다. 즉, 이러한 문제는2개 이상의 OneToMany 자식 테이블에 Fetch Join을 사용했을 때발생합니다.JPA에서
Fetch Join의 특징은 아래와 같습니다.OneToOne, ManyToOne는 몇개든 사용 가능합니다.ManyToMany, OneToMany는 1개만 사용 가능합니다.
그래서 이러한 문제를 해결하기 위해서는 아래와 같은 방법들이 존재합니다.
자식 테이블 하나만 Fetch Join을 걸고 나머지는 Lazy Loading모든 자식 테이블을 다 Lazy Loading으로(위에서 보았던 예제)Fetch Join을 나누어서 실행한 후에 조합하기
하지만 모든 방법 다 데이터가 많아진다면
Lazy Loading부분에서 쿼리가 정말 많이 나간다면 성능 이슈가 생길 방법들이라고 생각합니다.
그래서 저는
첫 번째 방법이 그나마 낫다고 생각해서Post - User - PostEvaluates만Fetch Join을 사용해서 가져오고 나머지PostImage,UserWith는Lazy Loading으로 설정하고 이 부분에선N + 1문제를 안고 가려고 이것도 시도해보았습니다. (Post - User는 N : 1이기 때문에 같이 Fetch Join을 사용할 수 있습니다.)
여기서
Fetch Join을 통해서Post - User - Evaluate를 한번에 가져온 것을 볼 수 있습니다.
그런데
Fetch Join으로 가져오지 못한 나머지 부분은Post 수 만큼 위의 쿼리를 반복하게 됩니다.즉,Lazy Loading부분에서는Entity- > DTO로 변환하면서N + 1쿼리를 안고 가야 합니다.Fetch Join도DB 서버에 부하를 주는 것일 수 있기 때문에 이것도 하나의 방법일 수 있습니다. 하지만 그럼에도N + 1쿼리를 안고가는건 조금 부담이 될 수 있어 좀 더 좋은 방법을 정리해보려 합니다.Hibernate default_batch_fetch_size 사용하기이건 김영한님 JPA 강의 에서도 소개해주신 적이 있고, Jojoldu님이 아주 좋은 내용을 정리 해주신 것 이 있습니다.

다시 한번
N + 1문제를 정리하고 가면 위의 그림 처럼하나의 게시글이 여러 장의 사진, 여러 개의 해시 태그를 가지다 보니 Post(부모 엔티티)의 Key를 자식 엔티티들을 조회로 사용하다 보니N + 1쿼리가 발생하는 상황인데요.
default_batch_fetch_size를 사용하면 기존에 쿼리를 나눠서 실행해서 발생했던N + 1문제를 해당 옵션에 지정된 수 만큼IN절에 부모 키를 사용해서 한번에 가져오기 때문에 성능 향상을 할 수 있습니다.
spring: jpa: show-sql: true properties: hibernate.default_batch_fetch_size: 1000 hibernate: format_sql: true javax: persistence: sharedcache: mode: ENABLE_SELECTIVE generate-ddl: true hibernate: ddl-auto: updateJPA 관련 설정
application.yml파일 입니다.
그리고 위에서 보았던
자식 테이블 하나만 Fetch Join을 걸고 나머지는 Lazy Loading예시 코드 그대로default_batch_fetch_size설정만 해놓고 실행하면 위와 같이IN을 사용해서 쿼리가 실행되는 것을 볼 수 있습니다. 즉,default_batch_fetch_size옵션을 사용하지 않았다면 게시글이 100개만 되어도 300번, 300번 총 600번 쿼리가 실행되었을 것입니다.그런데 현재
default_batch_fetch_size를 1000으로 주었기 때문에 이번엔2번쿼리로 다 가져올 수 있습니다. 엄청난 차이가 있는 것을 볼 수 있습니다.Tip)
보통 옵션값을 1,000 이상 주지는 않습니다.
in절 파라미터로 1,000 개 이상을 주었을때 너무 많은 in절 파라미터로 인해 문제가 발생할수도 있기 때문입니다.
지금 옵션은 1000으로 두었기 때문에 Store가 1000개를 넘지 않으면 단일 쿼리로 수행 된다는 장점도 있습니다.Tip)
같은 방법으로 Fetch 적용시 발생하는 페이징 문제도 동일하게 해결됩니다.
1 : N 관계에서의 페이징 문제는 Join으로 인해 1에 대한 페이징이 정상작동 하지 않기 때문입니다.Reference반응형'Server > JPA' 카테고리의 다른 글
[Spring] Spring Data JPA에서 Auditing 사용하는 법 (0) 2021.11.09 [Spring] Spring Data JPA에서 Paging 간단하게 구현하는 법 (3) 2021.11.08