N + 1
연관 관계가 설정된 엔티티를 조회할 경우, 조회된 데이터 개수(n)만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 현상
1:N 또는 N:1 관계를 가진 엔티티에서 발생한다.
현상 재연
위 그림과 같이 설계된 테이블을 예를 들어보자
CategoryRepository에서 findAll을 호출하면, 카테고리만 불러오는 것이 아니라 카테고리와 OneToMany관계를 맺고 있는 제품의 결과도 같이 불러오게 된다 (N+1 발생)
발생 이유
N+1 문제가 발생하는 이유는 JPA가 JPQL을 분석해서 sql을 생성할 때는 글로벌 Fetch 전략을 참고하지 않고 오직 JPQL 자체만을 사용한다.
1. Fetch 전략이 즉시 로딩인 경우
- findAll()을 한 순간 select c from Category c라는 JPQL 구문이 생성되고 해당 구문을 분석한 select * from category라는 SQL이 생성되어 실행된다.
- DB의 결과를 받아 category 엔티티의 인스턴스들을 생성한다.
- category와 연관되어 있는 product도 로딩을 해야 한다.
- 영속성 컨텍스트에서 연관된 product가 있는지 확인한다.
- 영속성 컨텍스트에 없다면 만들어진 category 인스턴스들 개수에 맞게 select * from product where cat_no =?이라는 SQL 구문이 생성된다. (N+1 발생)
2. Fetch 전략이 지연 로딩인 경우
- findAll()을 한 순간 select c from Category c라는 JPQL 구문이 생성되고 해당 구문을 분석한 select * from category라는 SQL이 생성되어 실행된다.
- DB의 결과를 받아 category 엔티티의 인스턴스들을 생성한다.
- 코드 중에서 category의 product 객체를 사용하려고 하는 시점에 영속성 컨텍스트에서 연관된 product가 있는지 확인한다.
- 영속성 컨텍스트에 없다면 만들어진 category 인스턴스들 개수에 맞게 select * from product where cat_no =?이라는 SQL 구문이 생성된다. (N+1 발생)
해결방법
Fetch Join, EntityGraph 어노테이션, Batch Size 등의 방법이 있다.
1. Fetch Join
JPQL을 사용하여 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 같이 가져오게 하는 방법이다. (SQL join 문)
별도의 메서드를 만들어줘야 하며 @Query 어노테이션을 사용해서 "join fetch ~ " 구문을 만들면 된다.
public interface CategoryRepository extends JpaRepository<Category, Long> {
@Query("select c from Category c join fetch c.products")
List<Category> findAllFetchJoin();
}
Fetch Join은 미리 category와 product 데이터를 조인(inner join)해서 데이터를 가져온다.
2. EntityGraph 어노테이션
@EntityGraph라는 어노테이션을 사용해서 fetch 조인을 한다.
Fetch Join과 동일하게 JPQL을 사용해 Query문을 작성하고 필요한 연관관계를 EntityGraph에 설정하면 된다.
public interface CategoryRepository extends JpaRepository<Category, Long> {
@EntityGraph(attributePaths = {"products"})
@Query("select c from Category c join fetch c.products")
List<Category> findAllFetchJoin();
}
EntityGraph는 outer join을 사용해서 데이터를 가져온다.
별도로 속성을 붙여주어야 하고 inner join보다 성능 최적적화에 불리한 outer join을 사용하기 때문에 잘 사용되지 않는다.
3. Batch Size
yml파일에 batch size값을 설정하고 설정된 값만큼 SQL의 IN절을 사용해서 데이터를 가져온다.
N+1 문제가 발생하더라도 select * from product where cat_no =? 이 아닌 select * from product where cat_no in (?,?,? ) 방식으로 100번이 일어날 N+1문제를 1번만 더 조회하는 방식으로 성능을 최적화할 수 있다.
실무에서 사용
우선 연관관계에 대한 설정이 필요하다면 FetchType을 성능 최적화를 하기 어려운 즉시로딩(EAGER)을 사용하는 게 아니라 지연로딩 (LAZY) 모드로 사용을 하고 성능 최적화가 필요한 부분에서는 Fetch 조인을 사용한다.
또한 기본적으로 Batch Size의 값을 1000 이하로 설정한다.
실전
Q. N+1 문제의 발생이유를 설명해 주세요
A. N+1 문제는 데이터베이스에서 연관관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 개수만큼 연관된 엔티티까지 쿼리가 추가로 발생하는 성능 문제입니다. 예를 들어 전체 게시글을 조회할 때 N개의 게시글을 조회하고, 게시글에 대한 추가 정보(게시글을 작성한 user 정보)를 얻기 위해 각 게시글에 대해 별도의 쿼리를 실행합니다. 10개의 게시글을 가져오고, 10번의 추가 쿼리를 실행하여 게시글을 작성한 user에 대한 정보를 가져오게 됩니다.
Q. 해결방법은 뭐가 있을까요?
A. Fetch Join, EntityGraph, BatchSize 등의 방법이 있는데 먼저 Fetch Join은 데이터베이스 쿼리에 JOIN FETCH를 사용하여 추가 정보를 한 번에 가져올 수 있습니다. EntityGraph는 어노테이션으로서 attributePath에 연관관계 멤버변수를 설정하면 Fetch Join과 동일하게 작동되어 문제를 해결할 수 있습니다. Batch Size 방법은 하나로 연관된 엔티티를 조회할 때 지정된 size만큼 SQL의 IN절을 사용해서 조회할 수 있게 합니다.
참조
https://programmer93.tistory.com/83
항해 개발자 취업 리부트 코스를 수강하고 작성한 콘텐츠 입니다.
IT 커리어 성장 코스 항해99, 개발자 취업부터 현직자 코스까지
항해99는 실무에 집중합니다. 최단기간에 개발자로 취업하고, 현직자 코스로 폭발 성장을 이어가세요. 실전 프로젝트, 포트폴리오 멘토링, 모의 면접까지.
hanghae99.spartacodingclub.kr
'멋진 개발자 > Java & Spring' 카테고리의 다른 글
[항해 취업코스] 개발자 취준 기록 18 - Spring bean container의 생명주기 (0) | 2024.03.12 |
---|---|
[항해 취업코스] 개발자 취준 기록 17 - 즉시로딩, 지연로딩 (3) | 2024.03.12 |
[항해 취업코스] 개발자 취준 기록 15 - Spring Security의 구조와 JWT 발급 과정 (0) | 2024.03.12 |
[항해 취업코스] 개발자 취준 기록 14 - Annotation (0) | 2024.03.10 |
[항해 취업코스] 개발자 취준 기록 13 - MVC 모델 (0) | 2024.03.07 |