강의 목표
- 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 | 연관된 엔티티가 항상 필요한 경우 | 권장하지 않음 | 연관 엔티티를 미리 로드하여 지연 없음 | 불필요한 데이터 로드로 성능 저하 가능성 |