
N+1 문제는 데이터베이스와 연관된 애플리케이션에서 발생하는 성능 문제로, 주로 ORM(Object-Relational Mapping) 프레임워크(예: Hibernate, JPA)에서 자주 나타남. 이 문제는 하나의 쿼리로 여러 데이터를 가져올 때, 연관된 데이터를 추가로 가져오는 과정에서 불필요하게 많은 쿼리가 발생하는 상황을 말함.
즉, 하나의 엔터티를 가져오는 데 1개의 쿼리가 실행되고, 그 엔터티에 연관된 N개의 데이터에 대해 각각 또 다른 쿼리가 실행되어 총 N+1개의 쿼리가 발생하는 문제임.
게시물과 댓글이 있는 상황에서 게시물 목록을 가져오고, 각 게시물에 달린 댓글을 가져온다고 가정함.
SELECT * FROM posts; (1개의 쿼리)
게시물 N개가 있으면 댓글을 가져오는 쿼리 N번 실행됨.
총 쿼리 수는 1(게시물 쿼리) + N(댓글 쿼리) = N+1 쿼리가 발생하게 됨.
N+1 문제는 주로 지연 로딩(Lazy Loading)으로 인해 발생함. ORM에서는 엔터티와 연관된 다른 엔터티를 즉시 로딩(Eager Loading) 하거나 지연 로딩 할 수 있음.
연관된 엔터티를 함께 로딩함. 즉, 처음에 한 번의 쿼리로 모든 데이터를 가져옴.
연관된 엔터티를 필요할 때만 별도의 쿼리로 로딩함.
문제가 발생하는 이유는, Lazy Loading을 사용할 때 주 쿼리로 특정 엔터티를 가져온 후, 연관된 데이터를 필요할 때마다 추가로 쿼리를 실행하기 때문에, N개의 데이터에 대해 각각 추가 쿼리가 발생하게 됨.
첫 번째 쿼리로 posts 테이블에서 게시물 목록을 가져옴. (SELECT FROM posts;)
게시물 엔터티가 로딩된 후, 각 게시물에 연관된 댓글을 가져올 때마다 각각 추가 쿼리가 발생함. (SELECT FROM comments WHERE post_id = ?;)
즉, 게시물 N개가 있을 때, 댓글을 가져오기 위한 N개의 추가 쿼리가 발생하여 N+1 쿼리 문제가 발생함.
N+1 문제는 성능 저하를 일으키므로 이를 해결하기 위해 다양한 방법이 있음. 주로 즉시 로딩이나 JOIN 쿼리를 사용하여 한 번에 데이터를 가져오는 방식으로 해결함.
지연 로딩 대신 즉시 로딩(Eager Loading) 설정을 사용하여 관련 데이터를 한 번에 로딩함. 즉, 첫 번째 쿼리에서 연관된 엔터티까지 모두 가져옴.
@Entity
public class Post {
@OneToMany(fetch = FetchType.EAGER) // 즉시 로딩
private List<Comment> comments;
}
페치 조인(Fetch Join)은 한 번의 쿼리로 연관된 엔터티를 모두 가져오는 방법임. 이를 통해 여러 쿼리 대신 하나의 쿼리로 필요한 데이터를 한 번에 조회할 수 있음.
SELECT p FROM Post p JOIN FETCH p.comments;
배치 사이즈를 설정하여 N+1 쿼리 대신 일정량의 데이터를 한 번에 로딩할 수 있음. Hibernate와 같은 ORM에서 제공하는 @BatchSize 어노테이션을 활용해 이를 설정할 수 있음.
@Entity
@BatchSize(size = 10)
public class Post {
@OneToMany(fetch = FetchType.LAZY)
private List<Comment> comments;
}
엔티티 그래프는 JPQL이나 JPA Criteria API에서 특정 엔터티의 연관 관계를 지정하여 원하는 시점에 데이터를 페치할 수 있는 기능임. 이는 N+1 문제를 해결하기 위해 필요한 연관 엔터티를 명시적으로 정의할 수 있게 해줌.
@EntityGraph(attributePaths = {"comments"})
@Query("SELECT p FROM Post p")
List<Post> findAllWithComments();
N+1 문제는 ORM을 사용하는 애플리케이션에서 자주 발생하는 성능 문제로, 주로 지연 로딩으로 인해 발생함. 이 문제를 해결하려면 즉시 로딩, 페치 조인, 배치 사이즈 설정, 엔티티 그래프와 같은 기술을 사용하여 불필요한 쿼리 호출을 줄이고 성능을 최적화할 수 있음.