본문 바로가기
JAVA/JPA

[JPA] JPA 컬렉션 조회 성능 최적화 #3 - JPA에서 DTO 직접 반환

by 개미가되고싶은사람 2024. 12. 14.

@BatchSize의 단점은 데이터베이스의 부하, 메모리 사용량의 동일성 등등 있습니다. @BatchSize을 1000으로 설정하면 한 번에 1000개의 데이터를 DB에서 애플리케이션으로 불러오게 되어 DB에 순간적으로 큰 부하가 발생할 수 있지만, 100으로 설정하면 10번의 쿼리를 통해 데이터를 가져와 부하가 분산될 수 있습니다. 하지만 애플리케이션은 전체 데이터를 로딩해야 하므로 메모리 사용량은 동일하며, 1000으로 설정하는 것이 좋을 수 있지만 DB와 애플리케이션 모두 순간 부하를 얼마나 견딜 수 있는지가 중요합니다. 

 

그럼 DB, 애플리케이션에 많은 부하를 주지 않고
많은 데이터를 어떻게 한번에 처리하는지 알아보겠습니다.

 

 

DTO 및 엔티티 코드

더보기
@Entity
@Getter
@Setter
public class Order {
    @Id
    @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id")
    private Member member; 

    @OneToOne(cascade = CascadeType.ALL, fetch = LAZY)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery; 

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>(); // 1:N

    private LocalDateTime orderDate; 

    @Enumerated(EnumType.STRING)
    private OrderStatus status; 
}
@Entity
@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; 
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    @JsonIgnore
    private Order order; 

    private int orderPrice; 
    private int count; 
}
@Data
public class OrderQueryDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    private List<OrderItemQueryDto> orderItems; // 1:N 관계

    public OrderQueryDto(
            Long orderId, String name,
            LocalDateTime orderDate, OrderStatus orderStatus,
            Address address) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }
}

 

@Data
public class OrderItemQueryDto {
    @JsonIgnore
    private Long orderId;

    private String orderName;
    private int orderPrice;
    private int count;

    public OrderItemQueryDto(Long orderId, String orderName, int orderPrice, int count) {
        this.orderId = orderId;
        this.orderName = orderName;
        this.orderPrice = orderPrice;
        this.count = count;
    }
}

 

1. xxToOne 관계 먼저 조회

private List<OrderQueryDto> findOrders() {
        return em.createQuery(
                        "select new jpabook.jpashop.repository.order.query.OrderQueryDto" +
                                "(o.id, m.name, o.orderDate, o.status, d.address) " +
                                " from Order o " +
                                " join o.member m " +
                                " join o.delivery d ", OrderQueryDto.class)
                .getResultList();
}

xxToOne 관계은 fetch join을 사용합니다. xxToOne 관계는 fetch join을 사용해도 레코드 수를 증가시키지 않습니다. 왜냐하면 xxToOne 관계의 관련된 값이 항상 하나이기 때문입니다. 따라서 쿼리를 실행해도 결과로 나오는 레코드 수는 변하지 않습니다.  해당 쿼리에서 fetch join 대신 join을 사용한 이유는 fetch join은 엔티티를 반환할 때 만 사용가능하므로 join을 사용했습니다. 어떻게 보면 fetch join은 join과 같아서 동일하게 사용해도 됩니다.(그렇다고 fetch join과 join이 같다고 생각하시면 안됩니다.)

 

2. DTO를 이용해 컬렉션을 Map으로 조회

private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
        List<OrderItemQueryDto> orderItems = em.createQuery(
                        " select new jpabook.jpashop.repository.order.query.OrderItemQueryDto" +
                                "(oi.order.id, i.name, oi.orderPrice, oi.count)" +
                                " from OrderItem oi " +
                                " join oi.item i " +
                                " where oi.order.id in :orderIds", OrderItemQueryDto.class)
                .setParameter("orderIds", orderIds)
                .getResultList();

        Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
                .collect(Collectors.groupingBy(OrderItemQueryDto -> OrderItemQueryDto.getOrderId()));

        return orderItemMap;
}

 

Where in 조건절을 이용해 Order과 1:N 관계인 OrderItem을 조회합니다. 이 과정에서 각 OrderItem에 대한 정보를 담은 OrderItemQueryDto를 생성합니다. 이후 스트림을 사용하여 orderId별로 Map 자료형으로 변환하며, Collectors.groupingBy 메서드를 활용하여 각 orderId에 해당하는 OrderItemQueryDto 리스트를 생성합니다.

 

 

3. for문을 이용해 DTO에 컬렉션 필드 데이터 추가

for (OrderQueryDto o : result) {
            o.setOrderItems(orderItemMap.get(o.getOrderId()));
}

 

 

전체 코드

public List<OrderQueryDto> findAllByDto() {
        List<OrderQueryDto> result = findOrders();

        List<Long> orderIds = toOrderIds(result);

        Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(orderIds);

        for (OrderQueryDto o : result) {
            o.setOrderItems(orderItemMap.get(o.getOrderId()));
        }

        return result;
}

 

 

결론

JPA 성능 최적화 1,2,3 결론

1. 엔티티를 직접 반환하지 말고 DTO를 반환

2. 페치조인으로 쿼리 수를 최적화      만약 select 절에 속성이 너무 많으면 new 키워드 이용

3. 컬렉션 최적화 시 페이징이 필요하면 @BatchSize를 사용하고, 페이징이 필요하지 않을 경우에는 페치 조인을 사용 

             단, 컬렉션이 하나일 경우에만 페치 조인을 사용

4. 대량의 데이터 조회 시 JPA에서 DTO 직접 반환해서 사용

 

 

참고자료

https://velog.io/@peppermint100/JPA-%EC%84%B1%EB%8A%A5-%ED%8A%9C%EB%8B%9D-2-%EC%BB%AC%EB%A0%89%EC%85%98-%EC%A1%B0%ED%9A%8C-%EC%B5%9C%EC%A0%81%ED%99%94#map%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%B0%A9%EB%B2%95

 

JPA 성능 튜닝 #2 - 컬렉션 조회 최적화

이번엔 컬렉션 조회 즉, 일대다 ManyToOne의 성능 최적화에 대해서 알아보자. 일대다로 조회하면 하나의 엔티티를 가져올 때 그 안에 여러개의 리스트가 프로퍼티로 있게 된다. 이건 Java의 입장이

velog.io