N + 1문제는 정말 뭔지 가늠이 안되는 질문이다.
데이터베이스 카테고리에 있는 질문인데, 뭔가 N개가 예상되는데 N + 1개가 호출되는? 그런 느낌을 가지고 예상했다. 하지만 조금 달랐다.
N + 1문제의 정의는 다음과 같다.
N + 1문제는 데이터베이스와 관련된 성능 문제로, 주로 ORM(Object-Relational Mapping)을 사용할 때 발생한다. 이는 하나의 쿼리로 여러 개의 엔티티를 가져온 후, 각 엔티티의 연관된 데이터를 조회할 때 추가적으로 N개의 쿼리가 발생하는 상황을 말한다.
즉, 한 개의 쿼리가 실행되었다고 예상이 되는데, 해당 엔티티에 추가적으로 연관된 엔티티 데이터들도 조회하려 할 때 N개의 쿼리가 또 실행되는 문제를 가리킨다.
이는 성능 저하로 이어질 수 있다.
예를 들어, 게시글과 댓글이 연관된 데이터라고 가정하면, 게시글을 조회하는 1개의 쿼리 후 각 게시글에 대한 댓글을 조회하는 N개의 쿼리가 발생하게 된다. 이로 인해 데이터베이스에 매우 많은 쿼리가 전송되며, 성능 저하로 이어지게 된다.
그렇다면 이 문제가 발생하는 이유는 무엇일까. 한 개의 쿼리로 모든 엔티티를 조회하면 안될까?
이 문제가 발생하는 이유는 ORM이 연관된 데이터를 즉시 가져오는 것이 아니라, 지연 로딩(lazy loading)방식을 사용하기 때문이다. 이는 필요할 때마다 데이터를 가져오기 위해 각각의 쿼리를 따로 실행하게 된다.
lazy loading은 연관된 데이터를 즉시 가져오지 않고, 실제로 접근할 때 쿼리를 실행하여 데이터를 조회하는 방식을 말한다.
해결책으로 무엇이 있을까?
즉시 로딩은 연관된 데이터를 한 번의 쿼리로 모두 조회하는 방식이다.
SELECT p FROM Post p JOIN FETCH p.comments
위 쿼리는 게시글과 댓글을 한 번의 쿼리로 모두 가져온다.
@EntityGraph(attributePaths = {"comments"}
List<Post> findAll();
또 다른 해결책으로 배치로딩(Batch Fetching)이 있다. 한 번에 일정한 크기만큼 데이터를 로딩하는 방식으로, JPA에서 @BatchSize 어노테이션을 사용하여 연관된 데이터를 배치 단위로 로드할 수 있다.
@BatchSzie(size = 10)
@CollectionTable(name = "comments")
private List<Comment> comments;
이 설정은 댓글을 한 번에 10개씩 가져오게 하여, 지나치게 많은 쿼리가 발생하는 것을 방지한다.
때로는 복잡한 관계에 있는 데이터를 효율적으로 가져오기 위해 쿼리 자체를 최적화하는 방법이 필요하다. 이 경우에는 필요한 데이터만 골라서 조회하는 JPQL 또는 SQL을 직접 작성하여 방지할 수 있다.
이로써 N + 1문제에 대한 정의와 발생 이유, 해결책에 대해 간단히 알아봤다.
대규모 시스템에서 고려해야 하는 문제로 실무에서는 더 자세히 다뤄질 것 같다.