CS

[CS] N+1 문제 해결

jjuya 개발 기록 2024. 5. 8. 00:48

JPA를 사용하다 보면 의도지 않았지만 여러 번의 select 문이 순식간에 여러 개가 나타나는 현상을 본 적 있을 것이다. 이러한 현상을 N+1 문제라고 부른다.

N+1이란?

연관관계로 매핑된 엔티티를 조회할 경우 조회된 데이터 개수 (n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 현상

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(length = 10, nullable = false)
    private String name; 

    @OneToMany(mappedBy = "user")
    private Set<Article> articles = emptySet();
}
@Entity
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 50, nullable = false)
    private String title;

    @Column(length = 300, nullable = false)
    private String content;

    @ManyToOne
    private User user;
}

 

발생 이유

N+1 문제가 발생하는 이유는 JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 fetch 전략을 참고하지 않고 오직 JPQL 자체만을 사용한다.

발생 과정

  1. findAll()과 같이 여러 Entity를 조회해야 하는 경우 JPQL에서 ‘select * from Table t’ 쿼리를 생성한다.
  2. DB의 결과를 받아 Table t 엔티티의 인스턴스들을 생성한다.
  3. Table t와 연관된 DB를 찾아 로드한다.
  4. 영속성 컨텍스트에 연관 DB가 있는지 확인한다.
  5. 영속성 컨텍스트에 없다면 ‘select * from {연관 table} where tableName+id =?’ 쿼리를 생성한다.
  6. 이 과정에서 N+1 문제가 발생한다.

즉시 로딩

fetch = FetchType.EAGER

// User.java
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private Set<Article> articles = emptySet();

// Article.java
@ManyToOne(fetch = FetchType.EAGER)
private User user;

User의 입장에서 즉시 로딩을 사용한다고 했을 때, Article의 모든 list를 다 같이 조회하고 싶을 상황이 생길 수도 있는데 왜 즉시 로딩이 문제가 될까?

User의 select 해왔지만 JPA는 Article에 대해서 EAGER(즉시 로딩)이 걸려있는 것을 보고 select 한 모든 User에 대해서 Article이 있는지를 검색하게 됩니다.

즉, User에 대해 검색하고 싶어서 select 쿼리를 하나 날렸지만(1), 즉시 로딩이 걸려있기 때문에 각각의 User가 가진 Article을 모두 검색한다(N)라는 N+1 문제가 발생하는 것입니다.

 

지연 로딩

  • 연관된 객체를 “사용” 하는 시점에 로딩을 해주는 방식 (fetch = FetchType.Lazy)
  • 연관관계에 있는 객체는 프록시 상태로 초기화되지 않은 상태로 존재함
  • 필요시에 가져오기 때문에 불필요한 쿼리를 발생시키지 않을 수 있음

N+1의 문제를 지연 로딩으로 문제를 해결할 수 있다.

하지만, 이 방식으로는 N+1문제가 전부 해결되지는 않는다.

처음 find 할 때는 N+1이 발생하지 않지만 추가로 User 검 생후 User의 Article을 사용해야 한다면 이미 캐싱된 User의 Article 프록시에 대한 쿼리가 또 발생하게 된다.

@OneToMany(mappedBy = “user”, fetch = FetchType.Lazy)
Private Set<Article> articles = emptySet();

그렇다면?

지연 로딩에서의 해결책 = fetch join

JPQL을 사용하여 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 같이 가져오게 하는 방법이다.

(SQL Join 문을 생각하면 된다. )

별도의 메서드를 만들어줘야 하며 @Query 어노테이션을 사용해서 "join fetch 엔티티. 연관관계_엔티티" 구문을 만들어 주면 된다.

@Query(“SELECT distinct u FROM User u left join fetch u.articles”)
List<User> findAllJPQFetch();