우주먼지

💡 발생이유


JPA가 JPQL을 분석해서 SQL을 생성할 때 글로벌 Fetch 전략을 참고하지 않고 JPQL 자체만 사용한다.
findAll()이 수행되면 해당 엔티티만을 기준으로 조회 쿼리가 생성되고,
FetchType으로 지정한 객체를 불러오는 시점에 별도외 조회 메서드를 호출하게 됨으로써 발생한다.

 

예시

1. Fetch 전략을 Eager(즉시 로딩)으로 한 경우 발생
2. Fetch 전략을 Lazy(지연 로딩)으로 한 경우, 객체 그래프 탐색 시 발생

 

예시 1의 경우

1. findAll()을 하면 JPQL구문이 생성되고 그 구문을 북석한 SQL이 생성 & 실행됨
2. DB의 결과를 받아 엔티티의 인스턴스를 생성
3. 영속성 컨텍스트에 검색하려는 엔티티와 연관된 엔티티가 있는지 확인
4. 없다면 2번에서 만들어진 엔티티의 인스턴스 개수에 맞게 select 쿼리 발생 (N+1 발생)

 

예시 2의 경우

1. findAll()을 하면 JPQL구문이 생성되고 그 구문을 분석한 SQL이 생성 & 실행됨
2. DB의 결과를 받아 엔티티의 인스턴스를 생성
3. 연관된 객체를 사용하는 시점에 영속성 컨텍스트에 검색하려는 엔티티와 연관된 엔티티가 있는지 확인
4. 없다면 2번에서 만들어진 엔티티의 인스턴스 개수에 맞게 select 쿼리 발생 (N+1 발생)

 

해결방법

1. Fetch Join
2. EntityGraph Annotation
3. Batch Size

 

실무에서 N+1으로 인해 DB가 죽는 문제를 방지하기 위해 사용할 수 있는 방법

1. Fetch 전략을 Lazy 모드로 사용하고, 성능 최적화가 필요한 부분에서는 Fetch Join 사용
2. Batch Size의 값을 1000이하로 설정 (대부분의 DB에서 In 쿼리의 최대 개수 값 = 1000)
3. 연관관계는 필요한 만큼만 연결해서 사용


💡 Fetch Join

  • Repository에서 별도의 메소드를 만들어줘야 함
  • @Query 어노테이션에서 "join fetch 엔티티.연관된엔티티" 구문 생성
  • Inner Join으로 수행됨
public interface TeamRepository extends JpaRepository<Team, Long> {
    @Query("select t from Team t join fetch t.users")
    List<Team> findAllFetchJoin();
}
List<Team> all = teamRepository.findAllFetchJoin();
System.out.println("============== N+1 확인용 ===================");
all.stream()
    .forEach(team -> {team.getUsers().size();}
);

💡 EntityGraph Annotation

  • @EntityGraph(attributePaths = "users") 와 같은 Annotations을 추가함으로써
    Lazy가 아닌 Eager 조회로 가져오도록 설정
  • Fetch Join과 다르게 Join 문이 Outer Join으로 수행 (성능 저하)
  • 둘 다 카테시안 곱이 발생하여 Rich 수 만큼 users의 중복 데이터가 생기는 상황 발생
    • 중복을 제거하기위해 Set 컬렉션 사용 (순서 필요 시 LinkedHashSet 사용)
    • JPQL을 사용하므로 distinct를 사용하여 중복 제거
public interface RichRepository extends JpaRepository<Rich, Long> {
    @EntityGraph(attributePaths = "users")
    @Query("select a from Rich a")
    Set<Rich> findAllJoinFetch();
}

💡Batch Size

  • 이 옵션은 N+1을 해결하는 방법이 아닌, 발생하더라도 select * from user where team_id = ? 이 아닌 select * from user where team_id in (?, ?, ? ) 방식으로 N+1 문제가 발생하게 하는 방법이다.
  • 이렇게하면 100번 일어날 N+1 문제를 1번만 더 조회하는 방식으로 성능 최적화 가능
  • 간단한 yml 수정으로 인해 in 쿼리가 나가게 됨
spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 1000

💡 실무에서 N+1문제로 DB가 죽는 문제 방지법

- 연관관계를 성능 최적화가 쉬운 Lazy로 설정하고 성능 최적화가 필요한 부분에서는 Fetch Join을 사용한다
- 기본적으로 Batch Size의 값을 1000 이하로 설정한다. (대부분의 DB에서 in절의 최대 개수 값은 1000개 이다)
- 연관관계를 끊어 버리는것도 하나의 방법이다

 

'Error Handling > Java & Spring' 카테고리의 다른 글

💡 OSIV 옵션 & 프록시 객체 초기화  (2) 2023.01.14
❌ RedisConnectionFailurException  (0) 2023.01.10
❌ RedisSystemException  (0) 2023.01.10
❌ Circular Dependency  (0) 2023.01.03
❌ DataIntegrityViolation Exception  (0) 2023.01.01
profile

우주먼지

@o귤o

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그