Post

아니 대리님 N+1 문제가 발생하길래 Fetch Join하고 EntityGraph 어노테이션을 사용했는데 서버가 죽었어요

아니 대리님 N+1 문제가 발생하길래 Fetch Join하고 EntityGraph 어노테이션을 사용했는데 서버가 죽었어요

image.png

📌 개요

N+1 문제를 효과적으로 해결하는 방법은 크게 세 가지가 있는데, Fetch Join , @EntityGraph , Batch Size 이다. 일반적인 경우에서 이들 중 뭐가 더 좋냐고 하면 Fetch Join@EntityGraph 가 더 좋다고 할 수 있다. 발생하는 쿼리의 개수 관점에서 보면 그렇다는 것이다. Fetch Join@EntityGraph 는 1개의 쿼리가 발생하는 반면 Batch Size 는 , 최소 2개의 쿼리가 발생한다. 설정된 size에 따라 그 이상이 될 수 있다.

단, 대량의 데이터에서 페이징과 Fetch Join 또는 @EntityGraph 를 함께 사용하는 경우 OutOfMemoryError 가 발생할 수 있다.

📌 원인

일대다 관계에서 Fetch JoinPageable 을 같이 사용하면 Hibernate 는 DB 레벨에서 LIMIT 또는 OFFSET 을 적용할 수 없다. 왜냐하면 SQL의 JOIN 결과와 JPA가 만들어야 하는 객체 그래프의 구조가 다르기 때문이다.

team_idteam_namemember_idmember_name
1TeamA101Member1
1TeamA102Member2
1TeamA200Member100
2TeamB201Member101

TeamMember 엔티티가 일대다 관계라고 하자. 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_fetchtrue 로 설정하면 데이터를 메모리에 페이징으로 가져오려고 할 때 에러를 발생시키도록 한다.

📌 참고

https://jojoldu.tistory.com/737

This post is licensed under CC BY 4.0 by the author.