JPA N+1 Problem and Solution
0. 들어가면서
-
이번 포스트에서는 JPA 개발 과정에서 흔히 발생하는 N+1 문제에 대해서 다뤄보고자 합니다.
-
Issue
- 연관 관계가 설정된 Entity를 조회할 경우에 조회된 데이터 갯수(n)만큼 연관 관계의 SELECT Query가 추가로 발생하여 데이터를 읽어오게 된다.
Entity
- 사용자는 여러 개의 아이템을 보유할 수 있다.
-
아이템은 한 명의 유저에게 종속되어 있다.
//User Entity @Entity @Getter @Setter @NoArgsConstructor public class User{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String name; @OneToMany(mappedBy = "user", fetch = FetchType.EAGER) private List<Item> items; }
//Item Entity @Entity @Getter @Setter @NoArgsConstructor public class Item{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String name; @ManyToOne private User user; public Item(String name){ this.name = name; } }
- userService.userList()를 실행한다.
- DB
- 현재 10명의 User가 존재한다.
- 각 User 별로 10개씩 Item을 보유하고 있다.
@Service("userService") @Transactional public class UserServiceImpl implements UserService{ private final UserRepository userRepository; public UserServiceImpl(UserRepository userRepository){ this.userRepository = userRepository; } @Override public List<User> userList() throws Exception{ User user = userRepository.findAll() .orElseThrow(() -> new BusinessException("유저가 존재하지 않습니다.") ); return user; } }
- 실행 결과
- Item을 조회하는 Query가 User를 조회한 횟수만큼 Query가 호출된다.
- FetchType.Eager라서 발생하는 문제일까??
Lazy Loading
- FetchType을 Eager에서 Lazy로 변경해본다.
//변경 전 @OneToMany(mappedBy = "user", fetch = FetchType.EAGER) private List<Item> items;
//변경 후 @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) private List<Item> items;
-
userService.userList()를 실행한다.
- 실행 결과
- User를 호출하는 Query 하나만 발생한다.
- FetchType.LAZY는 연관 관계 데이터를 Proxy 객체로 바인딩하고 실제로 연관 관계 Entity를 Proxy만으로는 사용하지 않는다.
- Item에 대한 정보를 호출하는 부분을 구현했을 때 N+1문제는 동일하게 발생한다.
- 단지 N+1 발생 시점을 연관 관계 데이터를 사용하는 시점으로 미룬 것 뿐이다.
- N+1 발생 이유??
- JPQL은 JPA에서 객체지향적으로 Query를 작성할 수 있게 해주는 Query 언어
- 실제 DB Table이 아닌 Entity 객체를 대상으로 작동한다.
- findAll() 수행 시 SELECT * FROM USER Query만 수행된다.
-
Solution
- Fetch Join
- EntityGraph
- FetchMode.SUBSELECT
- Batchsize
- QueryBuilder
Fetch Join
-
Fetch Join을 추가한 JPQL
@Query("select u from User u join fetch u.items") List<User> findAllFetchJoin();
-
수행 결과
- Fetch Join을 사용하면 호출 시점에 모든 연관 관계의 데이터를 가져온다.(Eager)
- INNER JOIN이 수행 된다.
- 연관 관계에서 설정한 FetchType을 사용할 수 없다.
@EntityGraph
-
attributePaths : Query 수행 시 바로 가져올 Field 지정
- @EntityGraph를 추가한 JPQL
@EntityGraph(attributePaths = "items") @Query("select u from User u") List<User> findAllEntityGraph();
- 수행 결과
- Fetch Join을 사용하면 호출 시점에 연관 관계의 데이터를 가져온다.(Eager)
- OUTER JOIN이 수행 된다.
- Fetch Join, @EntityGraph 주의사항
- 카테시안 곱(Cartesian Product)이 발생하여 User의 개수만큼 Item의 중복 데이터가 존재할 수 있다.
- JPQL의 distinct를 사용하여 중복을 방지한다.
-
Set(LinkedHashSet)을 사용하여 중복을 허용하지 않는 Collection을 사용한다
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY) private Set<Item> items = new LinkedHashSet<>();
- 카테시안 곱(Cartesian Product)이 발생하여 User의 개수만큼 Item의 중복 데이터가 존재할 수 있다.
FetchMode.SUBSELECT
-
연관 관계의 데이터를 조회할 경우 Subquery로 함께 조회하는 방법
-
User에 FetchMode.SUBSELECT를 적용
@Fetch(FetchMode.SUBSELECT) @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) private List<Item> items;
-
수행 결과
- FetchType.EAGER일 경우 조회 시점에 Subquery가 실행된다.
- FetchType.LAZY일 경우 연관 관계 데이터 조회 시점에 Subqery가 실행된다.
BatchSize
- Hibernate에서 제공하는 org.hibernate.annotations.BatchSize 사용
- 연관된 Entity를 조회할 때 지정된 size만큼 SQL의 IN절을 사용해서 조회
-
연관 관계 데이터의 최적화 데이터 사이즈를 알기가 어려움
-
User에 @BatchSize 적용
@BatchSize(size=5) @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) private Set<Item> items = new LinkedHashSet<>();
- 수행 결과
- User의 Id들을 모아서 SQL IN절을 수행한다.
- FetchType.EAGER일 경우 조회 시점에 Item의 개수가 10개이므로 IN절을 2번 실행한다.
- FetchType.LAZY일 경우 연관 관계 데이터 조회 시점에 5개를 미리 Loading하고 6번째 Entity 사용 시점에 다음 SQL IN절을 추가로 실행한다.
- application.yml에서 Default BatchSize 설정
spring: jpa: properties: hibernate: default_batch_fetch_size :
Reference
- https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85
- https://velog.io/@hero6027/JPA-Proxy%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-Lazy-Loading%EC%A7%80%EC%97%B0%EB%A1%9C%EB%94%A9
- https://jojoldu.tistory.com/165
- https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1#fetchmode.subselect