JPA N+1 문제
@Entity
public class Item {
@Id
private Long id;
@ManyToOne(fetch = FetchType.EAGER) // 기본값
private Order order;
}
✅ EAGER인 경우 실행되는 쿼리
Item 엔티티 조회:
SELECT * FROM item WHERE id = 1;
연관된 Order도 즉시 조회:
SELECT * FROM orders WHERE id = ?; -- item.order_id 에 해당하는 값
2. N+1 문제
List<Item> items = itemRepository.findAll(); // Item 100개
이럴 경우,
Item
리스트 조회 1번각 Item에 대해 Order 조회 100번 → 총 101번 쿼리 발생 (바로 이게 N+1 문제)
즉,
"Item만 조회한 건데, 각각에 딸린 Order를 100번 쿼리 날려서 가져오는 것"이
@ManyToOne
에서의 N+1 문제
✅ 1. @OneToMany
관계란?
@OneToMany
관계란?예시:
@Entity
public class Order {
@Id
private Long id;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> orderItems = new ArrayList<>();
}
하나의
Order
는 여러 개의OrderItem
을 가질 수 있는 관계mappedBy = "order"
는OrderItem
쪽에 외래 키(@ManyToOne
)가 있다는 뜻fetch = FetchType.LAZY
는 이 리스트를 즉시 로딩하지 않고, 실제로 접근할 때 DB에서 SELECT 쿼리를 날리겠다는 의미
✅ 2. @OneToMany
는 기본이 LAZY, 그 이유는?
@OneToMany
는 기본이 LAZY, 그 이유는?💥 EAGER
로 하면?
EAGER
로 하면?만약 @OneToMany(fetch = FetchType.EAGER)
를 쓴다면:
Order order = orderRepository.findById(1L).get();
이 쿼리 하나만으로 끝나지 않고, 내부적으로 아래처럼 조인 쿼리가 자동 실행됩니다:
SELECT * FROM orders o
LEFT JOIN order_items oi ON o.id = oi.order_id
WHERE o.id = 1;
문제점:
항상 조인이 발생 → 성능 저하
조인 결과가 많아지면 → 데이터 중복 (N개
OrderItem
이 있을 경우Order
데이터도 N번 반복됨)컬렉션의 경우
EAGER
는 Hibernate에서도 비추하는 전략
그래서 JPA는 기본값을 LAZY로 설정해둔 것.
✅ 3. LAZY 동작 방식
Order order = orderRepository.findById(1L).get(); // 여기서는 orderItems는 아직 조회 안 됨
order.getOrderItems(); // 이 시점에 쿼리 날림
이때
orderItems
필드는 Hibernate의PersistentBag
,ListProxy
같은 프록시 객체로 초기화되어 있음이 프록시는
.get()
이나.size()
같은 메서드가 호출될 때 진짜 DB 쿼리를 실행
✅ 4. Lazy 로딩의 문제점
❗ LazyInitializationException
@Transactional
public Order getOrder(Long id) {
return orderRepository.findById(id).get(); // orderItems는 아직 로딩 안 됨
}
// 트랜잭션 밖에서 호출
Order order = service.getOrder(1L);
order.getOrderItems().size(); // ❌ 여기서 LazyInitializationException 발생
트랜잭션이 종료된 후 프록시가 실제 데이터를 로딩하려 하면 실패 →
LazyInitializationException
✅ 5. 해결 방법
① Fetch Join
@Query("SELECT o FROM Order o JOIN FETCH o.orderItems WHERE o.id = :id")
Order findByIdWithItems(@Param("id") Long id);
한 번에 필요한 엔티티 + 연관 엔티티를 함께 조회
단점: 페이징 불가능 (
@OneToMany
컬렉션 조인은LIMIT
과 호환되지 않음)
이렇게 하면:
SELECT i.*, o.* FROM item i
JOIN order o ON i.order_id = o.id
✔ Item과 Order를 한 번의 쿼리로 모두 가져옴 (Join + 즉시 조회) ✔ Hibernate가 order 필드에 대해 추가 쿼리 없이 자동으로 채워줌
② DTO 직접 조회
@Query("SELECT new com.example.dto.OrderDto(o.id, i.name, i.price) " +
"FROM Order o JOIN o.orderItems i WHERE o.id = :id")
List<OrderDto> findOrderDetails(@Param("id") Long id);
엔티티 대신 필요한 필드만 조회하여 DTO로 매핑
페이징 가능, 성능 최적화에 가장 유리
③ Hibernate 배치 사이즈 설정
spring.jpa.properties.hibernate.default_batch_fetch_size=100
여러 개의 지연 로딩 컬렉션을 한 번에 모아서 쿼리 날림
N+1 → 1+1 쿼리로 최적화 가능
Last updated