-
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는 아래 이미지와 같습니다.
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/해당 블로그에 적힌 바로
-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만 가져오도록 하였습니다.