티스토리 뷰

springboot

[JPA] N + 1 문제

주다애 2024. 11. 20. 17:25

🪐 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 문제는 어렵지 않게 마주할 수 있다. 하지만 성능 이슈가 발생하므로 해결해야 한다.

  1. 즉시 로딩은 사용하지 말고 지연 로딩을 사용하자. 즉시 로딩은 성능 최적화도 어렵다.
  2. 지연 로딩으로 모두 설정하고 성능 최적화가 필요한 곳에 JPQL 페치 조인을 사용하자

참고 자료

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

 

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
링크
«   2025/04   »
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
글 보관함