Java/스프링 Data JPA

JPA ) 다대다 관계 목록 조회 fetch & paging

하이방가루 2025. 1. 3. 13:49
728x90
반응형

다음과 같이 동호회(Club)와 회원(Member) 엔티티가 있다.

// 동호회
@Entity
@Getter
@Setter
public class Club {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "club_id")
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "club", fetch = FetchType.LAZY)
    private List<ClubMembers> members = new ArrayList<>();
}

// 회원
@Entity
@Getter
@Setter
pubic class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
    private List<ClubMembers> clubs = new ArrayList<>();
}

@Entity
@Getter
@Setter
public class ClubMembers {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = Fetch.LAZY)
    private Club club;
    
    @ManyToOne(fetch = Fetch.LAZY)
    private Member member;
}

 

회원은 여러 동호회에 가입할 수 있고, 동호회도 여러 회원을 받을 수 있다.

 

클럽 목록 조회 무한스크롤 페이지를 위하여 다음과 같이 JPQL를 작성한다면,

@Query("SELECT c FROM Club c"
        + " JOIN FETCH c.members cm"
        + " JOIN FETCH cm.member m"
        + " WHERE c.id < :lastId"
        + "     AND c.name like %:keyword%")
List<Club> findFetchWithPaging(@Param("lastId") Long lastId, @Param("keyword") String keyword, Pageable pageable);

원하는 결과는 얻을 수 있지만,

언젠가는 "HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!" 라는 경고를 발견하게 될 것이다.

 

이는 생성된 쿼리를 보면 알 수 있다.

select
        club0_.club_id as club1_23_0_,
        members1_.id as club1_24_1_,
        members2_.member_id as member_1_18_2_,
        club0_.name as name11_23_0_,
        members1_.member_id as member_6_24_1_,
        members1_.club_id as open_api7_24_0__,
        members1_.id as open_api1_24_0__,
        member2_.name as name_14_18_2_ 
    from
        club club0_ 
    inner join
        club_members members1_ 
            on club0_.club_id=members1_.club_id 
    inner join
        member member2_ 
            on members1_.member_id=member2_.member_id 
    where
        club0_.club_id < ?
        and (
            club0_.name like concat('%', ?, '%')
        ) 
    order by
        club0_.club_id desc

쿼리를 보면 페이징을 위한 limit 쿼리가 없다.

그렇다면 어떻게 원하는 결과 수만큼 가져오는 것일까?

 

생각해 보면 동호회에 회원 정보를 fetch 하기 위해서는 동호회에 소속된 멤버만큼 쿼리 결과 Row 수가 늘어날 것이기 때문에 limit 을 사용하면 원하는 만큼 데이터를 불러올 수 없게 된다.

이 때문에  Hibernate 에서는 조건에 맞는 모든 결과를 불러와서 메모리에 올려둔 후에 이를 정리하여 사용자가 원하는 갯수만큼의 객체를 반환한다.

따라서 많은 메모리를 사용하게 될 수 있고, 실사용 중 OOM(Out Of Memory)이 발생할 수 있기 때문에 위와 같이 경고가 나오는 것이다.

 

이를 미리 깨닫고 방지하기 위해서 메모리에서 페이징 처리하는 것을 사전에 차단해 주는 옵션을 사용하면 좋다.

spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetch=true

출처 : https://jojoldu.tistory.com/737

 

그렇다면 어떻게 해야 원하는 만큼의 객체와 데이터를 받아올 수 있을까?

"HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!"를 구글링하여 가장 먼저 나오는 stackoverflow 결과를 참고하면

https://stackoverflow.com/questions/11431670/how-can-i-avoid-the-warning-firstresult-maxresults-specified-with-collection-fe

두 개의 쿼리로 나누어 결과를 도출할 수 있다.

먼저 조건의 맞는 동호회의 id만을 가져오고, 해당 id에 fetch하여 데이터를 불러오는 것이다.

@Query("SELECT c.id FROM Club c"
        + " WHERE c.id < :lastId"
        + "     AND c.name like %:keyword%")
List<Long> findIdsWithPaging(@Param("lastId") Long lastId, @Param("keyword") String keyword, Pageable pageable);

@Query("SELECT distinct(c) FROM Club c"
        + " JOIN FETCH c.members cm"
        + " JOIN FETCH cm.member m"
        + " WHERE c.id IN :ids")
List<Club> findFetchByIds(@Param("ids") List<Long> ids, Sort sort);

 

참고로 distinct가 생략되면 객체가 중복되어 결과를 준다.

// 중복 제거 조건 org.hibernate.hql.internal.ast.QueryTranslatorImpl::list::374
final boolean needsDistincting = (
    query.getSelectClause().isDistinct() ||
           getEntityGraphQueryHint() != null ||
           hasLimit )
    && containsCollectionFetches();

 

728x90
반응형