티스토리 뷰
N+1 문제는 데이터베이스를 사용하는 애플리케이션에서 자주 발생하는 성능 문제 중 하나로,
특히 ORM(Object-Relational Mapping) 도구를 사용할 때 자주 나타난다.
이 문제는 하나의 메인 쿼리를 실행한 후에, 그 결과로 얻어진 N개의 레코드 각각에 대해
추가적인 쿼리(N개의 쿼리)를 실행하는 상황에서 발생하며, 불필요하게 많은 쿼리를 보내게 되어 성능 저하를 초래한다.
요약하자면 N+1 문제는 다음과 같은 상황에서 발생한다.
- N개의 항목을 가져오는 쿼리를 실행한다.
- 각각의 항목에 대해 추가로 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문제가 성능 문제로 이어질까?
당연하다면 당연하게도 아래와 같은 이유가 있다.
- 많은 쿼리 실행: 데이터베이스에 보내는 쿼리의 수가 기하급수적으로 증가하여, 네트워크 오버헤드와 데이터베이스의 부하가 증가
- 지연 시간 증가: 각 쿼리는 데이터베이스에 요청을 보내고 응답을 받아오는 데 시간이 걸리며, 쿼리의 수가 많아질수록 전체 지연 시간이 증가한다.
- 비효율적인 자원 사용: 한 번에 여러 데이터를 가져올 수 있음에도 불구하고, 불필요하게 여러 번 데이터베이스에 접근하여 동일한 작업을 반복하게 된다.
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
자주 조회되는 데이터를 캐싱해 쿼리 수를 줄이는 것도 고려할 수 있다.
이는 쿼리 실행을 줄여 성능을 향상하지만, 캐싱된 데이터가 변경될 경우 일관성 문제가 발생할 수 있다.
'Development > Technical Interview' 카테고리의 다른 글
[면접 준비]SQL에서 UNION/UNION ALL 차이 (0) | 2024.07.31 |
---|---|
[면접 준비]로컬 스토리지, 쿠키, 세션 (3) | 2024.07.20 |
[면접 준비 - Spring]@Service와 @Repository (0) | 2024.06.03 |
[면접 준비 - Spring]@Controller와 @RestController (0) | 2024.06.03 |
[면접 준비 - Java]접근 제어자(Access Modifier) (0) | 2024.06.03 |
[면접 준비 - CS]프로세스 스케줄러 (0) | 2023.05.02 |
- Total
- Today
- Yesterday
- 알고리즘
- 남미
- 중남미
- 맛집
- 유럽여행
- 여행
- 리스트
- spring
- 칼이사
- 기술면접
- 세모
- 동적계획법
- java
- 스트림
- Algorithm
- a6000
- Python
- 백준
- RX100M5
- 스프링
- 면접 준비
- 지지
- 파이썬
- Backjoon
- 자바
- 야경
- 세계일주
- 유럽
- 세계여행
- BOJ
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |