N+1 문제
JPA에서의 N+1 문제는 주로 연관된 엔티티를 조회할 때 발생하는 성능 문제로, 데이터베이스 쿼리가 예상보다 많이 발생하여 성능이 저하될 수 있다. 이 문제는 즉시 로딩(Eager Loading)이나 지연 로딩(Lazy Loading) 설정에 따라 다르게 나타날 수 있다. N+1 문제를 해결하기 위해서는 페치 조인(Fetch Join), @BatchSize, @Fetch(FetchMode.SUBSELECT)와 같은 전략을 사용할 수 있다.
1. N+1 문제와 즉시 로딩
즉시 로딩은 연관된 엔티티를 조회할 때 기본적으로 즉시 데이터베이스에서 로드한다. 하지만 잘못된 설정이나 비효율적인 쿼리로 인해 N+1 문제가 발생할 수 있다. 엔티티가 아래와 같을 때, 특정 author를 조회한다고 가정해볼 때 books의 개수만큼 SELECT query가 실행된다.
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(fetch = FetchType.EAGER, mappedBy = "author")
private List<Book> books;
}
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "author_id")
private Author author;
}
SELECT * FROM Author;
-- 조회한 Author의 Book 개수 만큼
SELECT * FROM Book WHERE author_id = ?;
-- ...
SELECT * FROM Book WHERE author_id = ?;
2. 지연 로딩과 N+1 문제
지연 로딩(Lazy Loading)은 연관된 엔티티를 실제로 사용할 때 쿼리를 실행한다. 이 경우, 연관된 엔티티를 반복적으로 로드하면서 N+1 문제를 발생시킬 수 있다. 위와 같지만 지연 로딩으로써 books를 사용할때 N+1 문제가 발생한다.
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "author")
private List<Book> books;
}
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;
}
SELECT * FROM Author;
-- 조회한 Author의 Book 개수 만큼
SELECT * FROM Book WHERE author_id = ?;
-- ...
SELECT * FROM Book WHERE author_id = ?;
3. 페치 조인 사용
페치 조인(Fetch Join)은 JPQL을 사용하여 연관된 엔티티를 한 번의 쿼리로 로드한다. 이 방법을 사용하면 N+1 문제를 방지할 수 있다.
@Query("SELECT a FROM Author a JOIN FETCH a.books")
List<Author> findAllAuthorsWithBooks();
SELECT a.id, a.name, b.id, b.title
FROM Author a
JOIN Book b ON a.id = b.author_id;
4. 하이버네이트 @BatchSize
@BatchSize는 하이버네이트에서 연관된 엔티티를 배치로 로드할 수 있게 도와준다. 이 방법은 지연 로딩과 결합하여 N+1 문제를 줄일 수 있다.
@Entity
@BatchSize(size = 10)
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "author")
private List<Book> books;
}
SELECT * FROM Author;
-- BatchSize에 따라 한 번에 여러 개의 Book을 로드함
SELECT * FROM Book WHERE author_id IN (?, ?, ?, ...);
5. 하이버네이트 @Fetch(FetchMode.SUBSELECT)
@Fetch(FetchMode.SUBSELECT)는 하이버네이트에서 연관된 엔티티를 서브쿼리로 로드할 수 있게 도와준다. 이 방법은 N+1 문제를 해결할 수 있다.
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "author")
@Fetch(FetchMode.SUBSELECT)
private List<Book> books;
}
SELECT * FROM Author;
-- Author의 책을 서브쿼리로 로드함
SELECT * FROM Book WHERE author_id IN (SELECT id FROM Author);