티스토리 뷰

728x90
반응형

N+1 문제는 데이터베이스를 사용하는 애플리케이션에서 자주 발생하는 성능 문제 중 하나로,

 

특히 ORM(Object-Relational Mapping) 도구를 사용할 때 자주 나타난다.

 

이 문제는 하나의 메인 쿼리를 실행한 후에, 그 결과로 얻어진 N개의 레코드 각각에 대해 

 

추가적인 쿼리(N개의 쿼리)를 실행하는 상황에서 발생하며, 불필요하게 많은 쿼리를 보내게 되어 성능 저하를 초래한다.

 

요약하자면 N+1 문제는 다음과 같은 상황에서 발생한다.

 

  1. N개의 항목을 가져오는 쿼리를 실행한다.
  2. 각각의 항목에 대해 추가로 1개의 쿼리를 실행한다.

결과적으로, 처음에 실행한 1개의 쿼리(N개를 가져오는)와 각 항목에 대해 실행한 N개의 쿼리를 합쳐 N+1개의 쿼리가 실행된다.

 

Example

 

이어서 코드로 예를 들어보자.

 

게시판을 구현할 때 'Author'와 'Post'라는 두 개의 테이블이 1:N으로 매핑되어 있는 상황을 가정하자.

// 모든 작가를 가져오는 쿼리
List<Author> authors = authorRepository.findAll();

for (Author author : authors) {
    // 각 작가에 대한 모든 게시물을 가져오는 쿼리
    List<Post> posts = author.getPosts();
}

여기서 'authorRepository.findAll()'은 1개의 쿼리로 데이터베이스에서 모든 작성자를 가져온다.

 

만약 디비에 10명의 작성자가 있다면, 이 쿼리로 10개의 작성자가 메모리에 로드된다.

 

그런데 'author.getPosts()'는 각 작성자에 대한 게시물을 가져오기 위해 추가적인 쿼리를 실행한다.

 

10명의 작성자를 가정했기 때문에, 각 작성자에 대해 1번씩 쿼리가 실행되므로 총 10개의 쿼리가 실행된다.

 

즉, 처음의 1개 쿼리와 각각의 작성자에 대한 10개의 쿼리로 인해 총 11개의 쿼리가 실행되는 것이다.

 

여기서 작성자의 수를 N이라고 한다면, 실행되는 쿼리 수는 N+1이 된다.

 

Performance Issue

 

그렇다면 왜 N+1문제가 성능 문제로 이어질까?

 

당연하다면 당연하게도 아래와 같은 이유가 있다.

 

  1. 많은 쿼리 실행: 데이터베이스에 보내는 쿼리의 수가 기하급수적으로 증가하여, 네트워크 오버헤드와 데이터베이스의 부하가 증가
  2. 지연 시간 증가: 각 쿼리는 데이터베이스에 요청을 보내고 응답을 받아오는 데 시간이 걸리며, 쿼리의 수가 많아질수록 전체 지연 시간이 증가한다.
  3. 비효율적인 자원 사용: 한 번에 여러 데이터를 가져올 수 있음에도 불구하고, 불필요하게 여러 번 데이터베이스에 접근하여 동일한 작업을 반복하게 된다.

 

Solution

 

N+1문제는 해결을 위한 다양한 방법이 존재한다.

 

Eager Loading

 

즉시 로딩을 사용하면, 하나의 쿼리로 연관된 엔티티를 한 번에 가져올 수 있다.

 

일반적으로 조인 연산을 통해 쿼리가 이루어지며, 코드로 예를 들면 아래와 같다.

@Query("SELECT a FROM Author a JOIN FETCH a.posts")
List<Author> findAllWithPosts();

 

Batch Fetching

 

JPA를 비롯한 ORM 프레임워크에서는 배치 페치를 지원한다.

 

이는 N개의 쿼리가 아니라 특정 크기로 나누어 한 번에 여러 레코드를 가져오는 방식인데,

 

예를 들어 10개의 게시글을 가져오기 위해 1번의 쿼리를 실행하는 대신 한 번에 5개를 가져오는 2번의 쿼리로 최적화하는 것이다.

 

Subselect

 

서브 셀렉트는 첫 번째 쿼리에서 얻은 결과를 기반으로 두 번째 쿼리를 실행해 필요한 데이터를 한 번에 가져오는 방식이다.

 

이때 서브쿼리가 사용되며, 메인 엔티티의 모든 연관 데이터를 가져오기 위해 한 번의 추가 쿼리만 실행된다.

 

코드로 예시를 들면 아래와 같다.

import javax.persistence.*;
import java.util.List;

@Entity
public class Author {

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

    private String name;

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    @BatchSize(size = 10)  // 배치 페칭 크기를 설정할 수도 있다.
    @Fetch(FetchMode.SUBSELECT)  // 서브 셀렉트 전략을 적용
    private List<Post> posts;

    // Getters and Setters
}

@Entity
public class Post {

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

    private String title;

    @ManyToOne
    @JoinColumn(name = "author_id")
    private Author author;

    // Getters and Setters
}

위 코드에서 @Fetch(FetchMode.SUBSELECT) 어노테이션을 적용하여 서브셀렉트 전략을 사용한다.

 

이 전략은 부모 엔티티(Author)를 조회할 때, 그와 연관된 자식 엔티티들(Post)을 서브쿼리로 한 번에 가져온다.

 

Database Indexing

 

테이블의 크기가 커질 경우 데이터베이스 인덱스를 설정해 쿼리 성능을 개선할 수 있다.

 

이는 N+1문제를 근본적으로 해결하는 방법은 아니지만, 쿼리 실행 시간을 단축할 수 있다.

 

Caching

 

자주 조회되는 데이터를 캐싱해 쿼리 수를 줄이는 것도 고려할 수 있다.

 

이는 쿼리 실행을 줄여 성능을 향상하지만, 캐싱된 데이터가 변경될 경우 일관성 문제가 발생할 수 있다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
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
글 보관함