1. JPA의 N + 1 문제란?
N + 1 문제는 연관된 엔티티를 조회할 때 발생하는 성능 이슈를 말한다. 연관 관계가 설정된 엔티티를 조회할 경우에, 조회된 데이터 개수(N)만큼 연관관계의 조회 쿼리가 추가로 발생하는 현상이다. 다시 말하면, 조회시 1개의 쿼리를 생각하고 설계했으나 예상치 못한 쿼리가 N개 더 발생하는 문제이다.
2. 즉시 로딩(Eager)
// User.java
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private Set<Order> orders = emptySet();
// Order.java
@ManyToOne(fetch = FetchType.EAGER)
private Order order;
즉시 로딩이란 연관된 엔티티를 조인해서 가져오는 것을 말한다.
예를 들어, user를 findAll() 메서드로 가져온다면,
select u from User u;
위의 JPQL이 만들어지고, SQL로 번역되어 데이터베이스에 전달된다.
JPA는 Order가 즉시 로딩으로 설정되어 있으므로 Order를 가져오는 추가 쿼리(N + 1 문제)가 발생하게 된다.
3. 지연 로딩(Lazy)
// User.java
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private Set<Order> orders = emptySet();
// Order.java
@ManyToOne(fetch = FetchType.LAZY)
private Order order;
지연 로딩이란 연관된 엔티티를 실제 객체 대신에 프록시 객체를 채워 넣어준다.
이 때 user를 findAll() 메서드로 가져와도 연관된 엔티티가 프록시 객체가 채워지기 때문에 N + 1 문제가 발생하지 않는다.
하지만, 연관된 엔티티, order를 사용하게 될 경우, 데이터베이스에 조회해서 실제 객체의 값을 채워 넣어주게 된다.
말 그대로 "지연해서 나중에 쓸 때 로딩해주겠다"는 것이다.
결국, 연관된 엔티티를 사용하게 되면 추가 쿼리가 발생하기 때문에 지연 로딩도 N + 1 문제에서 자유롭지 못하다.
4. N + 1 문제는 어떻게 해결할 수 있는가?
- fetch join
- @EntityGraph
4 - 1. fetch join
fetch join은 연관 관계에 있는 엔티티를 한 번에 즉시 로딩하는 구문이다.
핵심은 "선별적으로" 즉시 로딩할 수 있다는 점이다.
어느 경우에는 즉시 로딩하지 않아도 되고, 다른 경우에는 즉시 로딩해야 하는 경우가 있을 수 있다.
이런 경우에, 연관된 엔티티를 지연 로딩으로 설정해두고, JPQL을 fetch join 해오면,
상황에 맞게 쿼리해올 수 있는 것이다.
// User.java
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private Set<Order> orders = emptySet();
// UserRepository.java
@Query("select distinct u from User u left join fetch u.orders")
List<User> findAllJPQLFetch();
4 - 2. @EntityGraph
@EntityGraph도 fetch 조인과 동일한 효과를 만들어 낸다.
@EntityGraph(attributePaths = {"orders"}, type = EntityGraphType.FETCH)
List<User> findAllEntityGraph();
쿼리 메서드에 해당 어노테이션을 추가해 사용할 수 있다.
5. 컬렉션 조인시 @BatchSize
컬렉션 조인을 하는 경우에는 데이터가 뻥튀기 될 위험이 있다.
페이징 처리를 하기 위해 1:N 관계의 엔티티에서 `1` 쪽을 기준으로 데이터를 fetch join 해오는 경우
페이징이 맞지 않게 된다.
예를 들어, user가 5명, order가 각각 3개씩 있는 상황에서
user를 조회해올 때 원하는 레코드 개수는 5개이지만, 조인이 일어나기 때문에 15개가 반환될 것이다.
따라서, 컬렉션을 조인해야 하는데, 페이징 처리까지 해야한다면, @BatchSize로 해결할 수 있다.
@BatchSize(size = 100)
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private Set<Order> orders = emptySet();
5 - 1. application.yml에서 글로벌하게 batch size 지정하기
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
설정 파일에서 위와 같이 지정하면 프로젝트 전체에 batch size가 적용된다.
References
https://www.maeil-mail.kr/question/49
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
https://tecoble.techcourse.co.kr/post/2021-07-26-jpa-pageable/
'백엔드 면접 질문' 카테고리의 다른 글
일급 컬렉션이 무엇인가요? (1) | 2024.11.29 |
---|---|
자바에서 Checked Exception과 Unchecked Exception에 대해서 설명해주세요. (0) | 2024.11.28 |
Entity Manager에 대해 설명해주세요. (0) | 2024.11.26 |
JPA의 ddl-auto 옵션은 각각 어떤 동작을 하고 어떤 상황에서 사용해야 할까요? (0) | 2024.11.25 |
Spring Data JPA에서 새로운 Entity인지 판단하는 방법은 무엇일까요? (0) | 2024.11.22 |