| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- buffer
- ㅅ
- 윤성우의 열혈 자료구조
- 이스케이프 문자
- JSON
- Selection Sorting
- 이것이 자바다
- list 컬렉션
- 알기쉬운 알고리즘
- datastructure
- Algorithm
- coding test
- C 언어 코딩 도장
- Serialization
- 혼자 공부하는 C언어
- C programming
- R
- insertion sort
- stream
- Stack
- 윤성우 열혈자료구조
- Graph
- 메모리구조
- s
- Today
- Total
Engineering Note
[JPA] N+1문제가 발생하는 예(주문내역 조회) 본문
주문내력 조회 기능 구현 과정 중 발생한 N+1문제 해결과정을 정리한 글입니다.
문제 정의
JPA 사용시 연관관계가 복잡한 Entity N+1문제
네트워크 오버헤드 및, 과도한 데이터베이스 쿼리로 인한 데이터베이스 부하 증가
페이지네이션 문제, 다수의 데이터 조회시 필요한 페이징처리에서 일대다 조인으로 발생하는 Cartesian Product 문제.
Order 기준 페이징 처리와 OrderItem 기준 페이징 처리 방식이 사용자에게 보여지는 데이터의 신뢰성 문제가 발생
주문 내역 조회는 여러 정보를 한 번에 보여주어야 하는 기능으로 Order Entity, Item Entity, OrderItem Entity, ItemImg Entity의 정보를 조합해서 응답해주어야 합니다. 이 과정에서 발생한 N+1문제를 해결한 과정입니다.
N+1 문제가 발생하면 데이터베이스 성능이 심각하게 저하됩니다. 구체적인 이유는 다음과 같습니다.
1. 과도한 데이터베이스 쿼리 연관된 Entity에 대한 추가쿼리가 발생합니다. 예를 들어,
2. 네트워크 오버헤드 데이터베이스와 애플리케이션 서버 사이의 네트워크 통신이 반복적으로 일어나면서 각 쿼리마다 네트워크 지연(latency)이 누적됩니다. 쿼리 실행 시간 자체보다 이 왕복 시간이 더 큰 병목이 될 수 있습니다.
3. 응답 속도 저하 사용자 입장에서는 페이지 로딩이 느려지고, API 응답 시간이 길어집니다. 데이터가 많아질수록 이 문제는 기하급수적으로 악화됩니다.
4. 데이터베이스 부하 증가 동시 접속자가 많은 경우, 모든 사용자가 이런 비효율적인 쿼리를 발생시키면 데이터베이스 서버에 과부하가 걸려 전체 시스템이 느려지거나 다운될 수 있습니다.
N+1 문제 해결을 위해 고민 과정
프록시로 초기화된 데이터를 실제 데이터로 세팅하기 위한 문제를 해결하기 위해 모든 데이터를 한 번에 Join하는 방식을 고려.
QueryProjection 방식과 Entity를 활용라는 방식이 있는데 현재 기능에서만 사용하는 재사용성이 떨어지는 단점이 있어서 채택하지 않았습니다.
fetch join 으로 주문 내역 조회에 필요한 데이터를 지연로딩이 아니라 즉시로딩처럼 연관된 데이터는 한 번에 조회하는 기능입니다.
하지만 일대다 컬렉션 fetch join은 JPA에서 페이지네이션을 메모리에서 수행하는 문제가 있습니다. 극단적으로 Out Of Memory 문제가 발생할 수 있습니다. 또, 데이터의 신뢰성이 떨어질 수 있습니다. 일대다 조인 테이블을 페이징처리를 하면 ‘다’를 기준으로 페이징이 수행되기 때문에 원하는 조건으로 데이터를 페이지별로 구분할 수 없습니다.
마지지막으로 일대다 컬렉션이 2개 이상 섞인 엔티티는 fetch join을 수행할 수 없습니다.
최종 결정은 Order Entity만 페이지네이션을 적용한 후 조회한 후 나머지 엔티티는 batch size로 조건에 맞는 데이터를 where in 절에 포함시켜 한 번에 조회하도록 했습니다.
구체적인 문제 분석과정
주문 내역 기능을 개발하고 로깅 설정과 테스트를 통해 개발한 코드의 쿼리가 예상보다 많이 실행되는 문제를 발견하였습니다.
N+1문제 발생 코드
@Transactional(readOnly = true)
public Page<OrderHistDto> getOrderList(String email, Pageable pageable) {
log.info("===== 주문 목록 조회 시작 =====");
log.info("1. findOrdersByEmail 호출");
List<Order> orders = orderRepository.findOrdersByEmail(email, pageable);
log.info("2. countOrdersByEmail 호출");
Long totalCount = orderRepository.countOrdersByEmail(email);
List<OrderHistDto> orderHistDtoList = new ArrayList<>();
for(Order order : orders){
log.info("주문별 상품 목록 조회");
OrderHistDto orderHistDto = new OrderHistDto(order);
log.info("3. getOrderItems 호출");
List<OrderItem> orderItems = order.getOrderItems();
for(OrderItem orderItem : orderItems){
log.info("4.findByItemIdAndRepImgYn 호출");
ItemImg itemImg = itemImgRepository.findByItemIdAndRepImgYn(orderItem.getItem().getId(), "Y");
OrderItemDto orderItemDto = new OrderItemDto(orderItem, itemImg.getImgUrl());
orderHistDto.addOrderItemDto(orderItemDto);
}
orderHistDtoList.add(orderHistDto);
}
return new PageImpl<>(orderHistDtoList, pageable, totalCount);
}
테스트 환경
@Test
@DisplayName("주문 목록 조회 테스트")
void getOrderListTest() {
// given
Member member = saveMember();
Item item1 = saveItem();
Item item2 = saveItem();
Item item3 = saveItem();
createItemImg(item1, "/images/item1.jpg");
createItemImg(item2, "/images/item2.jpg");
createItemImg(item3, "/images/item3.jpg");
createOrder(member, item1, 2);
createOrder(member, item2, 1);
createOrder(member, item3, 3);
em.flush();
em.clear();
3개의 상품을 생성하고, 상품별 이미지를 연결하는 코드를 만들고 Member가 주문하는 코드를 작성했습니다.
실행 쿼리 분석
사용자를 기준으로 Order Entity를 조회하는 메서드, 'findOrdersByEmail(email, peageable); 에서 1개의 쿼리가 수행되었습니다.
List<Order> orders = orderRepository.findOrdersByEmail(email, pageable);
select
o1_0.order_id,
o1_0.created_by,
o1_0.member_id,
o1_0.modified_by,
o1_0.order_date,
o1_0.order_status,
o1_0.reg_time,
o1_0.update_time
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
where
m1_0.email=?
order by
o1_0.order_date desc
offset
? rows
fetch
first ? rows only
이후에 반복문을 살펴보면, orderId가 '3'인 OrderItem 목록을 조회하기 위해 또 하나의 쿼리가 수행되었습니다.
select
oi1_0.order_id,
oi1_0.order_item_id,
oi1_0.created_by,
oi1_0.item_id,
oi1_0.modified_by,
oi1_0.order_price,
oi1_0.quantity,
oi1_0.reg_time,
oi1_0.update_time
from
order_item oi1_0
where
oi1_0.order_id=?
이렇게 조회한 OrderItem 리스트에서 대표 이미지를 조회하기 위해
최종 해결
Order Entity에 대해서만 offset, limit으로 페이지네이션을 적용하고 orderItem과 item, itemImg는 Batch Size로 조건절에 맞는 데이터만 조회할 수 있게 해결
'Server > JPA ORM' 카테고리의 다른 글
| [JPA] 고아객체 (0) | 2026.02.06 |
|---|---|
| [JPA] 영속성 전이 CASCADE (0) | 2026.02.06 |
| [JPA] JPA에서 연관관계가 맺어진 객체를 비교(eq)하는 것은, 실제 DB에서는 그 객체의 'ID(PK) 값'을 비교하는 것과 완전히 동일하다. (0) | 2026.01.14 |
| [JPA] 영속성 전이 옵션과 연관관계 편의 메서드가 필요한 이유, 고아객체 설정 (1) | 2026.01.10 |
| [JPA] JPA 활용 주의점과 사용팁 (0) | 2026.01.06 |