아니 대리님 N+1 문제가 발생하길래 Fetch Join하고 EntityGraph 어노테이션을 사용했는데 서버가 죽었어요
📌 개요
N+1 문제를 효과적으로 해결하는 방법은 크게 세 가지가 있는데, Fetch Join
, @EntityGraph
, Batch Size
이다. 일반적인 경우에서 이들 중 뭐가 더 좋냐고 하면 Fetch Join
과 @EntityGraph
가 더 좋다고 할 수 있다. 발생하는 쿼리의 개수 관점에서 보면 그렇다는 것이다. Fetch Join
과 @EntityGraph
는 1개의 쿼리가 발생하는 반면 Batch Size
는 , 최소 2개의 쿼리가 발생한다. 설정된 size에 따라 그 이상이 될 수 있다.
단, 대량의 데이터에서 페이징과 Fetch Join
또는 @EntityGraph
를 함께 사용하는 경우 OutOfMemoryError
가 발생할 수 있다.
📌 원인
일대다 관계에서 Fetch Join
과 Pageable
을 같이 사용하면 Hibernate
는 DB 레벨에서 LIMIT
또는 OFFSET
을 적용할 수 없다. 왜냐하면 SQL의 JOIN
결과와 JPA가 만들어야 하는 객체 그래프의 구조가 다르기 때문이다.
team_id | team_name | member_id | member_name |
---|---|---|---|
1 | TeamA | 101 | Member1 |
1 | TeamA | 102 | Member2 |
… | … | … | … |
1 | TeamA | 200 | Member100 |
2 | TeamB | 201 | Member101 |
… | … | … | … |
Team
과 Member
엔티티가 일대다 관계라고 하자. SQL JOIN
을 하게 되면 카테시안 곱이 발생하여 각 Team
의 데이터가 가진 Member
의 수만큼 중복되어 나타나게 된다. 반면 JPA 객체 그래프는 Page<Team>
을 기대한다. 중복되지 않는 Team
객체와 속한 members
컬렉션을 온전히 가져야 한다.
이 상황에서 LIMIT 10
을 적용하개 되면, TeamA
와 속한 10명의 Member
만 조회되게 된다. 이를 JPA 객체 그래프로 변환하게 되면 단 하나의 Team
객체만 생성되며, TeamA
에 속한 members
도 온전히 가지지 못하게 된다.
이러한 데이터 정합성 문제를 피하기 위해 Hibernate는 메모리에 모든 데이터를 올린 후 페이징을 하는 방법을 선택한다. 전체 데이터를 메모리에 올리려고 하다 보니 OOM
이 발생하는 것이다.
📌 해결 방법
페이징과 Batch Size
를 같이 사용하면 된다.
1
2
3
4
5
6
spring:
jpa:
properties:
hibernate:
query:
fail_on_pagination_over_collection_fetch: true
아니면 근본적인 해결 방법은 아니지만, fail_on_pagination_over_collection_fetch
를 true
로 설정하면 데이터를 메모리에 페이징으로 가져오려고 할 때 에러를 발생시키도록 한다.
📌 참고
https://jojoldu.tistory.com/737