JPA N+1문제에 대한 고찰
Updated:
N+1 문제
Entity 간의 연관 관계
의 매핑이 있을 경우, 전체 쿼리를 조회 할 때 (1 : n에서 1) 연관 관계의 엔티티도 조회해야 해서 결국은 전체 쿼리 수(n개 데이터) + 1(전체 조회 한번)
- 발생이유
- jpaRepository에 정의한 인터페이스 메서드를 실행하면
- JPA는 메서드 이름을 분석해서
JPQL을 생성하여 실행
하게 된다 - JPQL은 findAll()이란 메소드를 수행하였을 때
- 해당 엔티티를 조회하는 select * from Owner 쿼리만 실행
- JPQL은
연관관계 데이터를 무시하고 해당 엔티티 기준으로 쿼리를 조회
하기 때문
Fetch Type EAGER vs LAZY
FetchType을 변경하는 것은
단지 N+1 발생 시점을
- LAZY : 연관관계 데이터를 사용하는 시점으로 미룰지,
- EAGER : 아니면 초기 데이터 로드 시점에 가져오느냐에 차이만 있는 것이다
@Test
void exampleTest() {
Set<Cat> cats = new LinkedHashSet<>();
for(int i = 0; i < 10; i++){
cats.add(new Cat("cat" + i));
}
catRepository.saveAll(cats);
List<Owner> owners = new ArrayList<>();
for(int i = 0; i < 10; i++){
Owner owner = new Owner("owner" + i);
owner.setCats(cats);
owners.add(owner);
}
ownerRepository.saveAll(owners);
entityManager.clear();
System.out.println("-------------------------------------------------------------------------------");
List<Owner> everyOwners = ownerRepository.findAll(); // 연관관계 데이터 로드시점 (EAGER)
List<String> catNames = everyOwners.stream().flatMap(it -> it.getCats().stream().map(cat -> cat.getName())).collect(Collectors.toList());
// 연관관계 데이터 사용 시점 (LAZY)
assertFalse(everyOwners.isEmpty());
}
Fetch Join이나, EntityGraph는 둘다 즉시 로딩이다.
- Fetch Join은 JPQL로 직접 작성하는 것
- 실제로는 INNER JOIN문으로 호출됨
- Fetch Type 사용 불가능
- 페이징 쿼리 불가능
- EntityGraph는 LAZY가 아닌 EAGER 로딩
- OUTER JOIN문으로 호출됨
공통점 : JPQL을 사용하여 JOIN문을 호출함
공통 단점 : Catersian Product 곱으로 호출이 되기 때문에, 중복 데이터가 존재하고 성능이 떨어질 우려가 있음
[해결]
Set
을 사용하면 중복된 데이터를 제거할 수 있다.- JPQL에서
distinct을 사용하여 중복된 데이터를 제거
한다.
이유 : https://www.inflearn.com/questions/34797
JPA도 SQL의 row 갯수 만큼 맞춰서 리턴
한다.
그래서 중복된 데이터가 발생하는 것이다.
- 모두 지연 로딩을 설정한다.
- 단,
최적화가 필요한 곳에는 즉시 로딩을 설정
한다. - Fetch Join을 최적화가 필요한 곳에 쓰자
- 만약, Fetch Join을 사용할 수 없다면 BatchSize, EntityGraph 등 다른 방법을 고려해봐야 한다
- 단,
출처 : https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1
Leave a comment