JPA 성능 최적화 - N + 1 문제 해결 하기
JPA 성능 최적화
JPA 를 사용하면서 병목이 발생하는 구간 의 90% 는 조회에서 발생한다
그도 그럴것이 사실 insert 나 update delete 같은 method 들은 단건에 대해한 것들이 대부분이다.
하지만 select 로 여러 테이블을 join 하여 데이터를 가져올때 시간이 되게 오래 걸리는 경우 가 많다.
jpa 에서는 조금 과장을 보태여 이 개념만 잘 알고 있어도 90%는 해결 할 수 있을 것처럼 보인다.
그게 무슨 문제인지 알아보자
회원 이 있고 그 회원은 책을 주문 할 수 있고 배송 정보가 담긴 테이블이 있다고 가정한다.
Member.java
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
@NotEmpty(message = "required")
private String name;
@Embedded
private Address address;
@JsonIgnore
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
Order.java
@Entity
@Table(name = "orders")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
private LocalDateTime orderDate; //주문시간
@Enumerated(EnumType.STRING)
private OrderStatus status; //주문상태 [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);
}
//==생성 메서드==//
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {
Order order = new Order();
order.setMember(member);
order.setDelivery(delivery);
for (OrderItem orderItem : orderItems) {
order.addOrderItem(orderItem);
}
order.setStatus(OrderStatus.ORDER);
order.setOrderDate(LocalDateTime.now());
return order;
}
//==비즈니스 로직==//
/**
* 주문 취소
*/
public void cancel() {
if (delivery.getStatus() == DeliveryStatus.COMP) {
throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for (OrderItem orderItem : orderItems) {
orderItem.cancel();
}
}
//==조회 로직==//
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice() {
int totalPrice = 0;
for (OrderItem orderItem : orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
}
Delivery.java
@Entity
@Getter @Setter
public class Delivery {
@Id @GeneratedValue
@Column(name = "delivery_id")
private Long id;
@JsonIgnore
@OneToOne(mappedBy = "delivery", fetch = LAZY)
private Order order;
@Embedded
private Address address;
@Enumerated(EnumType.STRING)
private DeliveryStatus status; //READY, COMP
}
Jpa 에서는 성능 최적화를 위해
@ManyToOne(fetch = LAZY)
XXToOne 에 fetch value 를 Lazy 로 주었다. 기본값은 Eager 인데 Eager 로 하게되면 데이터베이스 조회시 점에 연관관계 매핑된 객체를 다 조회하여 들고온다.
Lazy로 하게되면
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member = new ProxyBuddy();
Proxy 객체가 member 객체를 상속받아 가짜 값을 넣어뒀다가 Order.getMember().getName() 즉 멤버 객체에 접근 할때 직접 쿼리를 날린다.
문제는 요기서 발생한다.
@GetMapping("/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<SimpleOrderDto> result = orders.stream().map(SimpleOrderDto::new).collect(Collectors.toList());
return result;
}
order 를 모두 불러와 return 해주는 controller 이다 예시를 들기위해 return 값은 간소화 했습니다.
처음 order 를 전체 조회하는 query 를 날립니다. 그다음 stream 으로 order 의 size 만큼 map 을 돌면서 orderDto 를 생성합니다.
@Data
static class SimpleOrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
public SimpleOrderDto(Order order) {
this.orderId = order.getId();
this.name = order.getMember().getName();
this.orderDate = order.getOrderDate();
this.orderStatus = order.getStatus();
this.address = order.getDelivery().getAddress();
}
}
orderDto 에서 member 와 delivery 에 대한 관계에 접근하려고 하는데 data 가 없기 때문에 query 를 날려야 합니다.
하지만 요기서 query 를 날릴 때 현재 order 에 대한 id 의 값을 기준으로 단건 query 를 날리기 때문에
order 가 총 2개라면 order 전체를 가져오는 query 1번 + member 2 번 + delivery 2 번 = 총 5번 의 query 가 발생하게 됩니다.
그렇다고 Eager 로 변경 하자니 추후에 성능 최적화를 하기에 제약 사항이 아주 많습니다.
Fetch join 사용하기
이 문제를 해결 하기 위해 Fetch join 을 사용하여 한번의 query 로 연관관계를 다 join 하여 가져 옵니다.
public List<Order> findAllWithMemberDelivery() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class
).getResultList();
}
이렇게 하여 N + 1 문제를 해결 하였습니다.
'Java > java - spring' 카테고리의 다른 글
상속 관계 매핑 Entity 를 QueryDsl 에서 사용하기 (0) | 2022.05.23 |
---|---|
Failed to start bean 'documentationPluginsBootstrapper' 오류 해결 (0) | 2022.03.02 |
Spring boot 에서 Fire base storage 연동 하여 사용하기 (1) | 2022.01.25 |
Aop 를 활용하여 binding result 처리 공통화 하기 (0) | 2022.01.24 |
Spring Bean 주입하기 (0) | 2021.12.08 |
댓글
이 글 공유하기
다른 글
-
상속 관계 매핑 Entity 를 QueryDsl 에서 사용하기
상속 관계 매핑 Entity 를 QueryDsl 에서 사용하기
2022.05.23 -
Failed to start bean 'documentationPluginsBootstrapper' 오류 해결
Failed to start bean 'documentationPluginsBootstrapper' 오류 해결
2022.03.02 -
Spring boot 에서 Fire base storage 연동 하여 사용하기
Spring boot 에서 Fire base storage 연동 하여 사용하기
2022.01.25 -
Aop 를 활용하여 binding result 처리 공통화 하기
Aop 를 활용하여 binding result 처리 공통화 하기
2022.01.24