[스탠다드] N+1 문제

토리·2025년 2월 24일
0

강의 목표

  • N+1 쿼리 문제의 정의와 발생 원인을 명확히 이해한다.
  • N+1 문제가 데이터베이스 성능과 애플리케이션 반응 속도에 미치는 영향을 학습한다.
  • 문제를 진단하고 해결하는 다양한 방법을 습득한다.
  • 실제 코드 예제를 통해 문제를 식별하고 최적화 방법을 적용하는 실습을 진행한다.

N+1 문제란?

  • 기본 정의: N+1 문제는 ORM(Object-Relational Mapping)을 사용할 때 발생하는 퍼포먼스 문제
    - 하나의 쿼리로 N개의 객체를 로딩한 후, 각 객체에 연관된 데이터를 추가로 조회하는 개별 쿼리가 N번 실행되면서 총 N+1번의 쿼리가 발생하는 문제
    - N+1 문제는 단순히 "하나의 쿼리가 더 실행된다"는 문제가 아니라, 시스템의 확장성과 성능에 심각한 영향을 미칠 수 있는 구조적 문제
    - 요청이 동시에 여러 사용자로부터 발생한다면, 데이터베이스는 수백,수천,수만개의 쿼리를 추가 처리!

해결 방법

1. JOIN FETCH 사용

  • 관계가 있는 엔티티를 한 번의 쿼리로 함께 로드해야 할 때 사용
  • JOIN FETCH를 사용하면 한 번의 쿼리로 연관된 엔티티들을 함께 로드할 수 있음
  • 이 방법은 FetchType.EAGER로 설정된 것과 유사한 효과를 내지만, 쿼리를 명시적으로 제어할 수 있다는 장점
  • 권장 여부 → 매우 추천. 필요한 모든 데이터를 포함한 효율적인 쿼리를 생성하여 성능을 크게 향상!
public interface BookRepository extends JpaRepository<Book, Long> {
    @Query("SELECT b FROM Book b JOIN FETCH b.reviews WHERE b.id = :id")
    Book findBookWithReviewsById(Long id);
}

2. 배치 사이즈 설정

  • 대량의 연관 데이터를 로드할 때 N+1 쿼리 수를 줄이기 위해 사용
  • 완전한 해결책은 아님 → 배치 사이즈는 쿼리의 수를 줄이지만, 여전히 여러 쿼리가 필요. 완벽한 해결을 위해서는 JOIN FETCH나 다른 데이터 로딩 전략을 고려
  • ORM 설정에서 @BatchSize 어노테이션을 사용하여 한 번에 로드할 연관 엔티티의 수를 조정할 수 있음
  • 이 방법은 많은 수의 엔티티를 처리할 때 유용하며, 너무 많은 쿼리가 발생하는 것을 줄일 수 있음
  • 권장 여부 → 사용 환경에 따라 선택. 데이터 양과 성능 요구 사항에 따라 적절한 배치 크기 설정이 필요

@Entity
@Table(name = "books")
public class Book {
    @OneToMany(mappedBy = "book", fetch = FetchType.LAZY)
    @BatchSize(size = 10)
    private List<Review> reviews;
}

이 설정은 Review를 로드할 때 최대 10개의 Book에 대한 리뷰를 한 번에 가져옴

3. DTO(Data Transfer Object) 사용

  • 뷰나 API 응답으로 필요한 데이터만 선택적으로 전달
  • 필요한 데이터만 선택적으로 로드하기 위해 DTO를 사용
  • 불필요한 데이터를 로드하지 않아 성능을 향상
  • 권장 여부 → 매우 추천. 불필요한 데이터 로드를 방지하여 성능을 개선
public class BookDetailDto {
    private String title;
    private String author;
    private List<String> reviewContents;

    public BookDetailDto(String title, String author, List<String> reviewContents) {
        this.title = title;
        this.author = author;
        this.reviewContents = reviewContents;
    }
}

public interface BookRepository extends JpaRepository<Book, Long> {
    @Query("SELECT new com.example.dto.BookDetailDto(b.title, b.author, r.content) FROM Book b JOIN b.reviews r")
    List<BookDetailDto> findAllBookDetails();
}

4. Entity Graphs 사용

  • Entity Graph를 사용하면 특정 쿼리에 대한 엔티티의 로딩 전략을 세밀하게 제어
  • JPA 2.1부터 지원!
  • 권장 여부 → 유연성을 제공하므로 추천, 특히 복잡한 도메인 모델에서 유용
public interface BookRepository extends JpaRepository<Book, Long> {
    @EntityGraph(attributePaths = {"reviews"})
    List<Book> findAll();
}

findAll 메소드를 호출할 때 Book의 reviews를 즉시 로드
attributePaths 속성에 "reviews"를 지정함으로써 Book 엔티티를 조회할 때 연관된 
Review 엔티티들을 즉시 로드.
이렇게 설정함으로써, 각 Book 엔티티에 대해 별도로 Review를 로드하는 쿼리를 실행하지 않아도, 
한 번의 쿼리로 필요한 모든 데이터를 가져옴

5. FetchType.EAGER 사용

  • 연관된 엔티티가 항상 필요한 경우, 미리 로드하여 지연이 발생하지 않도록 함
  • 권장 여부 → 일반적으로 권장하지 않음. EAGER 로딩은 너무 많은 데이터를 불필요하게 로드할 수 있으며, 특히 많은 연관 관계가 있는 경우 성능 저하를 초래
해결 방법사용 사례권장 여부주요 특징주의점
JOIN FETCH관계가 있는 엔티티를 한 번의 쿼리로 함께 로드할 때매우 추천한 번의 쿼리로 필요한 모든 데이터 로드반환되는 데이터의 양이 많아질 수 있음
배치 사이즈 설정대량의 연관 데이터를 로드할 때상황에 따라 선택N+1 쿼리 수를 줄임, 근본적 해결법은 아니고 그냥 성능 향상법적절한 배치 크기를 설정해야 함
DTO 사용뷰나 API 응답으로 필요한 데이터만 전달할 때매우 추천불필요한 데이터 로드 방지데이터 변환 과정이 필요함
Entity Graphs특정 쿼리에서 필드 로드 방식을 제어할 때추천쿼리 세밀 제어 가능복잡한 설정이 필요할 수 있음
FetchType.EAGER연관된 엔티티가 항상 필요한 경우권장하지 않음연관 엔티티를 미리 로드하여 지연 없음불필요한 데이터 로드로 성능 저하 가능성
profile
안녕하세요. 토리입니다.

0개의 댓글