티스토리 뷰

에러 메세지

org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.beside.backend.domain.entitiy.wisesaying.WiseSaying.emotionWiseSayingsWords, com.beside.backend.domain.entitiy.wisesaying.WiseSaying.wiseSayingBookmark]; nested exception is java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.beside.backend.domain.entitiy.wisesaying.WiseSaying.emotionWiseSayingsWords, com.beside.backend.domain.entitiy.wisesaying.WiseSaying.wiseSayingBookmark]

 
 
 
 

MultipleBagFetchException

N+1 문제를 방지하기 위해 Fetch Join 을 사용하다 보면 자주 만나는 에러이다.
 

N+1 

1. 처음에 N개의 엔티티를 모두 가져오기 위해 부모 엔티티에 대한 1개의 쿼리가 발생하고,
2. 부모 엔티티의 자식 엔티티를 가져오기 위해 부모 1개에 대한 자식 N개의 쿼리가 추가로 나가게 된다.

엄청나게 많은 쿼리가 나가면 발생할 성능 이슈를 Fetch Join을 사용하여 해결한다.

 

Fetch Join

연관된 엔티티나 컬렉션을 JPQL을 호출하는 시점에 같이 조회하여
한 번에 엔티티를 모두 조회하는 조인 방법
 
 

MultipleBagFetchException 발생하는 이유

  • 하나의 Entity에 fetch = FetchType.EAGER 타입을 2개 이상 쓰면 발생
  • 동시에 여러 개의 컬렉션을 로딩할 때 발생하는 문제

 

Bag(Multiset) 이란?

Set과 같이 순서가 없고,
List와 같이 중복을 허락하는 자료구조
 
하이버네이트는 컬렉션으로 참조하고 있는 대상을 추적하고 관리하기 위해 내부적으로 PersistentBag 타입 객체로 실제 인스턴스를 래핑하여 사용한다.
 
자바 컬렉션에는 Bag을 지원하지 않기 때문에, 하이버네이트는 Bag을 List로 활용한다.
 
 
하이버네이트 입장에서는 다량의 데이터를 한번에 불러오는데
중복도 허용하고, 순서도 보장하지 않는 BagType 데이터를 여러개의 패치조인으로 많이 불러온다면 데이터 정합성에 문제가 될 수 있다고 판단할 수 있을 거 같다.
 

  • Set : 중복을 허용 X, 순서를 보장 X 컬렉션
  • List : 중복을 허용 O , 순서를 보장 O  컬렉션

 
 

에러 발생 원인

fun findAllByEmotionIdAndWordIds(emotionId: Long, wordIds: List<String>): List<WiseSaying> {
    return from(wiseSaying)
        .leftJoin(wiseSaying.emotionWiseSayingsWords, emotionWiseSayingsWords).fetchJoin()
        .leftJoin(emotionWiseSayingsWords.emotion, emotion).fetchJoin()
        .leftJoin(wiseSaying.wiseSayingBookmark, wiseSayingBookmark).fetchJoin()
        .where(emotion.id.eq(emotionId), emotionWiseSayingsWords.wordId.`in`(wordIds))
        .distinct()
        .fetch()
}

문제가 된 조회하려는 쿼리의 entity
 
- emotionWiseSayingsWords
- wiseSayingBookmark
 
두개는 WiseSaying과 ToMany 매핑 관계이다.
 

ToMany 관계의 엔티티들을 동시에 .fetchJoin() 하고 있었다.

 
 
 

JPA에서 Fetch Join의 조건

  • @ToOne : 개수 제한 X
  • @ToMany (1:N 의 N) : 1개만 가능

 
 

에러 해결법

  1. 컬렉션의 FetchType = Lazy로 변경
  2. 컬렉션 중 하나를 Set으로 변경
  3. Join Fetch 혹은 Batch Join 사용
  4. Batch Size 설정

 
 
 

default_batch_fetch_size 혹은 @BatchSize 설정

default_batch_fetch_size 를 yml에 작성해 프로젝트 전체에 적용해도 되고,
원하는 엔티티나 컬럼에 @BatchSize를 적용해도 된다.
 
 
https://coding-duck1.tistory.com/entry/JPA-Batch-Size-N1%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0

JPA Batch Size : N+1문제 해결

@BatchSize 연관된 엔티티를 조회할 때 지정한 size 만큼 where 조건의 in 절으로 조회한다. 1:N 관계의 Entity 정의 정의 조건 : 1명의 멤버는 여러 개의 주문을 가질 수 있다. Members (1) : Orders (N) * 부모 clas

coding-duck1.tistory.com

 
ToMany 관계에서 부모-자식 엔티티간 조회 쿼리는 이러하다.

fetchJoin 에서 자식 엔티티를 조회할 때, 부모의 키값 `wise_saying_id` 를 where 조건으로 하나하나 조회한다. 
 
 
이 문제를 default_batch_fetch_size 혹은 @BatchSize 를 사용하면
where in(부모키, 부모키, 부모키, .....) 절에 한번에 가져올 수 있게 된다.
 

  • default_batch_fetch_size : 지정된 수만큼 in절에 부모 Key를 사용
  • 지정된 옵션의 수 (현재 100으로 적용) 가 넘어가면, 100개 단위로 where의 in절에 부모 Key를 넣어서 자식 엔티티들을 조회한다.
  • 따라서 쿼리 수행수 1/100

 
ToMany 가 있는 엔티티에 @BatchSize(size = 100) 어노테이션 추가!

@BatchSize(size = 100)
@Entity
@Table(name = "wise_saying")
class WiseSaying(
    @Column(nullable = true, length = 100)
    var author: String? = null,
    @Column(nullable = true, length = 500)
    var message: String,
) : AuditTime() {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null

    @Column(nullable = false)
    var deleted: Boolean = false

    @OneToMany(mappedBy = "wiseSaying", fetch = FetchType.LAZY)
    var emotionWiseSayingsWords = mutableListOf<EmotionWiseSayingsWords>()

    @OneToMany(mappedBy = "wiseSaying", fetch = FetchType.LAZY)
    var wiseSayingBookmark = mutableListOf<WiseSayingBookmark>()
}
    fun findAllByEmotionIdAndWordIds(emotionId: Long, wordIds: List<String>): List<WiseSaying> {
        return from(wiseSaying)
            .leftJoin(wiseSaying.emotionWiseSayingsWords, emotionWiseSayingsWords).fetchJoin()
            .leftJoin(emotionWiseSayingsWords.emotion, emotion).fetchJoin()
            .leftJoin(wiseSaying.wiseSayingBookmark, wiseSayingBookmark)
            .where(emotion.id.eq(emotionId), emotionWiseSayingsWords.wordId.`in`(wordIds))
            .distinct()
            .fetch()
    }

fetchJoin() 은 둘 중에 하나 제거한다.
 
 
 
 
 
 
 
 
 
 

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함