N+1 문제 ?
- 데이터베이스 쿼리 최적화와 관련된 성능문제
- 주로 ORM(Object-Relational Mapping) 도구(JPA, Hibernate 등)를 사용할 때 발생되며,
한 번의 데이터 조회로 충분한 정보를 가져올 수 있음에도 N개의 추가적인 쿼리가 실행되는 비효율적인 상황 - 1번의 메인 쿼리 + N번의 추가 쿼리 = 총 N + 1번의 쿼리
- 성능 저하
- 데이터가 많을수록 데이터베이스에 과도한 부하 발생
- 특히 대량의 데이터를 조회할 때 성능 문제가 크개 발생
- 네트워크 지연
- 데이터베이스와 애플리케이션 간 통신량이 증가하여 요청 처리 시간이 늘어남
- 성능 저하
N+1 문제의 동작 원리
- 쿼리 실행 과정
- 먼저 1개의 메인 쿼리로 데이터 리스트를 가져옵니다.
- 가져온 데이터 각각에 대해 추가로 1개의 쿼리가 실행되어,
결과적으로 N개의 추가 쿼리가 발생합니다.
N+1 문제 예제
- Post : Comment = 1 : N
@Entity
public class Post {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
private List<Comment> comments = new ArrayList<>();
}
@Entity
public class Comment {
@Id @GeneratedValue
private Long id;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
}
- 문제 상황 : Post 리스트와 각 Post의 Comment를 조회
- Post가 100개라면 : 1 + 100 = 101번 쿼리가 수행된다.
List<Post> posts = postRepository.findAll(); // 전체 조회를 위한 findAll() 호출 : SELECT * FROM post;
for (Post post : posts) {
System.out.println(post.getTitle());
for (Comment comment : post.getComments()) {
// 각 Post에 대한 getComments() 호출 = 각 Post마다 Comment를 가져오기 위한 N개의 추가 쿼리 실행
// SELECT * FROM comment WHERE post_id = 1;
// SELECT * FROM comment WHERE post_id = 2;
// SELECT * FROM comment WHERE post_id = 3;
// ...
System.out.println(comment.getContent());
}
}
N + 1 문제 해결 방법
1. Fetch Join 사용
- JPA의
fetch join
을 사용해 필요한 데이터를 한 번의 쿼리로 Post와 Comment를 가져옵니다.
@Query("SELECT p FROM Post p JOIN FETCH p.comments")
List<Post> findAllWithComments();
SELECT p.*, c.*
FROM post p
JOIN comment c ON p.id = c.post_id;
- 특징
- JPA의
JOIN FETCH
구문을 사용하여 연관된 Entity를 즉시 로딩(Eager Fetch)합니다.- 즉시 로딩 : JPA(Hibernate)에서 entity를 조회할 때, 연관된 모든 데이터(entity 나 collection)를 즉시 함께 로딩하는 방식
- 여러 연관 데이터를 한 번의 쿼리로 가져옵니다.
- JPA의
장점
- 한 번의 쿼리로 연관 데이터 로딩
- 모든 데이터를 한 번에 가져오므로 N+1문제가 발생하지 않음
- 직관적이고 간단
- 쿼리가 명확해 복잡한 설정 없이 해결 가능
단점
- 데이터 중복
- Post가 동일한 여러 Comment와 연관되어 있다면,
Post 데이터가 중복으로 반환될 수 있음. - 이를 해결하기 위해선 반환 결과를 수동으로 처리해야 함 (DISTINCT 사용)
- Post가 동일한 여러 Comment와 연관되어 있다면,
- 쿼리 복잡성 증가
- 연관 관계가 많아질수록 쿼리가 복잡해지고 데이터베이스 성능이 저하될 수 있다.
- 페이징 제한
- Fetch Join을 사용하면 JPA에서 페이징이 제대로 동작하지 않을 수 있음.
2. EntityGraph 사용
@EntityGraph
를 통해 연관 데이터를 한 번의 쿼리로 조회
@EntityGraph(attributePaths = {"comments"})
@Query("SELECT p FROM Post p")
List<Post> findAllWithComments();
- 특징
- JPA의 @EntityGraph를 사용해 연고나 데이터를 명시적으로 로딩
- Fetch Join과 같이 즉시로딩하지만, JPQL 대신 어노테이션 기반으로 간단히 정의할 수 있다.
장점
- JPA 표준 사용
- JPQL없이 어노테이션으로 연관 데이터 로딩 설정 가능
- 모듈화
- 엔티티 수준에서 로딩 전략을 정의하므로 재사용성 높음
- 페이징 지원
- Fetch Join과 달리 페이징이 제대로 동작
단점
- 복잡한 관계에서는 설정 번거로움
- 다중 연관 관계가 있는 경우 attributePaths에 모든 경로를 지정해야 함
- 쿼리 최적화 제약
- 복잡한 조건을 포함한 커스텀 쿼리에는 Fetch Join보다 유연하지 못함.
3. Batch Size사용
- Hibernate의
@BatchSize
를 사용해 연관 데이터를 묶어서 가져옵니다.- 최대 10개의 Post마다 한 번의 쿼리로 Comment를 가져옵니다.
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
@BatchSize(size = 10)
private List<Comment> comments;
- 특징
- @BatchSize를 사용해 지연 로딩(Lazy Loading)을 개선
- 연관된 데이터를 일정한 크기로 묶어 한 번에 로드함.
- Hibernate가 여러 데이터를 한꺼번에 가져오도록 최적화
장점
- 적당한 쿼리 수
- 데이터가 많은 경우 Fetch Join보다 쿼리 수를 줄이면서 메모리 사용량도 낮춤
- 지연 로딩 유지
- 필요한 데이터만 가져와 불필요한 데이터 로딩 방지
단점
- 여전히 여러 N개의 쿼리 실행
- Fetch Join처럼 한 번의 쿼리로 데이터를 가져오지 못하며,
N개의 쿼리는 아니지만 N / Batch Size 만큼 발생
- Fetch Join처럼 한 번의 쿼리로 데이터를 가져오지 못하며,
- 복잡한 설정
- 각 연관 관계에 대해 Batch Size를 설정해야 하며
기본(default)값이 없으므로 따로 관리해야함.
- 각 연관 관계에 대해 Batch Size를 설정해야 하며
4. DTO로 필요한 데이터만 조회
- 연관된 엔티티를 가져오는 대신 필요한 데이터를 DTO(Data Transfer Object)로 매핑하여 조회
@Query("SELECT new com.example.PostCommentDto(p.title, c.content) FROM Post p JOIN p.comments c")
List<PostCommentDto> findPostWithComments();
- 특징
- JPQL 또는 Native Query를 사용해 필요한 데이터만 선택적으로 조회하여 DTO에 매핑
장점
- 최소한의 데이터 로딩
- 필요한 필드만 로딩하여 메모리와 네트워크 자원 절약
- 복잡한 관계에서도 유연
- 다양한 조건 및 계산을 포함한 쿼리 작성 가능
단점
- 유연성 제한
- DTO가 정의되지 않은 추가 데이터를 사용하려면 쿼리를 재작성해야 함.
- 쿼리 유지보수 어려움
정리
Fetch Join | EntityGraph | Batch Size | DTO 조회 | |
장점 | 한 번의 쿼리로 간단히 문제 해결 | * JPA 표준 방식 * 페이징 지원 |
* 적절한 쿼리 수 유지 * 지연 로딩 유지 |
* 필요한 데이터만 로딩 * 메모리 효율적 사용 |
단점 | * 데이터 중복 조회 가능성 * 페이징 제한 |
복잡한 연관 관계 설정이 번거로움 | * 여전히 N / Batch Size개 만큼 여러 쿼리 발생 * 설정 관리 필요 |
* 유연성 부족 * 쿼리 유지보수 어려움 |
적합한 상황 | 데이터 크기가 작고, 페이징이 필요 없을 때 |
페이징이 필요하며, 간단한 연관 관계일 때 |
대량 데이터를 효율적으로 로딩할 때 | 복잡한 계산이나 특정 필드만 필요할 때 |
반응형
'Computer Science > Database' 카테고리의 다른 글
[Database] ERD 식별 - 비식별 관계 (1) | 2024.12.01 |
---|