티스토리 뷰
🪐 N + 1 문제
JPA의 N+1 문제는 연관 관계가 설정된 엔티티를 조회할 경우 조회된 데이터 개수(N)만큼 연관관계의 조회 쿼리가 추가로 발생하는 현상이다.
만약, 유저와 게시물 엔티티가 1 : N의 관계를 맺고 있는 경우, 유저를 조회한 후 각 유저마다 게시물을 조회하기 위해 유저의 개수(N)만큼의 쿼리가 더 날아간다.
데이터가 얼마 없는 환경에서는 큰 성능 이슈가 없겠지만 실제 업무를 수행하거나 많은 양의 데이터를 조회할 시에는 성능 이슈가 생기게 된다. JPA로 개발 시 성능상 가장 주의해야 하는 것이 N+!문제다.
즉시 로딩
즉시 로딩은 JPQL을 실행할 때 N + 1 문제가 발생할 수 있다.
// User.java
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private List<Article> articles = new ArrayList<>();
FetchType.EAGER를 사용해서 즉시 로딩으로 설정해주었다.
// Article.java
@ManyToOne
private User user;
User과 Article은 1 : N 양방향 연관관계이다. 유저는 3명이 있다고 가정한다.
1️⃣ id를 통해 특정 유저 한명을 조회해보자
Users users = em.find(Users.class, 1L);

join을 통해서 한 번의 쿼리로 유저와 article 정보를 가져온다. 즉, N+1 문제가 발생하지 않는다.
2️⃣ JPQL을 사용해서 유저들을 조회해보자
List<Users> users = em.createQuery("select u from Users u", Users.class)
.getResultList();


맨 처음 모든 유저를 조회하는 쿼리가 1개 나가고 + 유저의 개수만큼 게시물을 조회하는(여기서 N=3) 쿼리가 더 나가는 것을 볼 수 있다.
이것이 바로 N + 1 문제이다.
JPQL은 즉시 로딩이나 지연 로딩에 대해서는 상관하지 않고 JPQL만 사용해서 SQL을 생성한다.
실행되는 SQL문을 살펴보면
SELECT * FROM USERS;
SELECT * FROM ARTICLE WHERE USER_ID = 1;
SELECT * FROM ARTICLE WHERE USER_ID = 2;
SELECT * FROM ARTICLE WHERE USER_ID = 3;
유저 엔티티를 로딩하고, 유저의 개수만큼 게시물 엔티티를 즉시 로딩한다.
지연 로딩
그럼, 지연 로딩을 사용해서 엔티티를 조회하면 문제가 해결될까? 결론은 지연 로딩이라 하더라도 N + 1 문제에서 자유로울 수 없다.
@OneToMany(mappedBy = "users", fetch = FetchType.LAZY)
private List<Article> articles = new ArrayList<>();
FetchType.LAZY를 사용해서 지연 로딩으로 설정해주었다.
지연 로딩으로 설정하면 JPQL에서 N + 1 문제가 발생하지 않는다. 즉, 즉시 로딩의 한계를 극복한다.
List<Users> users = em.createQuery("select u from Users u", Users.class)
.getResultList();

즉시 로딩과 다르게 SELECT * FROM USERS의 SQL 구문만 나가는 것을 볼 수 있다.
그렇다면 지연 로딩에서 N + 1 문제가 발생하는 시점은 언제일까?
1️⃣ 모든 유저에 대한 게시물을 조회하는 비즈니스 로직을 작성해보자
List<Users> users = em.createQuery("select u from Users u", Users.class)
.getResultList();
for(Users user : users) {
// 지연 로딩 초기화
user.getArticles().size();
}

모든 유저에 대해 게시물을 초기화하면 결국 유저 개수만큼 SQL이 날아가게 되어 N + 1 문제가 발생한다.
결국, N + 1 문제는 즉시 로딩, 지연 로딩에서 모두 발생할 수 있다.
해결책 1 - 페치 조인 사용
지연 로딩 시 페치 조인을 사용하면 N + 1 문제를 해결 가능하다.
가장 일반적인 해결 방법이다.
List<Users> users = em.createQuery("select u from Users u join fetch u.articles", Users.class)
.getResultList();
for(Users user : users) {
// 지연 로딩 초기화
user.getArticles().size();
}

페치 조인을 사용하면 지연 로딩이 걸려있는 연관관계에 대해서 JOIN을 사용하여 한 번에 가져온다.
해결책 2 - 하이버네이트 @BatchSize
하이버네이트의 @BatchSize를 사용하면 지정한 size 만큼의 SQL의 IN절이 나간다.
만약 조회한 유저가 10명인데 size가 5라면 2번의 SQL만 추가로 실행한다.
@org.hibernate.annotations.BatchSize(size = 5)
@OneToMany(mappedBy = "users", fetch = FetchType.LAZY)
private List<Article> articles = new ArrayList<>();
해결책 3 - 하이버네이트 @Fetch(FetchMode.SUBSELECT)
FetchMode를 SUBSELECT로 사용하면 연관된 데이터를 조회할 때 서브 쿼리를 사용해서 N + ! 문제를 해결한다.
@org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "users", fetch = FetchType.LAZY)
private List<Article> articles = new ArrayList<>();
결론
JPA를 사용하게 되면 N + 1 문제는 어렵지 않게 마주할 수 있다. 하지만 성능 이슈가 발생하므로 해결해야 한다.
- 즉시 로딩은 사용하지 말고 지연 로딩을 사용하자. 즉시 로딩은 성능 최적화도 어렵다.
- 지연 로딩으로 모두 설정하고 성능 최적화가 필요한 곳에 JPQL 페치 조인을 사용하자
참고 자료
JPA 모든 N+1 발생 케이스과 해결책
N+1이 발생하는 모든 케이스 (즉시로딩, 지연로딩)에서의 해결책과 그 해결책에서의 문제를 해결하는 방법에 대해 이야기 하려합니다 😀
velog.io
도서 : 자바 ORM 표준 JPA 프로그래밍(김영한 지음)
'springboot' 카테고리의 다른 글
[JPA] ID 생성 전략 (1) | 2024.11.29 |
---|---|
[JPA] ddl-auto 옵션 (1) | 2024.11.18 |
[SETTING] intellij에서 JAVA JDK 버전 바꾸기 (0) | 2023.11.30 |
[용어] profile (0) | 2023.10.28 |
[JPA] @NotNull vs nullable = false (0) | 2023.08.13 |
- Total
- Today
- Yesterday
- 메인메소드
- @Spring
- 유효성 검사
- 일급컬렉션
- id생성전략
- NPE
- Optional
- upperBound
- @Value
- ddl-auto
- 동등성
- Spring
- checkedException
- 생성자
- JPA
- @NoArgsConstructor
- @ConfigurationProperties
- 이진탐색
- springboot
- lowerBound
- 티스토리챌린지
- 오블완
- null
- Java
- uncheckedException
- 자바
- 백준
- Thymeleaf
- N+1문제
- StreamAPI
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |