본문 바로가기

Programming/JPA

여러 개의 컬렉션 FetchJoin 시 성능 최적화

현재 도메인은 Post, Comment, Postlike가 있고,

Post - CommentOneToMany

Post - PostLike 역시 OneToMany 관계이다.

image

이제 post.commentspost.likes를 모두 포함해 List<Post>를 가져올 것이다.

 

image

이렇게 두 컬렉션에 fetchJoin을 걸고,

 

해당 메소드를 사용하는 Service 로직이 있다.

image

이 Service 메소드에 대한 테스트 코드를 실행해보면,

image

이렇게 comments가 likes에 영향을 받는 문제가 생긴다.

image

 

 

문제의 원인은 fetchJoin이었다.

 

2개 이상의 컬렉션을 페치조인하면 MultipleBagFetchException이 발생한다.

라는 사실을 놓치고 있었다.

 

MultipleBagFetchException이 발생하지 않았기 때문이다. 🤯

요상했다. 왜 남들은 다 발생하는데 왜 안 터지지..? 한참 삽질하다가 문제를 찾았다.

 

우선, MultipleBagFetchException이 발생하는 원인부터 알아보자면, 이 친구는 중복을 방지하기 위해 터진다.

즉, join된 두 컬렉션 간의 카테시안 곱이 발생함을 방지하기 위해서이다. 만약, 지금처럼 댓글이 1개, 좋아요가 2개인 경우에는 1 * 2 = 2이므로 성능에 커다란 영향을 미치지 않겠지만, 댓글이 200개, 좋아요가 300개라면 무려 60,000개의 결과가 나오므로 엄청난 성능 저하가 발생할 것이다.

그런데!

Set은 중복을 허용하지 않는다. 따라서 카테시안 곱이 발생할 일이 없고, MultipleBagFetchException도 터지지 않은 것이다!

 

그리고 도메인을 보면,

post.commentsList 형태의 자료구조

post.likesSet 형태의 자료구조

 

그러니 결국 List, List를 fetchJoin하면 해당 익셉션이 발생하겠지만, 현재처럼 List, Set 또는 Set, Set을 함께 fetchJoin하는 상황에선 터지지 않는다.

해결방법 1. List를 Set으로 변경하기

앞서 말한대로 Set으로 바꾼다면 해결된다. 정확히 말하면 해결은 아니고, 문제를 피하는 방법이다.

하지만 우리는 자료구조의 변경을 원하지 않는다.

왜?

현재 Comment는 순서 보장이 중요할 뿐더러 Set은 인덱스를 지원하지 않아 get() 이렇게 한 번에 가져오지 못하고 iterator를 써야 한다. 치명적이다.

해결방법 2. @BatchSize 설정 ✅

문제를 해결하기 위해 채택한 방법이다.

기존의 컬렉션 fetchJoin은 가장 데이터가 많은 1군데에만 걸어두고,

이런 식으로 컬렉션 위에 @BatchSize를 정해주면 N+1 문제가 발생하는 대신, 한꺼번에 지정한 size만큼 IN 절로 조회해온다.

image

전역적으로 application.properties(또는 yml)에 spring.jpa.properties.hibernate.default_batch_fetch_size=100 이런 식으로 설정하는 방법도 있다.

 

위에서 어떤 방법을 사용하던 상관 없이, 테스트의 경우에는

@TestPropertySource(properties = "spring.jpa.properties.hibernate.default_batch_fetch_size=100")

를 클래스 위에 붙여주어야 한다.

 

Q. 이상적인 size?

이러한 batch fetch가 활성화되었을 때 Hibernate는 많은 쿼리를 준비하게 되고, 이 과정에서 많은 메모리를 잡아먹게 된다. 따라서 전역적으로 적용하는 batch_size는 10, 50 같은 작은 숫자로 하고, 커다란 batch 작업이 필요할 때에는 @BatchSize를 이용해 큰 값을 설정해주는 것이 이상적이다.

 

참고

https://vladmihalcea.com/hibernate-multiplebagfetchexception/
https://jojoldu.tistory.com/457
https://stackoverflow.com/questions/21162172/default-batch-fetch-size-recommended-values