JPA에서의 N+1 문제
JPA와 JPQL
JPQL은 Java Persistence Query Language의 약자로, DB 테이블이 아니라 엔티티의 객체를 대상으로 검색하는 객체 지향 쿼리
When? 언제 발생하는건가요?
→ JPA Repository를 활용해 인터페이스 메소드를 호출 할 때(Read 시)
Who? 누가 발생시키나요?
→ 1:N 또는 N:1 관계를 가진 엔티티를 조회할 때 발생
How? 어떻게 하면 발생되나요?
→ JPA Fetch 전략(Fetch Type)이 EAGER 전략으로 데이터를 조회하는 경우
→ JPA Fetch 전략(Fetch Type)이 LAZY 전략으로, 전체 데이터를 가져온 이후 연관 관계인 하위 엔티티를 사용할 때 다시 조회하는 경우
Why? 왜 발생하나요?
→ JPA Repository로 find 시 실행하는 첫 쿼리에서 하위 엔티티까지 한 번에 가져오지 않고, 하위 엔티티를 사용할 때 추가로 조회하기 때문에.
→ JPQL은 기본적으로 글로벌 Fetch 전략을 무시하고 JPQL만 가지고 SQL을 생성하기 때문에.
EAGER(즉시 로딩)인 경우
1) JPQL에서 만든 SQL을 통해 데이터를 조회
2) 이후 JPA에서 Fetch 전략을 가지고(여기서는 즉시 로딩) 해당 데이터의 연관 관계인 하위 엔티티들을 추가 조회(LAZY - 지연 로딩 발생)
3) 2) 과정으로 N+1 문제 발생함
LAZY(지연 로딩)인 경우
1) JPQL에서 만든 SQL을 통해 데이터를 조회
2) JPA에서 Fetch 전략을 가지지만, 여기서는 지연 로딩이기 때문에 추가 조회는 하지 않음
3) 하지만, 하위 엔티티를 가지고 작업하게 되면 추가 조회하기 때문에 결국 N+1 문제가 발생함
해결방안
@Query("select o from Owner o join fetch o.cats")
List<Owner> findAllJoinFetch();
@EntityGraph(attributePaths = "cats")
@Query("select o from Owner o")
List<Owner> findAllEntityGraph();
위 두 방법의 주의 사항: 중복으로 조회가 되기 때문에 set이나 distinct 사용
BatchSize 사용(SQL의 IN절 설정)
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Owner {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@BatchSize(size=5)
@OneToMany(mappedBy = "owner", fetch = FetchType.EAGER)
private Set<Cat> cats = new LinkedHashSet<>();
}
// QueryDSL로 구현한 예제
return from(owner).leftJoin(owner.cats, cat)
.fetchJoin()
참고자료