-
[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: update
JPA 관련 설정
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