728x90

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 문제를 해결 하였습니다.

728x90