N+1 문제
연관 관계에서 발생하는 문제로 연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 갯수 (N) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 것을 N+1 문제라고 합니다.
- 1번의 쿼리로 N개의 엔티티를 조회(1번)
- 엔티티에 연관된 데이터를 가져오기 위해 추가적인 쿼리를 실행(N번)
예제) User, Order 엔티티
사용자(User) 모두를 조회하기 위해 쿼리를 하나 보냅니다. → 1번
이때 사용자마다 갖고 있는 주문(Order)에 대한 정보를 가져오기 위해 추가적인 쿼리를 날립니다.
User1의 주문,
User2의 주문,
...,
UserN의 주문 → N번
총 N+1개의 쿼리를 날리게 됩니다.
N+1 문제는 즉시로딩 때문인가?
fetch = FetchType.EAGER(즉시로딩)을 적용하면 발생하는 문제라고만 알고있는 경우가 있습니다. fetch = FetchType.LAZY(지연로딩)로 변경하면 연관관계 데이터를 사용하는 시점에 로딩을 해주기 때문입니다.
하지만 FetchType.LAZY(지연로딩)으로 변경하여도 해당 연결 엔티티에 대해서 프록시를 걸어두고 연관된 엔티티를 사용할 때 캐싱된 프록시에 대한 쿼리가 결국 발생하기 때문에 N+1 문제는 그대로 발생됩니다.
즉, 즉시로딩과 지연로딩은 N+1문제 발생시점 차이일 뿐 입니다. 둘 다 N+1 문제가 발생할 수 있지만 특별한 경우가 아니라면 LAZY를 기본 설정으로 사용하는 것이 좋습니다.
N+1 문제가 발생하는 이유는 JPQL 자체 문제입니다. JPQL은 SQL을 추상화한 객체지향 쿼리 언어로 단순히 엔티티 객체와 필드 이름을 가지고 쿼리를 수행합니다. JPQL은 연관관계 무시하고 해당 엔티티만으로 쿼리를 조회한 후, FetchType으로 지정한 시점에 연관관계 데이터를 별도로 호출하게되어 문제가 발생합니다.
N+1문제 해결방안
1. fetch join
`지연로딩에서 연관된 엔티티나 컬렉션을 함께 한번에 조회하는 역할`을 합니다. 자세한 설명은 JPQL 설명의 fetch join 부분을 읽어보시면됩니다. 아래처럼 @Query 어노테이션을 사용하여 `join fetch` 구문을 만들어주면 됩니다.
@Query("select u from User u join fetch u.orders")
List<Team> findAllByUsingFetchJoin();
- 1번의 호출로 User과 연관된 Orders까지 조회 됩니다.
- 실제로 INNER JOIN으로 호출됩니다.
단점:
- Fetch Join하면 FetchType 설정이 무의미해진다.
- 하나의 쿼리문으로 가져오다보니 Paing 쿼리를 사용하지 못한다. (Pageable 사용 못함)
2. @EntityGraph
@EntityGraph(attributePaths = "player")
@Query("select t from Team t")
List<Team> findAllByUsingEntityGraph();
- @EntityGraph의 attributePaths에 쿼리 수행시 바로 가져올 필드명을 지정하면 EAGER 조회로 가져옵니다.
- 실제로 OUTER JOIN으로 호출됩니다.
단점
- 관계가 복잡해지면 매우 골치아파집니다.
@XXXToOne의 경우 : fetch join (혹은 @EntityGraph)을 통해 해결
@XXXToMany의 경우 : BatchSize를 사용하여 해결
3. BatchSize
연관된 엔티티를 조횔할 때 지정된 size 만큼 SQL의 IN절을 사용해서 조회하는 방법입니다.
N+1 문제가 안 일어나는 것은 아니고 N+1 문제가 발생하더라도 SQL의 IN절을 사용하여 성능을 최적화하는 방식입니다.
@Entity
@Getter
@NoArgsConstructor
public class Team {
...
@BatchSize(size=10)
@OneToMany(mappedBy = "team", fetch = FetchType.EAGER)
private Set<Player> players = new LinkedHashSet<>();
}
- 즉시 로딩이기 때문에 Team을 조회할 때 Player를 같이 조회합니다.
- Player를 조회할 때 @BatchSize에 지정한 수 만큼 Team의 id를 축적한다음 한번에 SQL IN 절을 날립니다.
- 어노테이션 말고도 application.properties에 설정해주어도 됩니다.
spring.jpa.properties.hibernate.default_batch_fetch_size=1000
4. QueryBuilder 사용하기
Mybatis, QueryDSL, JDBC Template 등이 QueryBuilder 들입니다.
간단한 규모는 JPA가 유용하지만 복잡해질 수록 JPA로는 부족할 수가 있습니다. 때문에 위와 같은 QueryBuilder를 같이 사용하는 것이 좋습니다.
'Spring Data > JPA' 카테고리의 다른 글
JPA 생성 및 수정 날짜 자동 처리를 위한 공통 엔티티 만들기 @MappedSuperclass, @EnableJpaAuditing (0) | 2023.08.17 |
---|---|
JPA ddl-auto: 데이터베이스 스키마 자동 생성 전략 (0) | 2023.08.17 |
JPA 엔티티 매핑: 객체와 데이터베이스 테이블의 매핑 (0) | 2023.08.17 |
JPA EntityManager와 영속성: JPA의 데이터 관리 이해 (0) | 2023.08.17 |
JPA: 자바 ORM 표준 (0) | 2023.08.17 |