티스토리 뷰

문제 상황


oneToMany 관계에서 Fetch Join과 Limit을 같이 사용해 아래와 같은 경고 메시지가 발생했다.

HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory

쿼리 결과를 전부 메모리에 적재한 뒤 Pagination 작업을 어플리케이션 레벨에서 하기 때문에 위험하다는 로그이다. limit에 의해 제한된 개수의 쿼리 결과를 가져올 것이라는 예상과는 다르게 작동하고 있었다. 이처럼, One에서 Many를 fetch 하는 경우 limit절 (Pagination을 위해) 포함 시 원하는 대로 결과가 나오지 않고, 성능상 문제가 발생할 수 있다.

 

문제 상황 재현


문제 상황 재현을 위한 ERD이다. trip, place, trip_place 테이블이 있고, trip_place는 tripId, placeId를 외래키로 갖고 있다. trip과 place는 N:N 관계이므로, trip_place 테이블을 중간에 두어 일대다, 다대일 관계로 풀어주었다.

 

trip 5개와 해당 trip의 place 들을 조회해야하는 요구사항이 있다고 가정하자. 아래는 요구사항을 위해 작성한 쿼리이다. Cursor based Pagination 방식을 사용해 cursorId로 offset을 구현했다.

public List<Trip> findAllFetch(Long cursorId, LocalDateTime cursorDate) {
    return queryFactory.selectFrom(trip)
            .innerJoin(trip.tripPlaces, tripPlace).fetchJoin()
            .innerJoin(tripPlace.place, place).fetchJoin()
            .where(cursorGt(cursorId, cursorDate))
            .limit(5)
            .fetch();
}

실행 후 console 로그를 보니 아래와 같은 경고 로그가 발생했다.  HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory

 

쿼리 결과를 전부 메모리에 적재한 뒤 Limit 작업을 어플리케이션 레벨에서 하기 때문에 위험하다는 로그이다.

1개의 trip은 여러 개의 place를 가질 수 있기 때문에 Join 시 아래와 같이 데이터가 반환된다.

select t.id, t.name, t.meet_place, p.id, p.title, p.address
from trip t
inner join trip_place tp on t.id = tp.trip_id
inner join place p on tp.place_id = p.id

trip 데이터가 중복되어 반환되기 때문에 limit으로 원하는 개수의 trip을 가져올 수 없다. 위와 같이 반환된 데이터에서 limit 3을 한다면 1개의 trip만 조회될 것이다. 따라서, Hibernate는 데이터를 모두 가져온 뒤, JVM 메모리 상에서 필터링을 수행한다. 실제로, 실행된 sql 문을 확인해보니 limit절이 존재하지 않는다.

Hibernate: 
    select
        t1_0.id,
        t1_0.date_time,
        t1_0.meet_time,
        t1_0.name,
        t2_0.trip_id,
        t2_0.id,
        p1_0.id,
        p1_0.address,
        p1_0.title
    from
        trip t1_0 
    join
        trip_place t2_0 
            on t1_0.id=t2_0.trip_id 
    join
        place p1_0 
            on p1_0.id=t2_0.place_id 
    where
        t1_0.id>?

 

문제 해결


그러면, OneToMany 관계에서 fetch Join과 limit 같이 사용할 수 없는 것인가? 사용 가능하다. 아래와 같이 쿼리를 두개로 나누면 사용 가능하다. 첫번째 쿼리에서는 One에 해당하는 객체의 주식별자를 Pagination 으로 가져온다. 두번째 쿼리에서는 IN 절을 사용해 필터링하고, fetch join을 실행한다. 직접 적용해보자.

public List<Trip> findAllFetch(Long cursorId, LocalDateTime cursorDate) {
        
        // 1. One에 해당하는 객체의 주식별자를 Pagination으로 가져온다.
        List<Long> ids = queryFactory.select(trip.id)
                .from(trip)
                .where(cursorGt(cursorId, cursorDate))
                .limit(5)
                .fetch();

	// 2. IN 절을 사용해 필터링하고, fetch join을 실행한다.
        return queryFactory.selectFrom(trip)
                .innerJoin(trip.tripPlaces, tripPlace).fetchJoin()
                .innerJoin(tripPlace.place, place).fetchJoin()
                .where(trip.id.in(ids))
                .fetch();
}

실행된 sql은 아래와 같다. 쿼리 개수는 1개 늘었지만, limit 절이 정상작동해 필요한 개수의 데이터만 가져올 수 있게 되었다.

Hibernate: 
    select
        t1_0.id 
    from
        trip t1_0 
    where
        t1_0.date_time>? 
        and (
            t1_0.id>? 
            and t1_0.date_time=? 
            or t1_0.date_time>?
        ) limit ?
Hibernate: 
    select
        t1_0.id,
        t1_0.date_time,
        t1_0.meet_place,
        t1_0.name,
        t2_0.trip_id,
        t2_0.id,
        p1_0.id,
        p1_0.address,
        p1_0.title,
    from
        trip t1_0 
    join
        trip_place t2_0 
            on t1_0.id=t2_0.trip_id 
    join
        place p1_0 
            on p1_0.id=t2_0.place_id 
    where
        t1_0.id in (?,?)

결론


OneToMany 관계에서 fetch join과 limit을 함께 사용해 Pagination 하는 경우 쿼리 결과를 전부 메모리에 적재한 뒤 limit 작업을 어플리케이션 레벨에서 한다. 성능상 문제가 될 수 있으므로, pagination과 fetch join 쿼리를 분리해 limit절이 실행되도록 쿼리를 개선해보자.

 

References

https://github.com/pci2676/Spring-Data-JPA-Lab/tree/master/fetch-limit

https://velog.io/@cksdnr066/WARN-firstResultmaxResults-specified-with-collection-fetch-applying-in-memory

'Server > Spring' 카테고리의 다른 글

[Spring] HikariCP 에러 핸들링  (1) 2023.04.19
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
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
글 보관함