반응형
N+1 문제는 주로 JPA를 사용해서 쿼리를 조회할 때 조회하는 객체랑 연관관계가 있는 데이터를 조회할 때 N+1개의 쿼리가 발생해 N+1문제라 한다.
N+1 문제는 데이터베이스와 ORM(Object-Relational Mapping)을 사용할 때 자주 발생하는 성능 문제입니다. 주로 연관 관계가 있는 데이터를 조회할 때, 불필요하게 많은 SQL 쿼리가 실행되는 상황을 의미합니다. 이를 예시와 함께 자세히 설명하겠습니다.
N+1 문제란?
- N: 연관된 엔티티의 개수.
- +1: 부모 엔티티를 조회하기 위한 최초의 쿼리.
- 즉, 부모 엔티티를 조회한 뒤, 연관된 자식 엔티티를 개별적으로 조회하기 위해 N번의 추가 쿼리가 발생하는 문제입니다.
- 주로 ORM 프레임워크(예: Hibernate, JPA, Django ORM 등)에서 LAZY 로딩이나 잘못된 쿼리 설계로 발생합니다.
예시: 쇼핑몰 주문 시스템
쇼핑몰에서 Order(주문)과 OrderItem(주문 항목)의 관계를 생각해 봅시다.
엔티티 구조 (JPA 예시)
@Entity
public class Order {
@Id
private Long id;
private String customerName;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> orderItems;
}
@Entity
public class OrderItem {
@Id
private Long id;
private String productName;
private int quantity;
@ManyToOne
private Order order;
}
- Order와 OrderItem은 1:N 관계입니다.
- fetch = FetchType.LAZY로 설정되어 있어, OrderItem은 필요할 때만 로드됩니다.
문제 상황
모든 주문을 조회하고, 각 주문에 속한 주문 항목(OrderItem)의 상품 이름을 출력하려고 합니다.
List<Order> orders = entityManager.createQuery("SELECT o FROM Order o", Order.class).getResultList();
for (Order order : orders) {
for (OrderItem item : order.getOrderItems()) {
System.out.println(item.getProductName());
}
}
발생하는 쿼리
- 최초 쿼리 (+1): 모든 Order를 조회.
SELECT * FROM Order;
-
- 예: 10개의 주문이 있다고 가정.
- 추가 쿼리 (N): 각 Order의 OrderItem을 조회.
- order.getOrderItems()가 호출될 때마다 Lazy 로딩이 발생하며, 개별적으로 쿼리가 실행됨.
SELECT * FROM OrderItem WHERE order_id = 1;
SELECT * FROM OrderItem WHERE order_id = 2;
...
SELECT * FROM OrderItem WHERE order_id = 10;
-
- 10개의 주문마다 1번씩, 총 10번의 추가 쿼리 발생.
결과
- 총 쿼리 수: 1(최초 조회) + 10(주문별 항목 조회) = 11번.
- 주문이 100개라면? 1 + 100 = 101번.
- 이처럼 데이터가 많아질수록 쿼리 수가 선형적으로 증가하며 성능이 급격히 저하됩니다.
N+1 문제의 문제점
- 성능 저하: 쿼리 실행이 반복되며 데이터베이스 부하가 증가.
- 지연 시간: 네트워크 왕복이 많아져 응답 속도가 느려짐.
- 확장성 부족: 데이터가 많아질수록 문제가 심화됨.
해결 방법
N+1 문제를 해결하려면 연관 데이터를 효율적으로 조회해야 합니다. 주요 방법은 다음과 같습니다.
1. Eager Fetching
- FetchType.LAZY 대신 FetchType.EAGER로 설정.
- 단점: 불필요한 데이터까지 로드될 수 있음.
@OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
private List<OrderItem> orderItems;
2. JOIN FETCH
- JPQL 또는 HQL로 한 번에 조인하여 데이터를 가져옴.
List<Order> orders = entityManager.createQuery(
"SELECT o FROM Order o JOIN FETCH o.orderItems", Order.class
).getResultList();
쿼리:
SELECT o.*, oi.* FROM Order o INNER JOIN OrderItem oi ON o.id = oi.order_id;
결과: 단일 쿼리로 해결.
3. Entity Graph
- JPA의 @EntityGraph를 사용해 연관 데이터를 미리 로드.
@NamedEntityGraph(name = "Order.withOrderItems", attributeNodes = @NamedAttributeNode("orderItems"))
@Entity
public class Order { ... }
Order order = entityManager.find(Order.class, 1L,
Collections.singletonMap("javax.persistence.fetchgraph", entityManager.getEntityGraph("Order.withOrderItems")));
4. Batch Fetch
- Hibernate의 @BatchSize를 사용해 N개의 쿼리를 한 번에 묶음.
@OneToMany(mappedBy = "order")
@BatchSize(size = 10)
private List<OrderItem> orderItems;
쿼리:
SELECT * FROM OrderItem WHERE order_id IN (1, 2, 3, ..., 10);
해결 후 결과
- JOIN FETCH 사용 시: 단일 쿼리로 모든 데이터를 가져오므로 쿼리 수는 1번.
- 성능이 크게 개선되고, 데이터베이스 부하가 줄어듦.
요약
- N+1 문제: 연관 데이터를 개별적으로 조회하며 쿼리가 N+1번 발생.
- 예시: 주문 10개 → 쿼리 11번.
- 해결: JOIN FETCH, Eager Fetching, Entity Graph, Batch Fetch 등으로 최적화.
- 팁: 개발 시 쿼리 로그를 확인하며(예: spring.jpa.show-sql=true), N+1 문제를 조기에 탐지
반응형
'백엔드' 카테고리의 다른 글
ORM을 사용하는 이유 (3) | 2025.04.23 |
---|---|
[Kafka] 실시간 채팅에 카프카 적용하기 (0) | 2025.02.28 |
Spring Gradle Error - Cause: zip END header not found (0) | 2024.07.18 |
JAVA (1) (0) | 2024.06.05 |