안녕하세요
JPA를 공부하다가 가장 많은 문제가 발생하는
N + 1 문제에 대해서 설명해보겠습니다.
N+1 문제란?
연관 관게에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 된다. 이를 N+1 문제라고 한다. 그러면 실제로 어느 경우에 발생하는지 사례를 통해서 알아보자.

간단하게 Member와 Order는 1대 다 관계로 연관되어 있고, Order와 Delivery는 1대 1일 관계로 연관되어있습니다.
또한 Order와 OrderItem은 1대 다 관계로 연관되어있습니다.
[Member]
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
@Column(name ="member_id")
private Long id;
private String name;
@Embedded
private Address address;
@JsonIgnore
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
[Order]
@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name ="member_id")
private Member member; //주문 회원
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery; //배송정보
private LocalDateTime orderDate; //주문시간
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus; //주문 상태[Order,CANCEL]
//==연관관계 메서드==//
public void setMember(Member member) {
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery) {
this.delivery = delivery;
delivery.setOrder(this);
}
}
[OrderItem]
@Entity
@Table(name="order_item")
@Getter @Setter
public class OrderItem {
@Id @GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item; //주문 상품
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order; //주문
private int orderPrice; // 주문 가격
private int count; //주문 수량
}
[Delivery]
@Entity
@Getter
@Setter
public class Delivery {
@Id
@GeneratedValue
@Column(name = "delivery_id")
private Long id;
@OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
@JsonIgnore
private Order order;
@Embedded
private Address address;
@Enumerated(EnumType.STRING) //타입은 반드시 String으로!
private DeliveryStatus status; //ENUM[READY(준비), COMP(배송)]
}
[OrderDto]
@Data
static class OrderDto{
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getOrderStatus();
address = order.getDelivery().getAddress();
}
}
"전체 주문내역을 조회한다 거기에는 주문자 이름, 주문 날짜, 주문상태, 주소지를 출력해야한다."


[Query Check]

겉으로 보기에는 문제가 없어보이지만, getMember()와 getDelivery()가 호출되고 나면 저희가 의도치 않은 무수한 쿼리들을 보실 수 있습니다.
어쩌면 최악의 경우에는 1000개의 사용자와 1000개 Delivery가 있으면 select가 1 + 1000 + 1000개의 무수한 쿼리가 발생을 해서 성능 저하를 일으킵니다. 이러한 문제를 N+1 문제라 합니다.
N+1은 왜 발생하는 것일까?
repository에서 정의한 메서드를 실행하면 JPA는 메서드 JPQL를 생성하여 실행하게 됩니다, JPQL은 SQL을 추상화한 객체지향 쿼리 언어로서 특정 SQL에 종속되지 않고 엔티티 객체와 필드 이름을 가지고 쿼리를 실행합니다. 그렇게 때문에 JPQL은 findAll()이란 메서드를 수행하였을 때 해당 엔티티를 조회하는 select * from Order 쿼리만 실행이 되는 것이다. JPQL 입장에서는 연관관계 데이터를 무시하고 해당 엔티티 기준으로 쿼리를 조회하기 때문이다. 그렇기 때문에 연관된 엔티티 데이터가 필요한 경우 FetchType으로 지정한 시점에 조회를 별도로 호출하게 됩니다.

여기서 JOIN을 해서 Member와 Delivery를 들고 오는데, Lazy 지연로딩으로 되어있어서 Proxy로 가져왔고, 이후 이를 강제로 초기화했다고 볼 수 있습니다. select를 안했기 때문에 Member와 Delivery는 프록시로 가져온 것 입니다.
N+1 문제의 영향이 뭔가요?
N+1로 인한 문제는 짐작하고 있지만 '성능'과 매우 연관되어있습니다.
여러번 select 쿼리가 발생하면 이로 인해 담당한 서비스의 장애로 이어지게 됩니다.
성능도 성능이지만 우리가 의도치 않은 SQL이 호출되고 있으니 만족스럽지가 않습니다.
어떻게 해결함?
해결하는 방법은 패치조인, EntityGraph, DTO로 조회하는 방법이 있습니다.
그건 다음 블로그 포스팅에서 정리해보도록 하겠습니다.
'java and sping' 카테고리의 다른 글
| [Spring] JPA의 OSIV의 특징 (0) | 2023.06.10 |
|---|---|
| [Spring] JPA의 Fetch Join에 대해서? (0) | 2023.05.30 |
| [Spring] JPA @MappedSuperclass 사용법 (0) | 2023.05.04 |
| [Spring] JPA의 상속관계 (0) | 2023.05.03 |
| JPA 연관관계(단방향/양방향 , 다중성) (0) | 2023.05.02 |