ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA -ToMany 대상으로 Fetch Join, Paging 문제점
    Spring/Trouble Shooting 2024. 11. 10. 19:31

    빠른 정리

    N+1 문제를 해결하기 위해 EntityGraph를 통해 Fetch Join을 진행했는데, 결과적으로 N+1 문제는 해결하였으나 Paging이 메모리에서 수행되는 문제가 발생하였습니다.

    원인은 -ToMany 관계를 조회할 경우 조회시 데이터의 갯수가 실제 데이터의 갯수랑 달라져서 Paging처리를 메모리에서 진행하게 되는 것이였습니다.

    따라서 해결책으로 -ToOne 관계의 경우 FetchJoin을 통해 값을 가져오고 -ToMany 관계는 BatchSize를 통해 객체 조회시 추가 쿼리를 통해 값을 조회하도록 하였습니다.

     

    문제 기록

    일단 ERD는 아래 이미지와 같습니다.

    ERD

    event -> event_type 은 N:1

    event 와 hash_tag는 N:M 관계로 중간에 event_hash_tag 라는 테이블을 두어 매핑을 해 둔 상태입니다.

    event에 대해 페이징을 처리하기 위해

    public interface EventRepository extends JpaRepository<Event, Long> {
    
        Page<Event> findAll(Pageable pageable);
    }

    페이징 조회 JPA 메소드로 작성 후

    public record EventDto(
            Long id,
            String eventTitle,
            Integer price,
            Integer availableSlot,
            EventTypeDto eventType,
            List<HashTagDto> hashTags
    ) {
        public static EventDto from(Event event) {
            return new EventDto(
                    event.getId(),
                    event.getTitle(),
                    event.getPrice(),
                    event.getAvailableSlot(),
                    EventTypeDto.from(event.getEventType()),
                    event.getHashTags().stream()
                            .map(HashTagDto::from)
                            .toList()
            );
        }
    }

    DTO로 매핑시 

    Hibernate: 
        select
            e1_0.id,
            e1_0.available_slot,
            e1_0.created_at,
            e1_0.event_type_id,
            e1_0.price,
            e1_0.title,
            e1_0.updated_at 
        from
            event e1_0 
        limit
            ?, ?
    //...
    Hibernate: 
        select
            eht1_0.event_id,
            eht1_0.id,
            eht1_0.created_at,
            ht1_0.id,
            ht1_0.created_at,
            ht1_0.tag_name,
            ht1_0.updated_at,
            eht1_0.updated_at 
        from
            event_hash_tag eht1_0 
        left join
            hash_tag ht1_0 
                on ht1_0.id=eht1_0.hash_tag_id 
        where
            eht1_0.event_id=?

    많이 생략되었지만 페이징 처리를 하여 9개의 데이터 List만 조회하는 상황에서 50개 가량의 N+1 쿼리가 발생하는 상황이 발생하였습니다.

     

    그렇다면 Fetch Join을 통해서 "hash_tag"와 "event_type" 모두를 가져오면 되는거 아닌가?
    라고 생각해서

    @EntityGraph(attributePaths = {"eventHashTags", "eventHashTags.hashTag", "eventType"})
    Page<Event> findAll(Pageable pageable);

    @EntityGraph를 통해 각 객체를 미리 조회하도록 하여 조회를 진행했습니다.

    그 결과

    Hibernate: 
        select
            e1_0.id,
            e1_0.available_slot,
            e1_0.created_at,
            eht1_0.event_id,
            eht1_0.id,
            eht1_0.created_at,
            ht1_0.id,
            ht1_0.created_at,
            ht1_0.tag_name,
            ht1_0.updated_at,
            eht1_0.updated_at,
            et1_0.id,
            et1_0.created_at,
            et1_0.event_type_name,
            et1_0.updated_at,
            e1_0.price,
            e1_0.title,
            e1_0.updated_at 
        from
            event e1_0 
        left join
            event_hash_tag eht1_0 
                on e1_0.id=eht1_0.event_id 
        left join
            hash_tag ht1_0 
                on ht1_0.id=eht1_0.hash_tag_id 
        left join
            event_type et1_0 
                on et1_0.id=e1_0.event_type_id
    Hibernate: 
        select
            count(e1_0.id) 
        from
            event e1_0

    Left Join 쿼리를 통해 값을 조회하는 것을 볼 수 있습니다만

     

    2024-11-10T19:17:25.487+09:00 WARN 48987 --- [nio-8080-exec-2] org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory

    메모리 내에서 페이징 처리를 하고 있다는 WARN 로그가 발생했습니다.

    이에 대한 이유를 찾아봤는데
    https://junhyunny.github.io/spring-boot/jpa/jpa-fetch-join-paging-problem/

     

    JPA Fetch 조인(join)과 페이징(paging) 처리

    <br /><br />

    junhyunny.github.io

    해당 블로그에 적힌 바로

     

    -ToOne에 대해서는 데이터의 갯수의 차이는 없지만

    -ToMany에 대해서는 데이터의 개수 차이가 발생하기에
    페이징이 DB에서 수행되는 것이 아니라 메모리에서 직접 처리한다는 것을 알게 되었습니다.

     

    따라서 event 객체에서 hash_tag 테이블을 가져오기 위해 매핑된 OneToMany 관계인 event_hash_tag 조회 방식을

    @BatchSize(size = 100)
    @OneToMany(mappedBy = "event", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true)
    private List<EventHashTag> eventHashTags = new ArrayList<>();

    @BatchSize를 설정하여 조회하도록 하였고

    @EntityGraph(attributePaths = {"eventType"})
    Page<Event> findAll(Pageable pageable);

    @EntityGraph를 통해서는 -ToOne 관계인 event_type만 가져오도록 하였습니다.

    댓글

Designed by black7375.